Creating native extensions in Rust for Node.js

There are times when the performance of JavaScript in the Node.js runtime is not enough, so developers reach out to other low-level languages to create native Node.js modules. Often, C and C++ are used as the implementation language for these native modules. Rust can also be used to create native Node.js modules via the the same FFI abstractions that we saw for C and Python. In this section, we'll explore a high-level wrapper for these FFI abstractions, called the neon project, which was created by Dave Herman from Mozilla.

The neon project is a set of tools and glue code that makes the life of Node.js developers easier, allowing them to write native Node.js modules in Rust and consume them seamlessly in their JavaScript code. The project resides at https://github.com/neon-bindings/neon. It's partially written in JavaScript: there's a command-line tool called neon in the neon-cli package, a JavaScript-side support library, and a Rust-side support library. Node.js itself has good support for loading native modules, and neon uses that same support.

In the following demo, we will be building a native Node.js module in Rust as an npm package, exposing a function that can count occurrences of a given word in a chunk of text. We will then import this package and test the exposed function in a main.js file. This demo requires Node.js (version v11.0.0) to be installed, along with its package manager, npm (version 6.4.1). If you don't have Node.js and npm installed, head over to https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-16-04 to set them up. After you are done installing them, you need to install the neon-cli tool using npm by running the following command:

npm install --global neon-cli

Since we want this tool to be available globally to create new projects from anywhere, we pass the --global flag. The neon-cli tool is used to create a Node.js project with skeleton neon support included. Once it is installed, we create our project by running neon new native_counter, which prompts for basic information for the project, as shown in the following screenshot:

Here's the directory structure this command created for us:

  native_counter tree
.
├── lib
│ └── index.js
├── native
│ ├── build.rs
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── package.json
└── README.md

The project structure neon created for us is the same npm package structure that we get with the usual lib directory and package.json. In addition to the Node.js package structure, it has also created a cargo project for us under the native directory with some initial code in it. Let's see what the contents of this directory are, starting with Cargo.toml:

# native_counter/native/Cargo.toml

[package]
name = "native_counter"
version = "0.1.0"
authors = ["Rahul Sharma <[email protected]>"]
license = "MIT"
build = "build.rs"
exclude = ["artifacts.json", "index.node"]

[lib]
name = "native_counter"
crate-type = ["dylib"]

[build-dependencies]
neon-build = "0.2.0"

[dependencies]
neon = "0.2.0"

The prominent thing to note is the [lib] section, which specifies the crate type as dylib, which means we require Rust to create a shared library. There is also an autogenerated build.rs file at the root level, which does some initial build environment configuration by calling neon_build::setup() inside it. Next, we'll remove the existing code in our lib.rs file and add the following code:

// native_counter/native/src/lib.rs

#[macro_use]
extern crate neon;

use neon::prelude::*;

fn count_words(mut cx: FunctionContext) -> JsResult<JsNumber> {
let text = cx.argument::<JsString>(0)?.value();
let word = cx.argument::<JsString>(1)?.value();
Ok(cx.number(text.split(" ").filter(|s| s == &word).count() as f64))
}

register_module!(mut m, {
m.export_function("count_words", count_words)?;
Ok(())
});

First, we import the neon crate, along with the macros and all the items from the prelude module. Following that, we define a function, count_words, which takes in a FunctionContext instance. This contains information in JavaScript regarding the active function that's invoked, such as the argument list, length of arguments, the this binding, and other details. We expect the caller to pass two arguments to our count_words function. Firstly, the text, and secondly, the word to search for in the text. These values are extracted by calling the argument method on the cx instance and passing in the respective index to it. We also use the turbofish operator to ask it to give a value of the JsString type. On the returned JsString instance, we call the value method to get a Rust String value.

After we're done extracting the arguments, we split our text with white space and filter only the chunks that contain the given word before calling count() on the iterator chain to count the number of matched occurrences:

text.split(" ").filter(|s| s == &word).count()

count() returns usize. However, we need to cast usize to f64 because of the Into<f64>  trait bound on our number method on cx. Once we do that, we wrap this expression with a call to cx.number(), which creates a JavaScript-compatible JsNumber type. Our count_words method returns a JsResult<JsNumber> type, as accessing the arguments might fail and returning a proper JavaScript type might also fail. This error variant in the JsResult type represents any exception that's thrown from the JavaScript-side.

Next, we register our count_words function with the register_module! macro. This macro gets a mutable reference to a ModuleContext instance, m. Using this instance, we export our function by calling the export_function method, passing in the name of the function as string and the actual function type as the second parameter.

Now, here's our updated index.js file's contents:

// native_counter/lib/index.js

var word_counter = require('../native');
module.exports = word_counter.count_words;

As index.js is the root of an npm package, we require our native module and must export the function directly at the root of the module using module.exports. We can now build our module using the following code:

neon build

Once the package has been built, we can test it by creating a simple main.js file in the native_counter directory with the following code:

// native_counter/main.js

var count_words_func = require('.');
var wc = count_words_func("A test text to test native module", "test");
console.log(wc);

We'll run this file by running the following code:

node main.js

This gives us an output of 2. That concludes our awesome journey on making Rust and other languages talk to each other. It turns out that Rust is quite smooth at this interaction. There are rough edges in cases where other languages don't understand Rust's complex data types, but this is to be expected, as every language is different in its implementation.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset