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 whatpreludedoes, check out this link). - defines a function that takes a
FunctionContextas an argument and returns aJsResult. (Yes, it returns aResult. Why? because we are writing RUST). - The next thing is the
mainfunction with theattributeneon::main. It takes amut ModuleContextas 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::mainattribute, something thatJavaScriptcontext would look for. At this point, all it does is that it exports theRustfunctions tojs module, in other words, it tells the javascript runtim “what functions are present in this module?”. In this case it’s exportinghellofunction written inRusttojsusing 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.