Can Node use threaded Rust libraries?
Posted by in Programming onI've been working with threads, Rust, and Node.js recently. Working in JavaScript for years has gotten me to be very comfortable with the asynchronous model, and this experience transferred almost seamlessly into using Rust's threads.
Today, though, I got to thinking about two things:
- I've never written a binary extension for Node.js.
- I don't know if such an extension could be truly threaded.
If you're like me, then it's time to fix that!
Setup
You'll need to have some things installed before you can continue.
- NPM & Node.js. If you don't have it, I recommend using NVM to install it. The instructions seem complicated, but they're just very detailed.
- Cargo & Rust. If you don't have it, I recommend using Rustup to install it. It does pretty much the same thing as NVM, but with less documentation.
First, you'll need to set up the test area. I want to keep the code-bases somewhat separated by language, so let's do a folder for Node (consumer
) and a folder for Rust (ffi
):
$ cargo init --lib ffi
$ mkdir consumer
$ cd consumer
$ npm init -f
The Binary Extension
Since this is a proof-of-concept, use something simple for your library that can be easily converted into threads later. I wrote a simple Rust library that runs n
iterations, m
times.
Edit ffi/Cargo.toml
to add these lines:
[lib]
crate-type = ["dylib"]
This will tell Rust that you're trying to build a dynamically linked library.
Edit ffi/src/lib.rs
:
/// Create `n` counters that count to `m`. Returns 0 if all threads return
/// successfully, or else -1.
///
/// ```c
/// int create_counters(unsigned int m, unsigned int n);
/// ```
#[no_mangle]
pub extern "C" fn create_counters(m: u32, n: u32) -> i32 {
println!("Spawning {} threads", m);
println!("Each thread counts to {}", n);
let threads = (0..m).map(|i| {
println!("Thread #{} spawned", i);
for j in 0..n {
println!("Thread #{} -> {}", i, j);
}
}).collect::<Vec<_>>();
0
}
// We will use this to make sure it's threading in Rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_threads() {
assert_eq!(create_counters(5, 100), 0);
}
}
Run your tests with cargo test
and you should see a 5 iterators count from 0 to 99. If that worked, then build the release version:
$ cargo build --release
Now, it's time to bind the library to JavaScript. You'll need to install the NPM FFI package to continue, so navigate to the consumer
folder and install it:
$ npm install --save-dev ffi
Then, edit the consumer/index.js
file:
const ffi = require('ffi');
const path = require('path');
const library_name = path.resolve(__dirname, '../ffi/target/release/libffi');
const library = ffi.Library(library_name, {
create_counters: ["int32", ["uint32", "uint32"]]
});
console.log(library.create_counters(5, 100));
Run this in Node, and you should see output very similar to what you saw with cargo test
.
$ node consumer/index.js
Spawning 5 threads
Each thread counts to 100
Thread #0 spawned
Thread #0 -> 0
Thread #0 -> 1
Thread #0 -> 2
...
Thread #4 -> 97
Thread #4 -> 98
Thread #4 -> 99
If your output looks like this, great! You've now tackled the first problem: you have a Rust library that has been linked with Node.js. Pat yourself on the back, that was the most intimidating part for me.
Threads
The hard part is out of the way. Now, all we have to do is make sure that threads work. That might sound like I'm oversimplifying, but if you are comfortable with Node, you'll probably see some vaguely patterns here.
Edit your ffi/src/lib.rs
file again:
#[no_mangle]
pub extern "C" fn create_counters(m: u32, n: u32) -> i32 {
println!("Spawning {} threads", m);
println!("Each thread counts to {}", n);
let threads = (0..m).map(|i| {
println!("Thread #{} spawned", i);
// Span a thread, and return it's joiner. // +++
std::thread::spawn(move || { // +++
for j in 0..n {
println!("Thread #{} -> {}", i, j);
// Insert a little bit of delay in the thread. // +++
std::thread::sleep(std::time::Duration::from_micros(1)); // +++
}
}) // +++
}).collect::<Vec<_>>();
// 0
// +++ everything past here is new +++
// Create an array of any joiners that failed.
let failures = threads.into_iter()
.filter_map(|joiner| joiner.join().err())
.collect::<Vec<_>>();
// +++ If there are any failures, return -1; else return 0
if failures.is_empty() { 0 } else { -1 }
}
Now, if you run cargo test
, then you should see the threads executing out-of-order. If so, great! Your threads are working!
The final step is to see if those threads work in Node.js...
$ node consumer/index.js
Spawning 5 threads
Each thread counts to 100
Thread #0 spawned
Thread #1 spawned
Thread #2 spawned
Thread #3 spawned
Thread #4 spawned
Thread #1 -> 0
Thread #1 -> 1
Thread #0 -> 0
Thread #3 -> 0
Thread #1 -> 2
...
Thread #0 -> 99
Thread #2 -> 98
Thread #4 -> 98
Thread #3 -> 99
Thread #2 -> 99
Thread #4 -> 99
And it's done! We've proven that Node.js can use threaded binaries.
Summary
In this post, we learned about:
- Creating Rust libraries.
- Creating Node projects.
- Writing threaded Rust code.
- Linking Rust libraries into Node.