Preface
Goal: Create a native NodeJS addon using Rust.
There are so many rust potential of rust
,
varying degree from concurrency, to WASM.
One that ctach my attenti0on is the ability,
to create a native NodeJS addon easily.
This is a bonus article. This will be short.
The Alternative
So far there are two library to do this,
The first is neon binding
,
and the second is the napi.rs
as a new contender:
08: Neon Serde
Passing JSON
Consider go straight with working example.
This is actually just neon binding
with macros.
Rust Native
From the json
example, in neon binding
official documentation.
and unique code from previous example, we can easily develop this:
use neon::register_module;
use neon_serde::export;
use serde_derive::{Deserialize, Serialize};
mod my_utils;
#[derive(Serialize, Deserialize, Debug)]
struct Song {
title: String,
tags: Option<Vec<String>>
}
export! {
fn passRecords(songs: Vec<Song>) -> String {
let tags: Vec<String> = songs
.into_iter()
.filter_map(|song| song.tags)
.flat_map(|tag_s| tag_s)
.collect();
format!("{:?}", my_utils::unique(tags))
}
}
We need to compile first.
$ neon build --release
neon info running cargo
Compiling cc v1.0.41
Compiling memchr v2.2.1
…
Compiling neon-serde v0.2.0
Finished release [optimized] target(s) in 2m 49s
neon info generating native/index.node
A new file native/index.node
should be created by now.
Custom Utility Module
It is using the code from previous article.
NodeJS Script
Maybe Null 🤔?
I would like to point how Option
handled nicely.
We need an example data in javascript file,
with a record containing null
value.
const {
passRecords
} = require('../native/index.node');
const songs = [
{ title: "Cantaloupe Island",
tags: ["60s", "jazz"] },
{ title: "Let It Be",
tags: ["60s", "rock"] },
{ title: "Knockin' on Heaven's Door",
tags: ["70s", "rock"] },
{ title: "Emotion",
tags: ["70s", "pop"] },
{ title: "The River" }
];
console.log(
"Unique Tags: ",
passRecords(songs)
);
Consider test the native script:
$ node lib/index.js
Unique Tags: ["60s", "jazz", "rock", "70s", "pop"]
This wouldn’t be so hard.
09: Neon Binding
We can go low level using neon binding
without macro.
Rust Native Skeleton
There are four examples here.
- Hello World (only string result)
- Passing String (with string result)
- Passing Array (with array result)
- Passing Object (with array result)
The skeleton of the code is shown below:
use neon::prelude::*;
fn hello(mut cx: FunctionContext) -> JsResult<JsString> {…}
fn pass_str(mut cx: FunctionContext) -> JsResult<JsString> {…}
fn pass_array(mut cx: FunctionContext) -> JsResult<JsArray> {…}
fn pass_object(mut cx: FunctionContext) -> JsResult<JsArray> {…}
register_module!(mut cx, {
cx.export_function("hello", hello)?;
cx.export_function("passStr", pass_str)?;
cx.export_function("passArray", pass_array)?;
cx.export_function("passObject", pass_object)?;
Ok(())
});
NodeJS: Complete
This is the complete javascript file.
const {
hello,
passStr,
passArray,
passObject
} = require('../native/index.node');
console.log(hello(), "\n");
console.log(passStr("Cantaloupe Island"), "\n");
console.log(passArray(["60s", "jazz"]), "\n");
console.log(passObject({
title: "Cantaloupe Island",
tags: ["60s", "jazz"]
}));
With the result similar as below:
$ node lib/index.js
Aloha Done
| In Rust: Cantaloupe Island
Cantaloupe Island
| In Rust: ["60s", "jazz"]
[ '60s', 'jazz' ]
| Tags in Rust: ["60s", "jazz"]
[ '60s', 'jazz' ]
Rust Native: Hello
We can start from simple, from the official example.
fn hello(mut cx: FunctionContext) -> JsResult<JsString> {
Ok(cx.string("Aloha Done"))
}
And if we strip down the javascript to this:
const { hello } = require('../native/index.node');
console.log(hello());
We can have the result similar as below:
$ node lib/index.js
Aloha Done
We haven’t pass anything yet to rust
.
Rust Native: Passing String
Still from the official documentation example. We can pass astring as parameter argument.
fn pass_str(mut cx: FunctionContext) -> JsResult<JsString> {
let string = cx.argument::<JsString>(0)?.value();
println!("| In Rust: {}", string);
Ok(cx.string(string))
}
And if we strip down the javascript to this:
const { passStr } = require('../native/index.node');
console.log(passStr("Cantaloupe Island"));
We can have the result similar as below:
$ node lib/index.js
| In Rust: Cantaloupe Island
Cantaloupe Island
We have pass JsString
type to rust
,
and cast to rust’s native String
type.
Rust Native: Cast Helper
Before we continue,
we need two helper functions to cast from javascript type,
into rust’s native String
type.
First the string element.
fn jsvalue_to_string(
js_value: Handle<JsValue>, default: Handle<JsString>
) -> String {
js_value
.downcast::<JsString>()
.unwrap_or(default)
.value()
}
And then the vector
, containing those string elements.
fn vec_jsvalue_to_string(
vec_value: Vec<Handle<JsValue>>, default: Handle<JsString>
) -> Vec<String> {
let vec_string: Vec<String> = vec_value
.iter()
.map(|js_value| { jsvalue_to_string(*js_value, default) })
.collect();
vec_string
}
Rust Native: Passing Array
The casting process is getting more complex now.
The thing is we have to deal with,
the difference between rust vector
and JsArray
.
fn pass_array(mut cx: FunctionContext) -> JsResult<JsArray> {
// JsArray to Vec
let js_array_ori: Handle<JsArray> = cx.argument::<JsArray>(0)?;
let default: Handle<JsString> = cx.string("");
let vec_value: Vec<Handle<JsValue>> =
js_array_ori.to_vec(&mut cx)?;
let vec_string: Vec<String> =
vec_jsvalue_to_string(vec_value, default);
println!("| In Rust: {:?}", vec_string);
// Vec to JsArray
let js_array_new = JsArray::new(&mut cx, vec_string.len() as u32);
for (i, obj) in vec_string.iter().enumerate() {
let js_string = cx.string(obj);
js_array_new.set(&mut cx, i as u32, js_string).unwrap();
}
Ok(js_array_new)
}
With the javascript as below:
const { passArray } = require('../native/index.node');
console.log(passArray(["60s", "jazz"]));
We can have the result similar as below:
$ node lib/index.js
| In Rust: ["60s", "jazz"]
[ '60s', 'jazz' ]
Rust Native: Passing Object
If the above haven’t got you so much challenge, we can contiune with passing our previous song record as objects.
fn pass_object(mut cx: FunctionContext) -> JsResult<JsArray> {
let js_object_handle: Handle<JsObject> = cx.argument(0)?;
let js_object = js_object_handle
.downcast::<JsObject>()
.unwrap_or(JsObject::new(&mut cx));
let js_array_default: Handle<JsArray> =
JsArray::new(&mut cx, 0 as u32);
let js_array_new: Handle<JsArray> =
js_object
.get(&mut cx, "tags")?
.downcast::<JsArray>()
.unwrap_or(js_array_default);
let default: Handle<JsString> = cx.string("");
let vec_value: Vec<Handle<JsValue>> =
js_array_new.to_vec(&mut cx)?;
let vec_string: Vec<String> =
vec_jsvalue_to_string(vec_value, default);
println!("| Tags in Rust: {:?}", vec_string);
Ok(js_array_new)
}
Then consider give a paramater argument, as input in the javascript as below:
console.log(passObject({
title: "Cantaloupe Island",
tags: ["60s", "jazz"]
}));
We can have the result similar as below:
$ node lib/index.js
| Tags in Rust: ["60s", "jazz"]
[ '60s', 'jazz' ]' ]
We are done.
What is Next 🤔?
I thinks that’s all.
Consider continue reading [ C# - Playing with Records - Part One ].