6.8 Sync OPs

Overview

Synchronous operations halt the ongoing execution of the current thread (whether it's the main thread or a worker) until a result becomes available. Many operations in Deno are of the synchronous kind. File system operations are the exception, as they are available in both synchronous and asynchronous forms. This distinction exists because file system operations can consume a significant amount of time. For other scenarios, either synchronous operations are not logical or the operations are so straightforward that implementing them as asynchronous would be excessive.

In our example, we will focus on a synchronous operation called Deno.env.get. This operation is uncomplicated yet highly instructive for grasping the underlying principles. The purpose of this operation is to take an input and produce a corresponding output. To thoroughly comprehend how this operation functions from start to finish, we will dissect it step by step. Our exploration will encompass both JavaScript and Rust implementations.

JS part

User code

In the JS space, the application makes a call to get the value of an environment variable named HOME:

const v = Deno.env.get('HOME');

Deno.env.get

The Deno.env.get function is mapped to getEnv:

const env = {
  get: getEnv,
  toObject() {
    return ops.op_env();
  },
  set: setEnv,
  has(key) {
    return getEnv(key) !== undefined;
  },
  delete: deleteEnv,
};

getEnv

This immediately calls the JS function getEnv as env.get is mapped to getEnv:

function getEnv(key) {
  return ops.op_get_env(key) ?? undefined;
}

OPS

The Deno core gives us the global ops object. As we discussed earlier, the ops object is populated using the registerOp API, which is called when Deno starts up. Deno core handles the registration of every supported op. Here's a brief reminder of how the registerOp function looks:

Deno.__op__registerOp = function (isAsync, op, opName) {
  const core = Deno.core;
  if (isAsync) {
    if (core.ops[opName] !== undefined) {
      return;
    }
    core.asyncOps[opName] = op;
    const fn = function (...args) {
      if (this !== core.ops) {
        // deno-lint-ignore prefer-primordials
        throw new Error(
          "An async stub cannot be separated from Deno.core.ops. Use ???",
        );
      }
      return core.asyncStub(opName, args);
    };
    fn.name = opName;
    core.ops[opName] = fn;
  } else {
    core.ops[opName] = op;
  }
};

The __op__registerOp function is called during the initialization of context. There are three inputs for each op:

  • isAsync: True if the op is async

  • Op: This is the Rust-side ID of the op

  • OpName: The name of the OP

for op_ctx in op_ctxs {
    if op_ctx.decl.enabled {
      _ = writeln!(
        codegen,
        "Deno.__op__registerOp({}, opFns[{}], \"{}\");",
        op_ctx.decl.is_async, op_ctx.id, op_ctx.decl.name
      );
    } else {
      _ = writeln!(
        codegen,
        "Deno.__op__unregisterOp({}, \"{}\");",
        op_ctx.decl.is_async, op_ctx.decl.name
      );
    }
  }

Rust code

As op is registered as an external reference, V8 makes an external function call (like C extern functions).

fn op_get_env(
  state: &mut OpState,
  key: String,
) -> Result<Option<String>, AnyError> {
  let skip_permission_check = NODE_ENV_VAR_ALLOWLIST.contains(&key);

  if !skip_permission_check {
    state.borrow_mut::<PermissionsContainer>().check_env(&key)?;
  }

  if key.is_empty() {
    return Err(type_error("Key is an empty string."));
  }

  if key.contains(&['=', '\0'] as &[char]) {
    return Err(type_error(format!(
      "Key contains invalid characters: {key:?}"
    )));
  }

  let r = match env::var(key) {
    Err(env::VarError::NotPresent) => None,
    v => Some(v?),
  };
  Ok(r)
}

--

That was all about sync ops. It is worth noting that the sync ops are quite easy compared to async ops. Async ops are quite complex to implement. The next revision of this book (4th edition) will have a new chapter, Chapter 8, dedicated to async ops.

Last updated