Table of Contents
- Available Methods
- Native Addons
- Setting Of Project
- A deep dive in Napi
What is Rust?
Rust is a systems programming language by Mozilla. It can call the C library by default and includes first-class support for exporting functions to C.
Rust provides you with low-level control and high-level ergonomics. It gives you control of memory management without the hassle associated with these controls. It also delivers zero-cost abstraction, so you pay for only what you use.
Rust can be called in a Node.js context via various methods. I’ve listed some of the most widely used below.
- You can use FFI from Node.js and Rust, but this is very slow
- You can use WebAssembly to create a
node_module, but all Node.js functionality is not available
- You can use native addons
What is a native addon?
Node.js addons are shared objects written in C++ that are dynamically linked. You can load them into Node.js using the
A native addon provides a simple interface to work with another binary by loading it in V8 runtime. It is very fast and safe for making calls across the languages. Currently, Node.js supports two types of addon methods: C++ addons and N-API C++/C addons.
A C++ addon is an object that can be mounted by Node.js and used in the runtime. Since C++ is a compiled language, these addons are very fast. C++ has a wide array of production-ready libraries that can be used to expand the Node.js ecosystem. Many popular libraries use native addons to improve performance and code quality.
N-API C++/C Addons
Where does Rust come in?
Rust lays down the structs in memory differently, so we need to tell it to use the style C uses. It would a pain to create these functions by hand, so we’ll use a crate called
nodejs-sys that uses
bindgen to create a nice definition for N-API.
b``indgen automatically generates Rust FFI bindings to C and C++ libraries.
Note: There will a lot of unsafe code ahead, mostly external function calls.
Setting up your project
Create a directory named
rust-addon and initialize a new npm project by running
npm init. Next, init a cargo project called
cargo init --lib. Your project directory should look like this:
Configuring Rust to compile to the addon
We need Rust to compile to a dynamic C library or object. Configure cargo to compile to the
.so file on Linux,
.dylib on OS X, and
.dll on Windows. Rust can produce many different types of libraries using Rustc flags or Cargo.
lib key provides options to configure Rustc. The
name key gives the library name to the shared object in the form of
type provides the type of library it should be compiled to — e.g.,
cdylib creates a dynamically linked C library. This shared object behaves like a C library.
Starting with N-API
Let’s create our N-API library. We need to add a dependency.
nodejs-sys provides us with the binding required for
napi_register_module_v1 is the entry point for the addon. N-API documentation recommends
N-API``_MODULE_INIT macro for module registration which compiles to
Node.js calls this function and provides it with an opaque pointer called
Rust represents owned strings with the
S``tring type and borrowed slices of strings with the
str primitive. Both are always in UTF-8 encoding and may contain null bytes in the middle. If you look at the bytes that make up the string, there may be a
\0 among them. Both
str store their length explicitly; there are no null terminators at the end of strings like C strings.
Rust strings are very different from the ones in C, so we need to change our Rust strings to C strings before we can use then with N-API functions. Since exports is an object represented by
napi_env, which acts an anchor between Rust and Node.js.
napi_create_string_utf8 here to create a string. We passed in the environment a pointer to the string, the length of string, and a pointer to an empty memory location where it can write the pointer to the newly created value. All this code is unsafe because it includes many calls to external functions where the compiler cannot provide Rust guarantees. In the end, we returned the module that was provided to us by setting a property on it with the value
It’s important to understand that
nodejs-sys just provides the required definitions for the function you’re using, not their implementation. N-API implementation is included with Node.js and you call it from your Rust code.
Using the addon in Node.js
The next step is to add a linking configuration for different operating systems, then you can compile it.
build.rs file to add a few configuration flags for linking the N-API files on different operating systems.
Your directory should look like this:
Now you need to compile your Rust addon. You can do so pretty easily using the simple command
cargo build --release. This will take some time on the first run.
After your module is compiled, create a copy of this binary from
./target/release/libnative.so to your root directory and rename it as
index.node. The binary created by the cargo may have a different extension or name, depending on your crate setting and operating system.
Now you can require the file in Node.js and use it. You can also use it in a script. For example:
Next, we’ll move on to creating functions, arrays, and promises and using
libuv thread-pool to perform heavy tasks without blocking the main thread.
A deep dive into N-API
Now you know how to implement common patterns using N-API and Rust. A very common pattern is the export function, which can be called by the user of the library or Node module. Let’s start by creating a function.
You should use
napi_create_function to create your functions so that you can use them from Node.js. You can add these functions as a property to exports to use from Node.js.
Creating a function
napi_value pointer. A N-API function is pretty easy to create and use.
In the above example, we created a function in Rust named
napi_create_function, which takes the following arguments:
napi_envvalue of the environment
- The length of the function name string
- Context data that can be passed by the user later and accessed from the Rust function
- When you create this function, add it as a property to your
The function on the Rust side must have the same signature as shown in the example. We’ll discuss next how to access arguments inside a function using
napi_callback_info. We can access this from a function and other arguments as well.
Function arguments are very important. N-API provides a method to access these arguments.
napi_get_cb_info to get the arguments. The following arguments must be provided:
- The info pointer
- The number of expected arguments
- A buffer where arguments can be written as
- A memory location where this value pointer can be written
We need to create an array with memory locations where C can write a pointer to arguments and we can pass this pointer buffer to N-API function. We also get
this, but we aren’t using it in this example.
Working with strings arguments
napi_get_value_string_utf8 and call this function twice: the first time to get length and second time to get the value of the string.
You’ll need to pass a few arguments to
napi_create_string_utf8 to create a string. If a null pointer is passed as buffer, the length of the string is given. The following arguments are required:
napi_valuepointer to the string in
- The buffer where the string is to be written if null gives the length of the string
- The length of the buffer
- Bytes written to the buffer
Working with promises and libuv thread pool
It’s not a good idea to block the main thread of Node.js for doing calculations. You can use libuv threads to do the heavy lifting.
napi_create_async_work method for the libuv thread.
Creating a promise
To create a promise, simply use
napi_create_promise. This will provide a pointer,
napi_deferred, which can then resolve or reject a promise using the following functions:
You can create and throw an error from the Rust code using
napi_throw_error. Every N-API function returns a
napi_status, which should be checked.
The following example shows how to schedule async work.
We created a struct to store a pointer to our
napi_deferred as well as our output. Initially, the output is
None. Then we created a promise, which provides a
deferred that we save in our data. This data is available to us in all of our functions. Next, we converted our data into raw data and pass it to the
napi_create_async_work function with other callbacks. We returned the promise we created, executed
perform, and converted our data back to struct. Once
perform is completed on libuv thread,
complete is called from the main thread, along with the status of the previous operation and our data. Now we can reject or resolve our work and delete work from the queue.
Lets walk through the code
Create a function called
this in a function), etc.
We also examined an in-depth example of how to use
libuv threads and create an
There are many libraries available if you don’t want to write all the code by hand. These provide nice abstractions, but the downside is that they don't support all features.