DEV Community

Cover image for Writing Rust bindings for Node.js with Neon
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Writing Rust bindings for Node.js with Neon

Written by Oduah Chigozie✏️

While Node.js is a powerful JavaScript runtime environment, it has its limitations — especially in terms of performance and efficiency. Tools like Neon can enhance those aspects of Node.js applications, leveraging the power of Rust to provide a more streamlined experience.

Similar to WebAssembly (Wasm), Neon allows you to use Rust in your project. However, while Wasm converts your Rust code to Wasm, Neon compiles your Rust code to native code. This means the native code Neon produces runs faster than WebAssembly.

Unlike WebAssembly, Neon allows you to use almost all — if not all — of Rust's features. This enables you to bring Rust's benefits into your Node.js project, such as its speed, efficiency, ecosystem, and more.

In this article, we’ll look into what Neon is and how you can use it in your application. Jump ahead:

To follow along with the practical examples demonstrated in this article, you’ll need Node.js and Rust installed.

You can check out the code samples below in this GitHub repository.

What is Neon?

Neon allows developers to create native bindings for Node.js applications with Rust. Native bindings are libraries compiled to execute directly on your system’s hardware. You can also call them native libraries.

Native bindings operate when a piece of code interacts with them. They are similar to Node modules, but compile to native code.

You can use native libraries to speed up some necessary project processes. Any code interacting with a native library will experience the performance benefits of that library running directly on the system.

How to create a Neon project

Creating a Neon project is simple, but as previously mentioned, you need to have Node.js and Rust installed on your system. Neon depends heavily on these two programming languages.

If you have everything ready, open your terminal and type in this command:

npm init neon my-project
Enter fullscreen mode Exit fullscreen mode

Your terminal should be in the folder where you want your project because this command creates a new folder with your project name — the last string of text without whitespace at the end of the command — as its name. In this case, my-project would be the project name.

The project folder will have this structure:

.
├── Cargo.toml
├── README.md
├── package.json
└── src
    └── lib.rs
Enter fullscreen mode Exit fullscreen mode

If you’re interested in optimizing a Node project’s structure, check out our guide to best practices for Node.js project architecture. Otherwise, let’s take a closer look at the unique structure of our Neon project.

Neon project structure

If you look at the project structure, you’ll notice that the project seems like a combination of Node.js and Rust library projects. This is how all Neon projects are structured.

You’ll write all the Rust bindings in the Rust src/lib.rs library file, while your JavaScript files can live anywhere in the project folder.

Let’s look at the Rust library file in more detail.

The Rust library file: lib.rs

The Rust library file contains the following code:

use neon::prelude::*;

fn hello(mut cx: FunctionContext) -> JsResult<JsString> {
   Ok(cx.string("hello node"))
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
   cx.export_function("hello", hello)?;
   Ok(())
}
Enter fullscreen mode Exit fullscreen mode

This code has three components:

  • Line one imports all the data types and structures used by the rest of the code
  • Lines three through five define a function called hello that Neon can export into the JavaScript environment
  • Lines seven through eleven define the primary or main function allowing Neon to export all your functions into the JavaScript environment

Now let’s go into more detail about the hello and main functions.

Let’s start with the hello function’s argument, cx. cx is a reference to the JavaScript context through a function. With this argument, you can make function-like interactions and create and initialize values accessible from the JavaScript environment.

In this function, cx creates a string value of "hello node" with the type JsString in the Rust environment.

Now, look at the hello function’s return type — JsResult<JsString>. This indicates Neon expects the function to have a JsResult type when you’re exporting it, so you must wrap the value you want to return in a JsResult type.

The hello function has its return value wrapped in Ok, a variant of Rust’s built-in Result type. JsResult implements the Result type so that the function can accept Ok just fine.

Next, let’s look at the main function. This is the function that Neon needs to work with to know which functions to export to JavaScript.

The main function can have another name, but you need to mark that function with the #[neon::main] attribute so Neon knows what role the function should play.

Like in the hello function, cx is a reference to the JavaScript context. But in the main function, cx allows modular-based interactions and exports hello to the JavaScript environment. You can also create values and export those values using this cx.

Finally, let’s look at the main function’s return type: NeonResult<()>. As our primary Neon function, the main function must return a NeonResult type. NeonResult is an extended version of Rust’s built-in Result type, which fits better into the function’s role.

Interacting with the Rust library in JavaScript

Now, let’s interact with the Rust library using JavaScript. Before you do that, you first need to install the JavaScript dependencies with npm i and compile the Rust library with npm run build.

Once you’ve done so, you can test it with Node’s interactive shell:

