Rust has consistently been ranked as one of Stack Overflow's most loved programming language for many years now. Yet, usage of Rust (at only ~7% of respondents indicating that they use Rust) seemingly trails far behind the love for the language. Compare this to JavaScript, an extremely popular and widely used language (at ~65% usage), that definitely doesn't get the same love with about 39% of respondents saying they dread the language.

The question then becomes, can you combine these two languages into a project and get the best of both worlds? The answer is yes, you can. Let's dive in.

For this example we wil be using Neon to get our Rust code running in Node.js. There seem to be other alternatives out there, but Neon seems to have the most support, and works extremely well with the new Apple Silicon Macs (while I have had issues Apple Silicon support on some of the alternatives).

The first step is to ensure you have Rust & Node.js installed on your machine. After that is complete, you can run the following command in your Terminal to create the project.

npm init neon project -y

This will create a new directory called project in your current working directory. It might also ask if you want to install the packages necessary to create a Neon project. The nice thing about this is that everything is fully bootstrapped, and you don't need to put any effort into setting up the project yourself. The good news is that the Neon project starter is extremely minimal, which makes it easy to read through and understand what it has done. Side note: I personally dislike project starters that are extremely opinionated and create a bunch of unnecessary files/code. Project starters that do this often create more confusion than it's worth.

Let's look at the directory structure of the project.

├── src
│   ├── lib.rs
├── package.json
├── Cargo.toml
├── README.md
├── .gitignore
├── Cargo.lock
├── target
│   ├── ...

I haven't listed all of the contents of the target directory since you likely won't be working with it at all.

If we run npm install it will install all of the npm dependencies, and also run npm run build-release automatically (since that is listed as the install script in the package.json file).

Running npm run build-release or npm run build-debug will compile the Rust code into a index.node file at the root of the directory.

At this point you should have a working Neon project. If we run node we should be able to interact with our Rust code.

node
Welcome to Node.js v16.13.0.
Type ".help" for more information.
> require(".").hello();
'hello node'
>

Awesome! We have Rust code running in Node.js. Now let's look at the Rust code and see what it does and how we can modify it for our purposes.

If we open the src/lib.rs file we should see the following:

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(())
}

So what is this code doing. Let's take it line by line.

use neon::prelude::*;

Here we are basically importing what is necessary to have Neon work.

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

Then we are defining a function called hello. This function takes a mutable FunctionContext called cx and returns a JsResult which is a type that represents a JavaScript value. In this case, we are returning a JsString which is a JavaScript string.

Within the function body we are calling Ok, and passing in a cx.string and passing in "hello node". Think about this as a JavaScript return statement. In JavaScript the code would look something like this:

function hello() {
	return "hello node";
}

Pretty simple. Not quite as simple as the Node.js version with the cx parameter and the Ok function being called, but simple regardless.

So what is next? All that is left is setting up our main Neon function.

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

The #[neon::main] part is telling Neon this is our main function. Think about this as your entry point. This is what Neon will use to export to be used in your JavaScript code. Then create a function called main, that takes in a mutable ModuleContext (different from the FunctionContext this time) called cx and returns a NeonResult.

In the function body it calls cx.export_function and passes in "hello" and hello. The first argument is the name of the function that will be exported to JavaScript. The second argument is the function itself. Finally we call Ok(()).

So let's say we wanted to add a new function to our Rust code. How could we do this? Well, to get the function working and exported to Node.js we need the function itself, and the cx.export_function("hello", hello)?; line. Duplicating these two parts and changing the necessary pieces will allow us to add a new function to our Rust code that can be used by Node.js.

Let's increase the complexity a bit and accept a parameter from Node.js into our Rust function.

fn add(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let a: Handle<JsNumber> = cx.argument::<JsNumber>(0)?;
    let b: Handle<JsNumber> = cx.argument::<JsNumber>(1)?;

    let total: f64 = a.value(&mut cx) + b.value(&mut cx);

    Ok(cx.number(total))
}

Basically this code is getting two arguments from Node.js as numbers and adding them together and returning the result. Building that again and running it in the Terminal and trying again will result in the following:

node
Welcome to Node.js v16.13.0.
Type ".help" for more information.
> require(".").add(1, 2);
3
> require(".").add("Hello", "World");
Uncaught TypeError: failed to downcast any to number
> require(".").add();
Uncaught TypeError: not enough arguments

As you can see it adds just fine. But if you don't use the correct types it will through an Uncaught TypeError.

You can fix this by using let x: Handle<JsValue> = cx.argument(0)?; instead of let a: Handle<JsNumber> = cx.argument::<JsNumber>(0)?;. This will allow you to use any type of JavaScript value.

You can also use optional arguments by using cx.argument_opt(0)?; instead of cx.argument(0)?;.

There is a lot more you can do with Neon. Luckily the documentation is pretty solid and easy to get started with and understand. I'd encourage everyone to check it out to learn more details about how to use Neon.

Rust also has an ever growing ecosystem of third-party Rust libraries. These tend to work well with Neon, so long as you provide the bindings and interactions with Neon for them.


One important note. If you ever get an error like zsh: killed node when trying to require your index.node file, you can fix this by running the following commands:

cargo clean
rm index.node

Then rebuilding the project should fix the issue.