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.