There are many reasons to use Rust libraries in your Node application. Since Rust primitives allow us to write code that results in a smaller memory fingerprint, it can be a way to mitigate performance issues in certain parts of the Node app. Additionally, it can unlock a more efficient way of parallelization and thread management. And sometimes it is the only way to access low level OS functionalities. Considering all those benefits and Rust’s rich package ecosystem, it is a solid way to boost any Node application.
But all those benefits don’t come for free. It can be quite tricky to figure out how to bring two runtimes together and bridge the gap. Node provides Node-API (also known as N-API) for that specific purpose. It is a stable API to provide an Application Binary Interface (ABI). With this, it is possible to create and modify JS values in the target runtime. Although Node-API is specifically for C language, thanks to Rust’s C ABI, we can also use Rust to interact with Node-API.
Neon is a Rust package which provides an interface for Node-API. We can use it to make it easier to deal with Node-API in Rust.
Let's start by creating a Rust library project:
cargo new --lib math
And we can add neon as a dependency to the project by modifying Cargo.toml
:
[package]
name = "math"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
[dependencies.neon]
version = "0.10"
default-features = false
features = ["napi-6"]
In the following steps, we'll create a Rust function which returns a sequence of numbers starting from 0 to 10. We'll be updating src/lib.rs
as shown below:
use neon::prelude::*;
fn get_sequence(mut cx: FunctionContext) -> JsResult<JsArray> {
let array = JsArray::new(&mut cx, 10);
for i in 0..10 {
let n = JsNumber::new(&mut cx, i);
array.set(&mut cx, i, n)?;
}
return Ok(array);
}
Neon provides the primitive value types like JsBool
, JsString
, JsNumber
, JsNull
, JsUndefined
, JsArray
and JsObject
. With those it is possible to convert data types in Rust to Node runtime equivalents.
As a second step, we can read argument values that are passed to the custom function we are created. Let's set the size of the sequence with an argument:
fn get_sequence(mut cx: FunctionContext) -> JsResult<JsArray> {
let size = cx.argument::<JsNumber>(0)?.value(&mut cx) as u32;
let array = JsArray::new(&mut cx, size);
for i in 0..size {
let n = JsNumber::new(&mut cx, i);
array.set(&mut cx, i, n)?;
}
return Ok(array);
}
And as a final step, we set a main neon function to register the functions we want to expose:
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("getSequence", get_sequence)?;
Ok(())
}
Our library is ready to be compiled. To create the dynamically linked library, we can run the cargo command below:
cargo build --release
After a successful compilation step, cargo
creates the binary file of the library in the target/release
directory. File name is operating system dependant, but for Linux the file name extension will be .so
and for Windows it will be .dll
.
We can rename and copy the library to the root directory like this:
cp /target/release/libmath.so index.node
And finally we can use the library as if it is a native JS module:
const math = require('./index.node');
console.log(math.getSequence(4));
// [0, 1, 2, 3]
Node doesn't support importing native modules from ES Modules with import syntax (ESM Spec). However, it is possible to use module
library to load the Rust library.
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const math = require('./index.node');
console.log(math.getSequence(4));
// [0, 1, 2, 3]
Neon is a very handy tool for empowering Node applications with Rust. It makes it trivial to use Rust in Node applications. There are also a handful of examples in Neon's Github Organization including the ones that exposes async functions. Nonetheless it must be mentioned that if the goal is to improve performance and efficiency, always the benchmarks should be taken into account to make decisions. The abstractions that are used might nullify the benefits of using Rust.