Getting started

High-performance Rust + JavaScript hybrid apps in Node.js

There are great use cases for WebAssembly on the server-side, especially for AI, blockchain, and big data applications. In this tutorial, I will show you how to incorporate WebAssembly functions, written in Rust, into Node.js applications on the server. The Rust + Node.js hybrid apps combine Rust's performance, WebAssembly's security and portability, and JavaScript's ease-of-use. A typical Rust + Node.js hybrid app works like this.

The host application is a Node.js web application written in JavaScript. It makes WebAssembly function calls.

The WebAssembly bytecode program is written in Rust. It runs inside the SSVM, and is called from the Node.js web application.

The ssvmup npm module installs the Second State Virtual Machine (SSVM) into Node.js as a native addon, and provides the necessary compiler tools. Follow the steps below to install Rust and the ssvmup tool.

# Install ssvmup toolchain

$ npm install -g ssvmup # Append --unsafe-perm if permission denied

​

# Install the nodejs addon for SSVM

$ npm install ssvm

WebAssembly program in Rust

In this example, our Rust program appends the input string after “hello”. Let’s create a new cargo project. Since this program is intended to be called from a host application, not to run as a stand-alone executable, we will create a hello project.

$ cargo new --lib hello

$ cd hello

Edit the Cargo.toml file to add a [lib] section. It tells the compiler where to find the source code for the library and how to generate the bytecode output. We also need to add a dependency of wasm-bindgen here. It is the utility ssvmup uses to generate the JavaScript binding for the Rust WebAssembly program.

[lib]

name = "hello_lib"

path = "src/lib.rs"

crate-type =["cdylib"]

​

[dependencies]

wasm-bindgen = "=0.2.61"

Below is the content of the Rust program src/lib.rs. You can actually define multiple external functions in this library file, and all of them will be available to the host JaveScript app via WebAssembly.

use wasm_bindgen::prelude::*;

​

#[wasm_bindgen]

pub fn say(s: String) -> String {

let r = String::from("hello ");

return r + &s;

}

Next, you can compile the Rust source code into WebAssembly bytecode and generate the accompanying JavaScript module for the Node.js host environment.

$ ssvmup build --nowasi

The result are files in the pkg/ directory. the .wasm file is the WebAssembly bytecode program, and the .js files are for the JavaScript module.

The Node.js host application

Next, go to the node folder and examine the JavaScript program app.js. With the generated hello_lib.js module, it is very easy to write JavaScript to call WebAssembly functions. Below is the node application app.js. It simply imports the say() function from the generated module. The node application takes the name parameter from incoming an HTTP GET request, and responds with “hello name”.

const { say } = require('../pkg/hello_lib.js');

​

const http = require('http');

const url = require('url');

const hostname = '127.0.0.1';

const port = 8080;

​

const server = http.createServer((req, res) => {

const queryObject = url.parse(req.url,true).query;

res.statusCode = 200;

res.setHeader('Content-Type', 'text/plain');

res.end(say(queryObject['name']));

});

​

server.listen(port, hostname, () => {

console.log(`Server running at http://${hostname}:${port}/`);

});

Start the Node.js application server as follows.

$ node app.js

Server running at http://127.0.0.1:8080/

Then, you can test it.

$ curl http://127.0.0.1:8080/?name=Wasm

hello Wasm

More complex examples

Besides passing string values between Rust and JavaScript, the ssvmup tool supports the following data types.

Rust call parameters can be any combo of i32, String, &str, Vec<u8>, and &[u8]

Return value can be i32 or String or Vec<u8>

For complex data types, such as structs, you could use JSON strings to pass data.

The Rust program src/lib.rs in the functions example demonstrates how to pass in call arguments in various supported types, and return values.

#[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.

Next, let's examine the JavaScript program app.js. It 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 being passed into or returned from Rust functions.

What’s next?

Now we have seen a very simple example to call a Rust function from JavaScript in a Node.js application. In the next article, we will discuss how to pass arbitrary arguments from a JavaScript program to Rust.