Node isn’t the fastest framework out here. It’s not the slowest either, v8 is doing wonders to its speed, but nevertheless, if we setup an unfair battle between Node and say Rust; Node will lose.
If you are interested to compare this approach to WebAssembly, I’ve written a new article on WASM in comparison to native modules.
What is Rust?
Rust is a multi-paradigm, high-level, general-purpose programming language designed for performance and safety, especially safe concurrency. Rust is syntactically similar to C++, but can guarantee memory safety by using a borrow checker to validate references.
— Wikipedia
Rust is a strongly typed and compiled language. It supports a lot of, so called, modern features such as closure and anonymous functions, a rich standard library, types and polymorphism as well as modern tooling such as Cargo which is an equivalent to npm
in the world of Node.
Rust is also Memory Safe language. All safe Rust code is memory checked during compilation so unless you opt to use unsafe Rust, you won’t encounter the famous Segmentation Fault
you’d get in C or C++ by accessing a pointer that is pointing to a memory you no longer own.
All this makes Rust a very attractive replacement to C/C++.
Ok great and all, but I write Javascript!
Yeah, me too. And here comes the great part. We can use all the beauty of Rust in Javascript!
Oftentimes we encounter a situation when we need to run a heavy task such as PDF generation or some sort of computation. Most of the modules in NPM are Javascript based, meaning they will suffer a penalty at runtime. Usually in such situation we opt to “creative” solutions such as offloading the heavy process to a Lambda function and turning the entire process into asynchronous for the user. And yet, I rarely hear people suggest or even evaluate the solution of embedding a native module that most of the times will be faster.
Another great usage for Rust in the Javascript ecosystem - is providing shared backend for Desktop applications. Let’s say you are developing an MVP in Electron and want to use SQLite for data storage. While you can use the sqlite3 package which is native C++, you can also create a dynamic library (also knows as .so
or .dll
file in Linux and Windows respectively) in Rust. What this gives you, is the ability to later ditch Electron in favor of Native Framework for each OS while doing 0 changes to your Backend code.
So how do I use Rust from Javascript?
I’m glad you’ve asked! Please meet Neon
.
Neon is a library and toolchain for embedding Rust in your Node.js applications and libraries. It is similar to creating native modules with C/C++ but with none of the fear and headaches associated with unsafe systems programming.
Let’s try now to create a native Fibonacci function in Rust and call it from Node. Disclaimer: This is not a tutorial on Rust, so I’m not going to explain Rust specifics here. You can always go to the Rust Book to learn about Rust, however the syntax I’m gonna use here should be easy to understand to anyone familiar with Typescript, or any other statically typed language like C/C++, Java, Kotlin or C#. So let’s dive in.
Step 1 - Setup a project
Let’s start by creating a Rust project. We will start by creating Cargo.toml
file. It’s a file that mimics the role of package.json
:
[package]
name = "fibonacci_rs"
version = "0.1.0"
edition = "2018"
[lib]
crate-type = ["cdylib"]
[dependencies]
neon = { version = "0.9", default-features = false, features = ["napi-6"] }
First, we define our package. We give it a name, we define the version and what edition of Rust we are going to use.
Then we define the type of our package. Remember, Rust is a compiled language and it can produce all kinds of binary outputs. They could be executables (such as .exe
files in Windows) or they could be library files such as .so
or .dll
. In our case we create a library, hence the [lib]
declaration. There are 2 types of libraries that most languages (including Rust) can produce: dynamic
and static
. In short:
- Static libraries usually have the extension of
.a
or .lib
in Linux/MacOS and Windows respectively, Static libraries are compiled into the final executable. So if I produce a static library and then link my executable against it - the final output will be one file, where the static library is embedded into the executable itself. Their main purpose is to create reusable code but once the binary is compiled, you can’t change the implementation of a static library without recompiling the binary.
- Dynamic libraries have the extensions of
.so
, .dylib
and .dll
in Linux, Mac and Windows respectively. The difference is that dynamic libraries are not compiled into the final binary file and instead are loaded at runtime by the OS. So when I link my binary against a dynamic library, I’ll have 2 files in the output - the dynamic library and my binary. Their main use is to create a reusable code between multiple applications without locking the implementation. Also, since dynamic libraries are not compiled into the executable, I can reuse their code by having any number of executables that depend on the same dynamic libraries. Libraries such as Microsoft C/C++ runtime are dynamic libraries that are downloaded once by the user to their machine, and then used by countless applications that depend on MSVC.
Since we are not going to recompile Node.js itself, we are interested in producing a dynamic library that can be linked at runtime, rather than embedded directly into Node.js runtime. Therefore we define our crate-type
as cdylib
meaning that we want to create a dynamic library with C ABI (hence the “c” in the beginning). I’m not going to go into details what ABI is and what’s its purpose, I’ll instead drop a link to Wikipedia. Note that we can also create dylib
which will have unstable Rust ABI that can be changed across Rust releases.
Last but not least - we define our dependencies. In our case, we depend only on neon
library.
Step 2 - Write our Rust Fibonacci
Since we are creating a rust library, we need to create the following file in the root of our project: src/lib.rs
. In this file we are going to create our Fibonacci function:
fn fibonacci(n: i32) -> i32 {
return match n {
n if n < 1 => 0,
n if n <= 2 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2)
}
}
A simple, recursive function that accepts a 32bit integer and returns a 32bit integer.
Step 3 - Create a binding for our Rust Fibonacci into Javascript
Currently, we’ve created a Rust function. We can run cargo build
in order to compile our code. This will download and compile neon
and any of its dependencies, and will compile our lib.rs
file. We will get a compilation warning saying that fn fibonacci
is dead code, since no body uses it, but nevertheless, if you’ve done everything correctly, you should end up with a new directory tree of target/debug
and inside it you will find a file named libfibonacci_rs.{dylib,so,dll}
(the extension will depend on what type of OS you are compiling on).
Since its a dynamic library that does not extern any functionality, it is now useless. But let’s put some Neon lights onto it. Edit src/lib.rs
and add the following function:
use neon::context::{Context, FunctionContext};
use neon::types::JsNumber;
use neon::result::JsResult;
fn fibonacci(n: i32) -> i32 {
return match n {
n if n < 1 => 0,
n if n <= 2 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2)
}
}
fn fibonacci_api(mut cx: FunctionContext) -> JsResult<JsNumber> {
let handle = cx.argument::<JsNumber>(0).unwrap();
let res = fibonacci(handle.value(&mut cx) as i32);
Ok(cx.number(res))
}
We’ve added some use neon
statements. They are similar to import
or require
statements in Javascript. We’ve also added a new function called fibonacci_api
. This function is the conversion layer between Javascript and Rust. It accepts a FunctionContext
since it’s a function and returns a JsResult
of JsNumber
meaning that on successful execution, it will return a value that is equivalent to number
type in Javascript.
cx
is used to interface with the Javascript world. We first get a Handle
to JsNumber
which is the first (index 0) argument that was passed to our function. Handle
is a safe reference to a Javascript value that is owned and managed by the Javascript engine’s memory management. Since Javascript is a dynamic language and Rust is statically typed, we need to manually downcast the javascript value to a number. Since fibonacci_api
can be called with anything (a string, an object or array) from Javascript, this downcast can fail, therefore we need to handle a situation where it fails. In my example, I just .unwrap
the result of the downcast, since this is a tutorial and not a real world example, but in real world you’d need to handle a situation where the first argument is not a JsNumber
.
Just a few more words on values and down/up casting and we will continue. Everything you get in Rust via Neon, initially has the type of JsValue
. JsValue
is sort of a catch all type that can be anything: JsNumber
, JsObject
, JsNull
, JsBuffer
and etc. A Handle
with JsValue
can be downcasted to a specific type by using Handle::downcast()
or upcasted by using Handle::upcast()
. There is a nice diagram on the neon::types
Documentation that shows the relation between JsValue
and other Javascript types. I’ll include it here:
Anyway, back to the code.
So now we have a Handle<JsNumber>
. We can access the internal value by calling Handle::value()
method, which in case of a JsNumber
handle, returns f64
(since number
in Javascript is represented as float), but we safely convert it to i32
for our Fibonacci function.
We then safely return the result as JsNumber
by calling the .number()
method of the Function Context
. And we are done.
Once again, we can compile the code by running cargo build
. It will once again warn us about dead code, but this time it will mark both fibonacci
and fibonacci_api
function as dead code, since no body calls them.
Step 4 - Export our Rust function to Javascript world
We are almost done with Rust. Bear with me for a little bit more.
Right now we have a function that converts a JS call into Rust call, but we have no way to call that function from Javascript. Enter the main
!
If you are familiar with C or C++, every executable has a main
function. This is a special type of function that the OS will run once it loads your binary into memory. Libraries however do not have main
functions, since libraries are not meant to be executed by themselves. However you’ve probably seen a Javascript module. It usually ends with module.exports = {}
. We can treat it as main
. And that’s what Neon does.
Once again, edit our src/lib.rs
file:
use neon::context::{Context, ModuleContext, FunctionContext};
use neon::types::JsNumber;
use neon::result::JsResult;
use neon::result::NeonResult;
fn fibonacci(n: i32) -> i32 {
return match n {
n if n < 1 => 0,
n if n <= 2 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2)
}
}
fn fibonacci_api(mut cx: FunctionContext) -> JsResult<JsNumber> {
let handle = cx.argument::<JsNumber>(0).unwrap();
let res = fibonacci(handle.value(&mut cx) as i32);
Ok(cx.number(res))
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("fibonacci_rs", fibonacci_api)?;
Ok(())
}
We’ve added a small function named main
and marked it with neon::main
attribute to tell Neon that this is the main entry of the module. This function also accepts a context, but not FunctionContext
like our previous function, instead it accepts ModuleContext
. While function context is used to interface with js functions and have operation like getting arguments of a function, up/down casting JsValue
s into specific types and generating results; Module context is used to interface with Javascript modules, and the main methods in ModuleContext
are ModuleContext::export_function()
and ModuleContext::export_value()
, which are used to export functions and values from a module.
In our case we export a function and name it fibonacci_rs
, while the actual implementation of it is fibonacci_api
we’ve written earlier.
In Javascript world, this code would look something like this:
function fibonacci(n) {
...
}
module.exports = {
fibonacci_rs: fibonacci
}
Running cargo build
once again will produce our library without any warnings. We are now done with Rust, let’s move to Javascript.
Step 5 - Fibonacci Javascript
Let us now create a new Javascript file: fibonacci.js
:
const value = process.argv[2] || null;
const number = parseInt(value);
if(isNaN(number)) {
console.log("Provided value is not a number");
return;
}
console.log(number);
This is a simple file that reads the first argument we pass to it from CLI, validates that it is a number and prints it back. You can execute it by node fibonacci.js 10
.
Now we somehow need to call the fibonacci function we’ve created in Rust. For that we will have to create a package.json
.
{
"name": "fibonacci_rs",
"version": "1.0.0",
"private": true,
"main": "fibonacci.js",
"scripts": {
"build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics"
},
"dependencies": {
"cargo-cp-artifact": "^0.1.5"
},
"devDependencies": {}
}
Run npm install
to install the dependency and then run npm run build
to generate the native module. If everything worked correctly, you should end up with index.node
file in your root directory.
Let’s explain. cargo-cp-artifact
is a package maintained by Neon team. What this package does, is handling the extraction of the library produced by cargo build
. Remember, cargo build
generates different files depending on the host OS. In my case it generated libfibonacci_rs.dylib
since I’m on Mac, but it could also generate .so
or .dll
files if you are on Linux or Windows. Theoretically, you can go manually to the target
directory and copy the library file to the root of the project and name it index.node
, but cargo-cp-artifact
is a handy utility to do that for you.
Also note: index.node
is not a special name. You can call it what.ever
, but the .node
extension is used to represent a native Node.js module.
Now lets go back to our fibonacci.js
:
const {fibonacci_rs} = require("./index.node");
const value = process.argv[2] || null;
const number = parseInt(value);
if(isNaN(number)) {
console.log("Provided value is not a number");
return;
}
const result = fibonacci_rs(number);
console.log(result);
You can run it with node fibonacci.js 10
and you should get the result of 55
. Note: do not input high numbers. Since this is a very inefficient and recursive implementation of Fibonacci, it will hang on numbers above 40ish.
Let’s explain. We require our index.node
as if it was a regular Javascript module. We know that it exports only 1 function: fibonnaci_rs
since that’s what we did in cx.export_function("fibonacci_rs", fibonacci_api)?;
in src/lib.rs
, hence we require it and then execute it.
That’s it! We’ve created a native module in Rust.
You can clone the entire code of this tutorial from my GitHub. You can also explore the examples repository by Neon for more robust examples like gzip-stream
or async sqlite
. Feel free to explore the neon repo as well, they also have a link to their Slack channel for questions and support.
What you can do with it?
Many things. You can access native implementations that are not available in Node. You can use any Rust crate
(equivalent to npm
package) from crates.io (provided you implement the required neon bindings), or you can have another strong tool to evaluate when your Javascript implementation is not fast enough for your needs. I’m myself currently experimenting in writing a Desktop application using Electron for the Frontend and Rust for the Backend. This allows me to isolate my backend code into a reusable library that I can plug into any more performant UI Framework such as Qt, GTK and SwiftUI, as well as letting me access SQLite using native sqlite implementation written in C rather than Javascript.
Things to take into account
If you are not coming from the world of compiled languages, or you knowledge of it became rusty (no pun intended), you might need to pay attention to some caveats with using native modules written in any language and not just Rust.
With Javascript, Python or Ruby, I can give you a code and you can run it (provided you have the runtime installed). Dynamic languages are easily shared between people and machines. Compiled languages are not. If I give you my binary index.node
from this tutorial, you might or might not be able to use it. It depends on the host OS you are running. The safe bet is to compile the code yourself on the OS that you intend to run the code. This means that native modules should not be committed into your source control, but instead be built during your CI/CD. And they must be built on the same OS that they are going to be executed on. So if your Node application is running in Alpine Linux Docker container, it is very much advised that you compile your native modules on the same docker image.
That’s it for today. I hope this newly acquired knowledge will help you to become a better Node developer.