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:
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
functionreq( from:number, to:number,): [flatbuffers.Builder,msg.Any,flatbuffers.Offset] {// Get a builder to create a serialized bufferconstbuilder=flatbuffers.createBuilder();msg.RandRange.startRandRange(builder);// Put stuff inside the buffer!msg.RandRange.addFrom(builder, from);msg.RandRange.addTo(builder, to);constinner=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];}functionres(baseRes:null|msg.Base):number {// Some checksassert(baseRes !==null);// Make sure we actually do get a correct response typeassert(msg.Any.RandRangeRes === baseRes!.innerType());// Create the RandRangeRes templateconstres=newmsg.RandRangeRes();// Deserialize!assert(baseRes!.inner(res) !==null);// Extract the resultreturnres.result();}
Great! With req and res defined, we can very easily define the actual sync/async APIs:
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
fnop_rand_range( _state:&IsolateState, base:&msg::Base, data: libdeno::deno_buf,) ->Box<Op> {assert_eq!(data.len(), 0);// Decode the message as RandRangelet inner = base.inner_as_rand_range().unwrap();// Get the command id, used to respond to async callslet cmd_id = base.cmd_id();// Get `from` and `to` out of the bufferlet 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 poolblocking(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 nowlet builder =&mut FlatBufferBuilder::new();// We want the message type to be RandRangeReslet inner = msg::RandRangeRes::create( builder,&msg::RandRangeResArgs { result, // put in our result here }, );// Get message serializedOk(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:
import { test, assert } from"./test_util.ts";import*as deno from"deno";test(functionrandRangeSync() {constv=deno.randRangeSync(0,100);assert(0<= v && v <100);});test(asyncfunctionrandRange() {constv=awaitdeno.randRange(0,100);assert(0<= v && v <100);});
Include this test in js/unit_tests.ts:
js/unit_tests.ts
// ADD THIS LINEimport"./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!