6.6 Registration of ops

Operations, often referred to as "Ops," play a crucial role in Deno. Ops act as the primary connectors between JavaScript and Rust, effectively forming a bridge between the two. This interaction happens back and forth, allowing seamless communication. Ops are essential because V8 strictly adheres to the ECMAScript specification, the standard for JavaScript. Any functionality that goes beyond this specification must be provided as an external reference. Even fundamental actions like using console.log require an external op call, as the ECMAScript specification doesn't encompass console output.

In the previous chapter, we explored how external references are registered with V8. We will now quickly revisit that concept. Additionally, we will look into built-in and ext ops. This section will specifically focus on the registration process for ops. Later on, we will examine how ops are triggered during runtime and how the system manages their responses.

Overview

In short:

Ops are low-level functions that are implemented in Rust to support the high-level functions in JS.

For example, crypto.randomUUID is a high-level function, and the corresponding Rust op is implemented by op_crypto_random_uuid.

The high-level JS function is;

randomUUID() {
  webidl.assertBranded(this, CryptoPrototype);
  return ops.op_crypto_random_uuid();
}

The corresponding low-level OP is:

pub fn op_crypto_random_uuid(state: &mut OpState) -> Result<String, AnyError> {
  let maybe_seeded_rng = state.try_borrow_mut::<StdRng>();
  let uuid = if let Some(seeded_rng) = maybe_seeded_rng {
    let mut bytes = [0u8; 16];
    seeded_rng.fill(&mut bytes);
    uuid::Builder::from_bytes(bytes)
      .with_version(uuid::Version::Random)
      .into_uuid()
  } else {
    uuid::Uuid::new_v4()
  };

  Ok(uuid.to_string())
}

In Deno, operations (OPs) typically have a one-to-one mapping. This means that one JavaScript API directly corresponds to one Rust operation (OP). All the operations (OPs) follow a consistent pattern, from the initial setup to processing the result. The following outlines the typical phases of an operation (OP):

Deno operations progress through various stages. The diagram above illustrates the standard operation state machine. Let's examine each state:

  1. Register handler

    • In this step, an operation handler is registered.

  2. Waiting to get called

    • This state resembles an idle condition where the operation awaits a call.

  3. Dispatch

    • Dispatch involves moving across the bridge from V8 to Deno.

  4. Execute

    • Deno executes the operation's handler.

  5. Process result

    • The outcome is sent back to V8, where it undergoes processing in JavaScript.

    • This step is akin to returning from the bridge.

The process of dispatching and result processing happens during runtime. We will discuss the details later. For now, our focus is on registering operations.

Types of Ops

All the operations can be divided into two main categories:

  1. Sync Operations:

    • In sync operations, the request and response are processed one after the other, in a step-by-step manner.

    • During the execution of sync operations, the process is blocked, meaning it waits for each step to complete before moving forward.

  2. Async Operations:

    • Async operations, on the other hand, handle requests and responses asynchronously, which means they can process multiple tasks concurrently.

    • The execution of async operations is not blocked, allowing the program to continue with other tasks while waiting for certain operations to finish.

Certain operations fall distinctly into either of these categories, while some operations can belong to both. For instance, operations like getting and setting environment variables are categorized as sync operations. Another example involves reading a file, which can involve both sync and async operations, depending on the specific operation and the user's choice.

The suitability of async operations varies based on the nature of the operation. Some operations are better suited for sync processing, and thus only have sync variants. On the other hand, for operations like reading a file, the decision to use sync or async mode is up to the user. If dealing with a particularly large file, the user might opt for async reading to improve efficiency.

Registration

Op registration happens at startup. Deno registers all of its low-level ops right at the process startup. There two sources of OPs:

  • Built-in OPs: These OPs are part of Deno core. These cover the most basic & fundamental OPs. Some examples are - op_print, op_resources, op_read, op_write, op_encode, op_decode, etc.

  • EXT OPs: These OPs come from Deno's ext modules. As we know that the ext modules are a close combination of JS APIs alongwith their low-level OPs. The OPs coming from EXT usually have the 1:1 mapping from JS API to the OP. Some examples are - op_fetch, op_crypto_sign_key, op_crypto_get_random_values, etc.

The code in the main worker below registers all the OPs with V8 as external references:

Let's look at the master function that register all the Ops with V8:

Although the code of OPs is in Rust, they're invoked from JS space. Therefore, each OP has a corresponding registration function in JS too. This also comes from Deno core.

The call to registerOp called from Rust code:

At the time of writing, there are 478 ops that get registered. Here is the complete list:

Once again, the call to OP happens like this:

--

That was all about ops registration. Let's move on to module evaluation.

Last updated