Exchanging data between Node and Rust
The FFI interface
Recently i had to do some integration between Rust and Node.js app. I was suprised how relatively straightforward job it was. I found working with node-ffi to be pretty good apart from some ocasional unclear error messages.
The next important element of the FFI puzzle is the ref library along with its little helpers ref-array and ref-struct.
The ref
library allows to manipulate the data obtained from rust functions. It essentially allows pointer operations to be performed in javascript. The ref-array
is a helper allowing to manipulate the C arrays and the ref-struct
help in creating a javascript based representations of the C (and Rust) structures.
Return array
In the first example we will see how we can return a array from a Rust function to Javascript.
use std::slice;
#[no_mangle]
fn return_incremented(data: *const u8, len: usize) -> *const u8 {
/* Lets turn it into format thats easier to operate on */
let array = unsafe { slice::from_raw_parts(data, len as usize) };
let mut return_array = vec![];
/* The return values are function of input values */
for v in array.iter() {
return_array.push(v + 1);
}
/* Obtain a pointer to the array */
let buf = return_array.as_ptr();
/* Stop Rust from destroying the variable */
std::mem::forget(return_array);
buf
}
The #[no_mangle]
prevents compiler from changing the name of the function. We then create a variable of type slice
that allows for easier manipulation. We then perform some silly operations on the array. Once its done we need to obtain the raw pointer to the variable so that we can return it back to the Javascript caller.
The call to std::mem::forget
is very important here. It prevents Rust from calling this variable destuctor when it leaves the function scope. I don't want to reiterate the basic FFI info here so to understand to fundamentals of FFI i highly recommend the The Rust FFI Omnibus.
Lets compile the Rust code and have a look at how the function can be invoked in javascript.
const ref = require('ref');
const ArrayType = require('ref-array');
const ByteArray = ArrayType(ref.types.uint8);
const lib = ffi.Library(path.join(__dirname, './target/release/librust_ffi_js'), {
return_incremented: [ByteArray, [ByteArray, 'int']]
});
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
/* Add 1 to every element of js_array and return new array */
(function(js_array){
const ret_array = lib.return_incremented(js_array, js_array.length);
/* Extract the data from the returned buffer */
const out = ret_array.buffer.reinterpret(js_array.length).toString('hex');
console.log('Modify array: ', out);
})(array);
And when we run this code we should get the output looking like this:
Modify array: 02030405060708090a
So the interesting part of the javascript code is the: ret_array.buffer.reinterpret(js_array.length).toString('hex')
. The return type of the function is ByteArray object we defined. The ByteArray contains a buffer object which is extended by the ref
package. The ref
provides a bunch of methods that allow allocation, type casting, dereferencing etc of the objects. Full ref
documentation is available here. The reinterpret method will dereference the returned poiner and return an array of specified length. Which correcponds to array returned from out Rust function. The ref
package extends the javascript Buffer class so all Buffer methods are available. The whole puffer can be turned in to Blob or written to file as needed.
Function with a callback
You cannot really use javascript without using callbacks. So lets see how we can infest out Rust functions with them.
#[no_mangle]
fn return_modified(data: *const u8, len: usize, callback: fn(u8) -> u8) -> *const u8 {
/* Lets turn it into format thats easier to operate on */
let array = unsafe { slice::from_raw_parts(data, len as usize) };
let mut return_array = vec![];
/* The return values are function of input values
the function is provided by caller */
for v in array.iter() {
return_array.push(callback(*v));
}
/* Obtain a pointer to the array */
let buf = return_array.as_ptr();
/* Stop Rust from destroying the variable */
std::mem::forget(return_array);
buf
}
So not much changes here. We just need to tell the function to expect the function to be passed in. This is pretty much straighforward on the Rust side. In this scenario javascript is doing the heavy stuff.
const ref = require('ref');
const ArrayType = require('ref-array');
const ByteArray = ArrayType(ref.types.uint8);
const CallbackMutate = ffi.Function('int', ['int']);
const lib = ffi.Library(path.join(__dirname, './target/release/librust_ffi_js'), {
return_modified: [ByteArray, [ByteArray, 'int', CallbackMutate]],
});
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
/* Mutate the js_array with the logic in the callback function */
(function(js_array){
const ret_array = lib.return_modified(js_array, js_array.length, function (num) {
return num + 3;
});
const out = ret_array.buffer.reinterpret(js_array.length).toString('hex');
console.log('With callback: ', out);
})(array);
So again we define the javascript objects but we also need to specify the callbac function type that will be passed to the Rust function. Slightly different, but still quite easy. When run we should see the output:
With callback: 0405060708090a0b0c
The returned pointer to array needs to be reinterpreted and bounded to specific length. But what if the caller wont know the length of the returned array. In the 2 previous examples we assumed that the returned array will be of same length as the input array. Lets see how we can address this.
Returning struct
One of the approaches is to wrap the array to be returned in a struct along with the length of the array and send the whole struct back to the caller.
#[repr(C)]
struct ArrayStruct {
data: *const u8,
len: usize,
}
#[no_mangle]
fn return_struct(data: *const u8, len: usize, callback: fn(u8) -> bool) -> *const ArrayStruct {
/* Lets turn it into format thats easier to operate on */
let array = unsafe { slice::from_raw_parts(data, len as usize) };
let return_array: Vec<_> = array.iter().filter(|&&v| callback(v)).map(|v| *v).collect();
/* Obtain a pointer to the array */
let buf = return_array.as_ptr();
let len = return_array.len();
/* Stop Rust from destroying the variable */
std::mem::forget(return_array);
unsafe {
std::mem::transmute(Box::new(ArrayStruct {
data: buf,
len: len,
}))
}
}
In here we need to use Box to place the struct onto the heap. Otherwise it would be located on the stack and destroyed after the method finishes. On the javascript side we then need to create a javascript object with the object layout resempling the Rust struct.
const ref = require('ref');
const ArrayType = require('ref-array');
const ByteArray = ArrayType(ref.types.uint8);
const StructType = require('ref-struct');
const ArrayStruct = StructType({
data: ByteArray,
len: ref.types.int
});
const ArrayStructPtr = ref.refType(ArrayStruct);
const CallbackFilter = ffi.Function('bool', ['int']);
const lib = ffi.Library(path.join(__dirname, './target/release/librust_ffi_js'), {
return_struct: [ArrayStructPtr, [ByteArray, 'int', CallbackFilter]]
});
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
/* What if the returning array length is changed. It would be great to have the new length returned as well */
(function(js_array) {
const ret_struct = lib.return_struct(js_array, js_array.length, function (num) {
/* lets filter all even numbers */
return num % 2;
});
const struct_value = ret_struct.deref();
const arr_len = struct_value.len;
const out = struct_value.data.buffer.reinterpret(arr_len).toString('hex');
console.log('Array bytes: ', out);
console.log('Array len: ', arr_len);
})(array);
So we still need the ByteArray
as the input array. We create the new callback that will act as the filter method in Rust function. Then we need to create the javascript equivalent of the Rust ArrayStruct
we defined.
const ArrayStruct = StructType({
data: ByteArray,
len: ref.types.int
});
The first field is the data of type ByteArray which is uint8 type wrapped in ref-array
. The len field is just an integer.
Then we create a pointer to this object by wrapping it in refType
i.e. we create a 'pointer' to it:
const ArrayStructPtr = ref.refType(ArrayStruct);
To match the Rust function declaration return type:
fn return_struct(data: *const u8, len: usize, callback: fn(u8) -> bool) -> *const ArrayStruct
The returned value itself is going to be a pointer to the struct therefore we need the call to deref()
before we can access the vaues in it:
const struct_value = ret_struct.deref();
const arr_len = struct_value.len;
After this step the process is similar as in previous examples.
With this all in place we can run the index.js in node and see the output:
Array bytes: 0103050709
Array len: 5
Conclusion
The entire example code as available on my Gitlab repo. I think that the above shows the principles needed to start using the FFI interface. Most of the data passed in from Rust (or even C) are pointers to data encapsulated in structs. As shown above with the ref
library accessing it becomes relatively easy.