Example: Adding to Deno API

In this example, we will walk through a simple example that adds the functionality of generating a random number inside of a range.

(This is just an example. You can definitely implement with JavaScript Math.random if you want.)

Define Messages

As mentioned in previous sections, Deno relies on message passing to communicate between TypeScript to Rust and do all the fancy stuff. Therefore, we must first define the messages we want to send and receive.

Go to src/msg.fbs and do the following:

src/msg.fbs
union Any {
    // ... Other message types, omitted
    // ADD THE FOLLOWING LINES
    RandRange,
    RandRangeRes
}

// ADD THE FOLLOWING TABLES
table RandRange {
  from: int32;
  to: int32;
}

table RandRangeRes {
  result: int32;
}

With such code, we tell FlatBuffers that we want 2 new message types, RandRange and RandRangeRes. RandRange contains the range from and to we want. RandRangeRes contains a single value result that represents the random number we will get from the Rust side.

Now, run ./tools/build.py to update the corresponding serialization and deserialization code. Such auto-generated code are store in target/debug/gen/msg_generated.ts and target/debug/gen/msg_generated.rs, if you are interested.

Add Frontend Code

Go to js/ folder. We will now create the TypeScript frontend interface.

Create a new file, js/rand_range.ts. Add the following imports:

js/rand_range.ts
import * as msg from "gen/msg_generated";
import * as flatbuffers from "./flatbuffers";
import { assert } from "./util";
import * as dispatch from "./dispatch";

We will be using dispatch.sendAsync and dispatch.sendSync to dispatch the request as async or sync operation.

Since we are dealing with messages, we need to do some serialization and deserialization work. For request, we need to put user provided from and to to the table RandRange (as defined above); for responses, we need to extract the result from the table RandRangeRes.

Therefore, let's define 2 functions, req and res, that does the work:

js/rand_range.ts
function req(
  from: number,
  to: number,
): [flatbuffers.Builder, msg.Any, flatbuffers.Offset] {
  // Get a builder to create a serialized buffer
  const builder = flatbuffers.createBuilder();
  msg.RandRange.startRandRange(builder);
  // Put stuff inside the buffer!
  msg.RandRange.addFrom(builder, from);
  msg.RandRange.addTo(builder, to);
  const inner = msg.RandRange.endRandRange(builder);
  // We return these 3 pieces of information.
  // dispatch.sendSync/sendAsync will need these as arguments!
  // (treat such as boilerplate)
  return [builder, msg.Any.RandRange, inner];
}

function res(baseRes: null | msg.Base): number {
  // Some checks
  assert(baseRes !== null);
  // Make sure we actually do get a correct response type
  assert(msg.Any.RandRangeRes === baseRes!.innerType());
  // Create the RandRangeRes template
  const res = new msg.RandRangeRes();
  // Deserialize!
  assert(baseRes!.inner(res) !== null);
  // Extract the result
  return res.result();
}

Great! With req and res defined, we can very easily define the actual sync/async APIs:

js/rand_range.ts
// Sync
export function randRangeSync(from: number, to: number): number {
  return res(dispatch.sendSync(...req(from, to)));
}

// Async
export async function randRange(from: number, to: number): Promise<number> {
  return res(await dispatch.sendAsync(...req(from, to)));
}

Expose the Interface

Go to js/deno.ts and add the following line:

js/deno.ts
export { randRangeSync, randRange } from "./rand_range";

Also, go to BUILD.gn and add our file to ts_sources:

BUILD.gn
ts_sources = [
  "js/assets.ts",
  "js/blob.ts",
  "js/buffer.ts",
  # ...
  "js/rand_range.ts"
]

Add Backend Code

Go to src/ops.rs. This is the only Rust backend file we need to modify to add this functionality.

Inside ops.rs, let's first import the utility for generating random numbers. Fortunately, Deno already includes a Rust crate called rand that could handle the job. See more about its API here.

Let's import it:

src/ops.rs
use rand::{Rng, thread_rng};

Now, we can create a function (with some boilerplate code) that implements the random range logic:

src/ops
fn op_rand_range(
  _state: &IsolateState,
  base: &msg::Base,
  data: libdeno::deno_buf,
) -> Box<Op> {
  assert_eq!(data.len(), 0);
  // Decode the message as RandRange
  let inner = base.inner_as_rand_range().unwrap();
  // Get the command id, used to respond to async calls
  let cmd_id = base.cmd_id();
  // Get `from` and `to` out of the buffer
  let from = inner.from();
  let to = inner.to();

  // Wrap our potentially slow code and respond code here
  // Based on dispatch.sendSync and dispatch.sendAsync,
  // base.sync() will be true or false.
  // If true, blocking() will spawn the task on the main thread
  // Else, blocking() would spawn it in the Tokio thread pool
  blocking(base.sync(), move || -> OpResult {
    // Actual random number generation code!
    let result = thread_rng().gen_range(from, to);
    
    // Prepare respond message serialization
    // Treat these as boilerplate code for now
    let builder = &mut FlatBufferBuilder::new();
    // We want the message type to be RandRangeRes
    let inner = msg::RandRangeRes::create(
      builder,
      &msg::RandRangeResArgs {
        result, // put in our result here
      },
    );
    // Get message serialized
    Ok(serialize_response(
      cmd_id, // Used to reply to TypeScript if this is an async call
      builder,
      msg::BaseArgs {
        inner: Some(inner.as_union_value()),
        inner_type: msg::Any::RandRangeRes,
        ..Default::default()
      },
    ))
  })
}

After completing our implementation, let's tell Rust when we should invoke it.

Notice there is a function called dispatch in src/ops.rs. Inside, we could find a match statement on message types to set corresponding handlers. Let's set our function to be the handler of the msg::Any::RandRange type, by inserting this line:

src/ops.rs
pub fn dispatch(
    // ...
) -> (bool, Box<Op>) {
    // ...
    let op_creator: OpCreator = match inner_type {
        msg::Any::Accept => op_accept,
        msg::Any::Chdir => op_chdir,
        // ...
        /* ADD THE FOLLOWING LINE! */
        msg::Any::RandRange => op_rand_range,
        // ...
        _ => panic!(format!(
            "Unhandled message {}",
            msg::enum_name_any(inner_type)
          )),
    }
}

Done! This is all the code you need to add for this call!

Run ./tools/build.py to build your project! You would now be able to call deno.randRangeSync and deno.randRange .

Try it out in the terminal!

$ ./target/debug/deno
> deno.randRangeSync(0, 100)
96
> (async () => console.log(await deno.randRange(0, 100)))()
Promise {}
> 74

Congrats!

Add Tests

Now let's add a basic test for our code.

Create a file js/rand_range_test.ts:

js/rand_range_ts.ts
import { test, assert } from "./test_util.ts";
import * as deno from "deno";

test(function randRangeSync() {
  const v = deno.randRangeSync(0, 100);
  assert(0 <= v && v < 100);
});

test(async function randRange() {
  const v = await deno.randRange(0, 100);
  assert(0 <= v && v < 100);
});

Include this test in js/unit_tests.ts:

js/unit_tests.ts
// ADD THIS LINE
import "./rand_range_test.ts";

Now, run ./tools/test.py. From all the tests, you will be able to find that your tests are passing:

test randRangeSync_permW0N0E0R0
... ok
test randRange_permW0N0E0R0
... ok

Submit a PR!

Let's suppose that Deno really needs this functionality and you are resolved to contribute to Deno. You can submit a PR!

Remember to run the following commands to ensure your code is ready for submit!

./tools/format.py
./tools/lint.py
./tools/build.py
./tools/test.py

Last updated