In a previous post Writing python extension with rust
I wrote about calling Rust
from Python
.
This time, I wanted to do the same from JavaScript
. I did it using a great module
Neon. It does the exact same thing that maturin
does for Python
.
Here’s how you can achieve this. Setup
You are all set to start a Neon project.
- Initialize a project with Not
cargo
π butnpm
.npm init neon <your projectname>
for example
npm init neon crabby_binding
And you will get output, something like
girish in ~/git/rust/neon-projs π 00:00:00 β npm init neon crabby_bindings Need to install the following packages: create-neon@0.2.0 Ok to proceed? (y) y This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sensible defaults. See `npm help init` for definitive documentation on these fields and exactly what they do. Use `npm install <pkg>` afterwards to install a package and save it as a dependency in the package.json file. Press ^C at any time to quit. package name: (crabby_bindings) version: (0.1.0) description: test_crabby_bindings git repository: keywords: author: girish license: (ISC) About to write to /home/girish/git/rust/neon-projs/crabby_bindings-LlRCyJ/package.json: { "name": "crabby_bindings", "version": "0.1.0", "main": "index.node", "scripts": { "build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", "build-debug": "npm run build --", "build-release": "npm run build -- --release", "install": "npm run build-release", "test": "cargo test" }, "devDependencies": { "cargo-cp-artifact": "^0.1" }, "description": "test_crabby_bindings", "author": "girish", "license": "ISC" } Is this OK? (yes) β¨ Created Neon project `crabby_bindings`. Happy π¦ hacking! β¨
This will bootstrap a project and you are all set to write rust.
The project structure for this looks like
girish in ~/git/rust/neon-projs π 00:00:01 β― cd crabby_bindings/ girish in git/rust/neon-projs/crabby_bindings via v18.16.1 π¦ v1.66.0 π 00:00:01 β― tree . . βββ Cargo.toml βββ package.json βββ README.md βββ src βββ lib.rs 2 directories, 4 files
The default contents in the src/lib.rs
look like
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(())
}
Also, the Default contents of Cargo.toml
looks like
[package]
name = "crabby_bindings"
version = "0.1.0"
description = "test_crabby_bindings"
authors = ["girish"]
license = "ISC"
edition = "2018"
exclude = ["index.node"]
[lib]
crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[dependencies.neon]
version = "0.10"
default-features = false
features = ["napi-6"]
Just based on these two files, we can say that, it
- ‘preludes’
neon
. (If you don’t know whatprelude
does, check out this link). - defines a function that takes a
FunctionContext
as an argument and returns aJsResult
. (Yes, it returns aResult
. Why? because we are writing RUST). - The next thing is the
main
function with theattribute
neon::main
. It takes amut ModuleContext
as an argument and returns aNeonResult
. (it’s not aJsResult
).
So what exactly it does:
hello
: is a regular rust function that returns aString
.main
: This is function withneon::main
attribute, something thatJavaScript
context would look for. At this point, all it does is that it exports theRust
functions tojs module
, in other words, it tells the javascript runtim “what functions are present in this module?”. In this case it’s exportinghello
function written inRust
tojs
using apiexport_function
.
After exploring the Rust side, let’s examine the package.jsonβour main JavaScript configuration.
package.json
{ "name": "crabby_bindings", "version": "0.1.0", "description": "test_crabby_bindings", "main": "index.node", "scripts": { "build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", "build-debug": "npm run build --", "build-release": "npm run build -- --release", "install": "npm run build-release", "test": "cargo test" }, "author": "girish", "license": "ISC", "devDependencies": { "cargo-cp-artifact": "^0.1" } }
It defines the build scripts for this project.
So let’s try running
npm install
as it will generate a release
build for this module.
Once this is done, we get index.node
in the same directory
girish in git/rust/neon-projs/crabby_bindings via v18.16.1 π¦ v1.66.0 π 00:00:02 β ls Cargo.lock Cargo.toml index.node node_modules package.json package-lock.json README.md src target
We can change the name of this output file in the package.json
.
This module can be directly used in a js
file.
for example,
// import crabby, our module written in rust
const crabby = require('.');
// print the contents of crabby.
console.log("crabby module: ", crabby);
// call a function from crabby.
var crabby_hello_output = crabby.hello();
console.log("output of the crabby.hello(): ", crabby_hello_output);
And the output of this looks like:
girish in git/rust/neon-projs/crabby_bindings via v18.16.1 π¦ v1.66.0 π 00:00:03 β ls Cargo.lock Cargo.toml index.node node_modules package.json package-lock.json README.md src target test.js girish in git/rust/neon-projs/crabby_bindings via v18.16.1 π¦ v1.66.0 π 00:00:03 β― node test.js crabby module: { hello: [Function: crabby_bindings::hello] } output of the crabby.hello(): hello node
While our example/boilerplate code demonstrates a synchronous Rust function, integrating asynchronous Rust functions, especially those relying on frameworks like tokio, can be more complex. Stay tuned for a follow-up post where we’ll dive deeper into asynchronous Rust-JavaScript interoperability.