% node  
Welcome to Node.js v18.16.0.
Type ".help" for more information.
> const mod = require(".")
undefined
> mod.hello()
'hello node'
>
Enter fullscreen mode Exit fullscreen mode

You can also try it in a normal JavaScript file. Create an index.js file, copy the code below into it, and run node index.js:

const mod = require(".");

console.log(mod.hello());
Enter fullscreen mode Exit fullscreen mode

Examples of what you can do with Neon

In this section, I’ll give a few examples demonstrating what you can do with Neon. I’ll give you the Rust function, the JavaScript interaction, and the outputs.

Creating objects

Having the ability to create objects enables you to build much more complex data structures with your Rust function. This, in turn, increases the scope of what you can do with Rust in your Node.js project.

This example shows how you can create an empty JavaScript object and store string and number data in its fields. Here’s the function in Rust:

fn get_user(mut cx: FunctionContext) -> JsResult<JsObject> {
   / Create an empty object
    let obj = cx.empty_object();

    // Create values to store in the object
    let name = cx.string("Chigozie");
    let age = cx.number(19);

    // Store these values in the object
    obj.set(&mut cx, "name", name)?;
    obj.set(&mut cx, "age", age)?;

   Ok(obj)
}

// `main` function
cx.export_function("getUser", get_user)?;
Enter fullscreen mode Exit fullscreen mode

To interact with this Rust function from our Node.js environment, we can use the following JavaScript code:

const mod = require(".");
console.log(mod.getUser());
Enter fullscreen mode Exit fullscreen mode

Then, when executed, the output should display the following:

% node index.js
{ name: 'Chigozie', age: 19 }
Enter fullscreen mode Exit fullscreen mode

Reading and writing files

Reading and writing files is one of Rust's standout features, thanks to its emphasis on safety, performance, and expressiveness.

JavaScript is great for web and lightweight applications. However, if you're building a data-intensive application that requires high performance, Rust’s speed and efficiency can make it more suitable than JavaScript.

The following is an example of file I/O in Rust:

use std::fs::File;
use std::io::prelude::*;

fn write_file(mut cx: FunctionContext) -> JsResult<JsBoolean> {
   if let Ok(mut file) = File::create("foo.txt") {
       if let Ok(()) = file.write_all(b"Hello, world!") {
           Ok(cx.boolean(true))
       } else {
           Ok(cx.boolean(false))
       }
   } else {
       Ok(cx.boolean(false))
   }
}

fn read_file(mut cx: FunctionContext) -> JsResult<JsString> {
   match File::open("foo.txt") {
       Ok(mut file) => {
           let mut contents = String::new();
           if let Ok(_) = file.read_to_string(&mut contents) {
               Ok(cx.string(contents))
           } else {
               Ok(cx.string(""))
           }
       }
       Err(_) => Ok(cx.string("")),
   }
}

// `main` function
cx.export_function("readFile", read_file)?;
cx.export_function("writeFile", write_file)?;
Enter fullscreen mode Exit fullscreen mode

Here’s the JavaScript interaction you’d use to bridge the gap between Rust and Node.js to use the above functions in your application:

const mod = require(".");

if (mod.writeFile()) {
 console.log(mod.readFile());
} else {
 console.log("couldn't write file");
}
Enter fullscreen mode Exit fullscreen mode

After running the script, you should see this output:

% node index.js
Hello, world!
Enter fullscreen mode Exit fullscreen mode

Accessing function arguments

Sometimes, you need to pass arguments to your Rust function, and you need your function to handle arguments that it receives from the JavaScript environment.

This example shows how you can make your Rust function handle arguments from JavaScript. Let’s start with the Rust code:

fn add(mut cx: FunctionContext) -> JsResult<JsNumber> {
   let num1 = cx
       .argument::<JsNumber>(0)? // Access the first argument
       .value(&mut cx);
   let num2 = cx
       .argument::<JsNumber>(1)? // Access the second argument
       .value(&mut cx);

   Ok(cx.number(num1 + num2))
}

// `main` function
cx.export_function("add", add)?;
Enter fullscreen mode Exit fullscreen mode

Here’s how you’d invoke the Rust function using JavaScript:

console.log(mod.add(1, 3));
Enter fullscreen mode Exit fullscreen mode

Finally, you should get this output:

% node index.js
4
Enter fullscreen mode Exit fullscreen mode

Conclusion

Neon is a powerful tool that dramatically improves Node.js projects with the power and safety of Rust. Not only that, but it also allows you to use all of Rust’s features. I hope this guide has helped you a lot with understanding it. You can check out the GitHub repo for this tutorial to see the code examples we used. If you have any questions about using Neon as a bridge between Node.js and Rust, feel free to comment them below. Thanks for reading!


200’s only Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket.

LogRocket Signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.

Top comments (0)