Key Takeaways
- Deno and Node.js both execute JavaScript on C/C++ based runtimes to achieve high performance.
- Deno is a single binary app incompatible with NPM modules, and has no easy way to incorporate native modules in apps.
- WebAssembly provides a way to run high performance code in Deno apps.
- WebAssembly is a safe, portable, and lightweight container for server-side applications.
- The Rust compiler toolchain provides great support for WebAssembly.
The much anticipated Deno project has finally reached v1.0! Deno is created by the original developer of Node.js, Ryan Dahl, to address what he called “10 things I regret about Node.js”.
Deno did away with the NPM and the infamous node_modules
. It is a single binary executable to run applications written in TypeScript and JavaScript.
However, while TypeScript and JavaScript are suitable for the majority of web applications, they could be inadequate for computationally intensive tasks, such as neural network training and inference, machine learning, and cryptography. In fact, Node.js itself often needs to use native libraries for those tasks (e.g., to use openssl for cryptography).
Without an NPM-like system to incorporate native modules, how do we write server-side applications that require native performance on Deno? WebAssembly is here to help! In this article, we will write high performance functions in Rust, compile them into WebAssembly, and run them inside your Deno application.
TL;DR
Clone or fork this Deno starter project template from GitHub. Follow the instructions and you will have your first WebAssembly function (authored in Rust) running in Deno in 5 minutes.
A little background
Node.js is very successful because it gave developers the best of two worlds: the ease of use of JavaScript, especially for event-based asynchronous applications, and the high performance of C/C++. Node.js applications are written in JavaScript, but are executed on a C/C++-based native runtime, including the Google V8 JavaScript engine and many native library modules. Deno aims to repeat this formula for success, but in the process, supports a modern technology stack with TypeScript and Rust.
Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust. -- The deno.land website.
In his famous presentation, “10 things I regret about Node.js”, Node.js creator Ryan Dahl explained the rationale to start over and create Deno as a Node.js competitor or even replacement. Dahl’s regrets are centered around how Node.js manages third-party code and modules.
- The complex build system for linking C modules to Node.js.
- The unnecessary complexities of
package.json
,node_modules
,index.js
, and other NPM artifacts.
As a result, Deno makes some very conscious and opinionated choices for managing dependencies.
- Deno is a single binary executable.
- Applications are authored in TypeScript or JavaScript with dependencies explicitly declared in code as
import
statements with full URL linking to the dependency’s source code. - Deno is not compatible with Node.js modules.
That’s all good. But what about applications that require even higher performance, such as AI-as-a-Service applications that need to execute complex neural network models in sub-seconds? In both Deno and Node.js, many functions are called via a TypeScript or JavaScript API, but are executed as native code authored in Rust or C. In Node.js, there is always the option to call third-party native libraries from the JavaScript API. But we cannot currently do this with Deno?
WebAssembly support in Deno
WebAssembly is a lightweight virtual machine designed to execute portable bytecode at near native speed. You can compile Rust or C/C++ functions to WebAssembly bytecode, and access those functions from TypeScript. For some tasks, it could be much faster than executing equivalent functions authored in TypeScript itself. For example, this IBM study found that Rust and WebAssembly could improve Node.js execution speed by 1200% to 1500% for certain data processing algorithms.
Deno uses the Google V8 engine internally. V8 is not only a JavaScript runtime, but also a WebAssembly virtual machine. WebAssembly is supported in Deno out of the box. Deno provides an API for your TypeScript application to call functions in WebAssembly.
In fact, some popular Deno components are already implemented in WebAssembly. For example, the sqlite module in Deno is created by compiling sqlite’s C source code into WebAssembly using Emscripten. The Deno WASI component enables WebAssembly applications to access the underlying operating system resources, such as the file system. In this article, I will teach you how to write high performance Deno applications in Rust and WebAssembly.
Set up
The first step of course is to install Deno! On most systems, it is just a single command.
$ curl -fsSL https://deno.land/x/install/install.sh | sh
Since we are writing functions in Rust, you also need to install Rust language compilers and tools.
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Finally, the ssvmup tool automates the build process and generates all the artifacts to make it easy for your Deno applications to call Rust functions. Again, a single command installs the ssvmup dependency.
$ curl https://raw.githubusercontent.com/second-state/ssvmup/master/installer/init.sh -sSf | sh
Note: The ssvmup uses wasm-bindgen
to automatically generate the “glue” code between JavaScript and Rust source code so that they can communicate using their native data types. Without it, the function arguments and return values would be limited to very simple types (i.e., 32 bit integers) supported natively by WebAssembly. For example, strings or arrays would not be possible without ssvmup and wasm-bindgen
.
Hello world
To get started, let's look into a hello world example adopted from the Deno hello world example. You can get the hello world source code and application template from GitHub.
The Rust function is in the src/lib.rs file and simply prepends “hello” to an input string. Notice that the say()
function gets annotated with #[wasm_bindgen]
, allowing ssvmup to generate the necessary “plumbing” to call it from TypeScript.
#[wasm_bindgen]
pub fn say(s: &str) -> String {
let r = String::from("hello ");
return r + s;
}
The Deno application exists in the deno/server.ts file. The application imports the Rust say()
function from the pkg/functions_lib.js file, which gets generated by the ssvmup tool. The functions_lib.js
file name depends on the Rust project name defined in the Cargo.toml file.
import { serve } from "https://deno.land/std/http/server.ts";
import { say } from '../pkg/functions_lib.js';
type Resp = {
body: string;
}
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
let r = {} as Resp;
r.body = say (" World\n");
req.respond(r);
}
Now, let's run ssvmup to build the Rust function into a Deno WebAssembly function.
$ ssvmup build --target deno
After ssvmup successfully completes, you can inspect the pkg/functions_lib.js
file to see how the Deno WebAssembly API gets used to execute the compiled WebAssembly file pkg/functions_lib.wasm
.
Next, run the Deno application. Deno requires permissions to read the file system since it needs to load the WebAssembly file, and to access the network since it needs to receive and respond to HTTP requests.
$ deno run --allow-read --allow-net --allow-env --unstable deno/server.ts
Note: If you have installed Deno in the past and encounter an error here, it is likely caused by a conflict of cached library versions. Follow the instructions to reload your Deno cache.
In another terminal window, you can now access the Deno web application to make it say hello over an HTTP connection!
$ curl http://localhost:8000/
hello World
A more complex example
The starter template project includes several more elaborate examples to show how to pass complex data between the Deno TypeScript and Rust functions. Here are some additional Rust functions in src/lib.rs. Notice that each of them is annotated with #[wasm_bindgen]
.
#[wasm_bindgen]
pub fn obfusticate(s: String) -> String {
(&s).chars().map(|c| {
match c {
'A' ..= 'M' | 'a' ..= 'm' => ((c as u8) + 13) as char,
'N' ..= 'Z' | 'n' ..= 'z' => ((c as u8) - 13) as char,
_ => c
}
}).collect()
}
#[wasm_bindgen]
pub fn lowest_common_denominator(a: i32, b: i32) -> i32 {
let r = lcm(a, b);
return r;
}
#[wasm_bindgen]
pub fn sha3_digest(v: Vec<u8>) -> Vec<u8> {
return Sha3_256::digest(&v).as_slice().to_vec();
}
#[wasm_bindgen]
pub fn keccak_digest(s: &[u8]) -> Vec<u8> {
return Keccak256::digest(s).as_slice().to_vec();
}
Perhaps the most interesting is the create_line()
function. It takes two JSON strings, each representing a Point
struct, and returns a JSON string representing a Line
struct. Notice that both the Point
and Line
structs are annotated with Serialize
and Deserialize
so that the Rust compiler automatically generates necessary code to support their conversion to and from JSON strings.
use wasm_bindgen::prelude::*;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Point {
x: f32,
y: f32
}
#[derive(Serialize, Deserialize, Debug)]
struct Line {
points: Vec<Point>,
valid: bool,
length: f32,
desc: String
}
#[wasm_bindgen]
pub fn create_line (p1: &str, p2: &str, desc: &str) -> String {
let point1: Point = serde_json::from_str(p1).unwrap();
let point2: Point = serde_json::from_str(p2).unwrap();
let length = ((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y)).sqrt();
let valid = if length == 0.0 { false } else { true };
let line = Line { points: vec![point1, point2], valid: valid, length: length, desc: desc.to_string() };
return serde_json::to_string(&line).unwrap();
}
#[wasm_bindgen]
pub fn say(s: &str) -> String {
let r = String::from("hello ");
return r + s;
}
Next, let's examine the JavaScript program deno/test.ts which shows how to call the Rust functions. As you can see String
and &str
are simply strings in JavaScript, i32
are numbers, and Vec<u8>
or &[8]
are JavaScript Uint8Array
. JavaScript objects need to go through JSON.stringify()
or JSON.parse()
before getting passed into or returned from Rust functions.
import { say, obfusticate, lowest_common_denominator, sha3_digest, keccak_digest, create_line } from '../pkg/functions_lib.js';
const encoder = new TextEncoder();
console.log( say("SSVM") );
console.log( obfusticate("A quick brown fox jumps over the lazy dog") );
console.log( lowest_common_denominator(123, 2) );
console.log( sha3_digest(encoder.encode("This is an important message")) );
console.log( keccak_digest(encoder.encode("This is an important message")) );
var p1 = {x:1.5, y:3.8};
var p2 = {x:2.5, y:5.8};
var line = JSON.parse(create_line(JSON.stringify(p1), JSON.stringify(p2), "A thin red line"));
console.log( line );
After running ssvmup to build the Rust library, running deno/test.ts in the Deno runtime produces the following output:
$ ssvmup build --target deno
... Building the wasm file and JS shim file in pkg/ ...
$ deno run --allow-read --allow-env --unstable deno/test.ts
hello SSVM
N dhvpx oebja sbk whzcf bire gur ynml qbt
246
Uint8Array(32) [
87, 27, 231, 209, 189, 105, 251, 49,
... ...
]
Uint8Array(32) [
126, 194, 241, 200, 151, 116, 227,
... ...
]
{
points: [ { x: 1.5, y: 3.8 }, { x: 2.5, y: 5.8 } ],
valid: true,
length: 2.2360682,
desc: "A thin red line"
}
What’s next
We can now create Rust functions and access them from a Deno TypeScript application. You can put a lot of computationally intensive tasks in Rust functions, and offer high performance and safe web services through Deno. Examples of such services include machine learning and image recognition.
Furthermore, you can access system resources such as random numbers, environment variables, and the file system through the WebAssembly Systems Interface (WASI) from your Deno application.
About the author
Dr. Michael Yuan is the author of five books on software engineering. His latest book, Building Blockchain Apps, was published by Addison-Wesley in Dec 2019. Dr. Yuan is the co-founder of Second State, a startup that brings WebAssembly and Rust technologies to cloud, blockchain, and AI applications. Second State enables developers to deploy fast, safe, portable, and serverless Rust functions on Node.js. Stay in touch by subscribing to the WebAssembly.Today newsletter.