2.5 OPs

Introduction

Operations (OPs) extend Deno's capabilities beyond the ECMAScript specification. Unlike OPs, the V8 engine operates within a limited environment that strictly follows ECMAScript guidelines, unable to perform tasks like reading files, managing sockets, and handling timers on its own. These tasks require external APIs to function. OPs bridge this gap, expanding Deno's functionality beyond V8's limitations. Let's consider a simple example: extracting an environment variable in Deno.

To achieve this, an OP interacts with the underlying operating system, retrieves the variable's value (or perform the desired operation), and makes it accessible to your JavaScript or TypeScript code. By providing access to system resources, OPs enable Deno to interact with its environment beyond ECMAScript's scope.

OPs play a crucial role in enhancing Deno's capabilities, allowing it to perform tasks that would otherwise be impossible within the V8 engine's constrained environment.

Types of OPs

Operations (OPs) can be categorized into two main types: synchronous ops and asynchronous ops.

Synchronous OPs:

  • Run continuously from start to finish without interruption.

  • When executed, the script's execution is paused until the operation is complete and a result is obtained.

  • This means that synchronous ops block the main thread until they finish processing.

Asynchronous OPs:

  • Scheduled to produce a result at a later time.

  • Results become available later and are processed without blocking the main thread.

  • Unlike synchronous OPs, asynchronous OPs do not halt program execution, allowing for non-blocking behavior.

Synchronous operations (ops) block the current thread's progress until the desired result is achieved. In contrast, asynchronous ops allow the program to continue running without waiting for the result.

To implement asynchronous ops, Deno uses the Tokio runtime, which employs asynchronous green threads. These threads enable the execution of asynchronous ops without impeding the overall program flow. We will explore Tokio's workings in more detail later.

Note that some operations can be both synchronous and asynchronous, depending on their nature. Others are exclusively synchronous ops. The classification of an operation depends on its specific characteristics. For example, consider the "getRandomValues()" function in the crypto module. This function is only available as a synchronous op through "op_get_random_values()." Since this function involves intensive CPU computations, there is no need to provide an asynchronous version.

List of OPs

Let's review the list of services provided by Ops. Note that this list is not exhaustive, but rather a selection of some of the services offered::

  • Crypto

  • Fetch

  • HTTP client

  • FS ops

    • open

    • seek

    • mkdir

    • chmod

    • stat

    • read dir

    • etc. (the list if quite long)

  • Net

    • accept

    • connect

    • shutdown

    • listen

    • etc.

  • OS

    • exit

    • env

    • set/get env

    • hostname

    • load average

    • memory info

    • CPU info

  • Permissions

    • query

    • revoke

    • request

  • Open plugin

  • Process

    • run

    • kill

  • Runtime

    • compile

    • transpile

  • Signal

    • Bind

    • Unbind

    • Poll

  • Timer

    • Start

    • Stop

    • Now

    • Sleep sync

  • TLS

    • start

    • connect

    • listen

    • accept

  • Worker

    • Create a worker

    • Terminate worker

    • Get message

    • Post message

    • close

  • WebSocket

    • Create

    • Send

    • Close

    • Next event

The list of operations is quite long, and a notable pattern emerges upon closer inspection. Most operations have a direct connection to the functions provided by the JavaScript component of the runtime, with many forming a one-to-one correspondence. This relationship can be seen in examples such as the "kill()" function in the Process module, which directly aligns with the "op_kill()" operation. Similarly, the "openSync()" function used for file operations has a corresponding "op_open_sync()" operation. This parallel between JavaScript functions and their associated operations highlights the strong link between Deno's runtime services and its JavaScript foundation.

Example

Let's consider the Deno.env.get() function in Deno as a representative example. This function is a simple and uncomplicated API within the Deno programming environment. When we refer to it as a "sync" function, we mean that the JavaScript thread - the sequence of instructions being executed - will temporarily pause its execution until the desired outcome is obtained from a lower-level operation. In other words, the JavaScript thread will wait until the operation is complete before continuing to execute.

User program

Retrieving the value of an environment variable in Deno is a simple process. Let's demonstrate how to retrieve the value of the "HOME" variable with a concise code example:

Deno.env.get('HOME');

Deno code

The "env" object provides several useful APIs, including "get", "set", "delete", and "toObject". Upon closer inspection, it becomes clear that the Deno.env.get method is equivalent to a function called "getEnv".

Deno.env
{
  get: [Function: getEnv],
  toObject: [Function: toObject],
  set: [Function: setEnv],
  has: [Function: has],
  delete: [Function: deleteEnv]
}

The getEnv functions is part of the OS service:

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

The getEnv() function is relatively simple, serving as a wrapper for the lower-level op_get_env() operation. This operation, implemented in Rust, performs the underlying work. While exploring how Deno's JavaScript environment triggers this low-level Rust operation (referred to as "OP") can be fascinating, we'll save that discussion for later. For now, let's proceed to examine the actual code behind op_get_env, which is written in Rust. By doing so, we'll gain insight into its internal mechanics and functionality, helping us better understand the underlying mechanics that drive this aspect of Deno's functionality.

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)
}

The Rust code is relatively simple, performing a series of validations in a single step. Then, it uses the convenient std::env crate to access the environment variable. The result is then returned as a value in the response. Interestingly, this response travels from the Rust code to the JavaScript code, a connection that will be thoroughly explored in the upcoming chapters of this book. This transition from Rust to JavaScript is a crucial aspect of Deno's functionality, and understanding its intricacies is essential for appreciating the overall architecture of Deno.

--

We have now covered the fundamentals of ops. In Chapter 6, we will further explore the intricacies of ops, gaining a thorough understanding of the bidirectional transition between JavaScript and Rust. This language switching mechanism is essential to the Deno environment, and in the next chapter, we will dissect its step-by-step mechanics, revealing the underlying dynamics that facilitate seamless communication between the two programming languages.

Last updated