Supercharge Your NodeJS With Rust

·

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 Bindings#774x300

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:

javascript type hierarchy#960x480

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 JsValues 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 .nodeextension 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.

Share this:

Published by

Dmitry Kudryavtsev

Dmitry Kudryavtsev

Senior Software Engineer / Tech Lead / Consultant

With more than 14 years of professional experience in tech, Dmitry is a generalist software engineer with a strong passion to writing code and writing about code.


Technical Writing for Software Engineers - Book Cover

Recently, I released a new book called Technical Writing for Software Engineers - A Handbook. It’s a short handbook about how to improve your technical writing.

The book contains my experience and mistakes I made, together with examples of different technical documents you will have to write during your career. If you believe it might help you, consider purchasing it to support my work and this blog.

Get it on Gumroad or Leanpub


From Applicant to Employee - Book Cover

Were you affected by the recent lay-offs in tech? Are you looking for a new workplace? Do you want to get into tech?

Consider getting my and my wife’s recent book From Applicant to Employee - Your blueprint for landing a job in tech. It contains our combined knowledge on the interviewing process in small, and big tech companies. Together with tips and tricks on how to prepare for your interview, befriend your recruiter, and find a good match between you and potential employer.

Get it on Gumroad or LeanPub