Building and using a sys-crate with Rust - let's make a node clone (well kind of...)
Rust is an awesome language and platform to use, however there’s so much great software already written in c/c++. Luckily it’s not too complicated to make use of c/c++ projects in Rust. In this short post I’ll show you how.
From a high-level perspective you can take any c/c++ project, for this example I’m going to use Duktape, the lightweight embeddable JavaScript engine. I’m choosing Duktape because it’s very simple to build it - it’s just 1 .c
file. Hopefully this will make the steps easy to understand and we won’t get too bogged down in build system issues.
The steps are:
- Create a *-sys create which has your native dependency included along with knowledge of how to build it and exposes a preferably safe API to consumers. We’ll be making
duktape-sys
crate here. - Make a crate that consumes the
-sys
crate and uses it to build something cool. Here we’ll be buildingduk
which is just a binary which you can pass a JavaScript file too and it will print out the last number on the stack
Creating duktape-sys crate
First we’ll create a new library crate using cargo:
$ cargo new --lib duktape2-sys
We’ll download and bundle duktape source code from https://duktape.org/. I’ve added this to a vendor
directory:
.
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── src
│ └── lib.rs
├── target
│ ├── ...
└── vendor
└── duktape-2.3.0
└── ...
We’ll need some dependencies:
// cargo.toml
[package]
name = "duktape2-sys"
version = "0.1.0"
authors = ["Matt Harrison <[email protected]>"]
[dependencies]
libc = "0.2.0"
[build-dependencies]
cc = "1.0"
We’ll use the libc crate here to use the c_void
type, we’ll pass this around in Rust as a opaque pointer to some memory allocated by C. We’ll use the cc crate because it provides us with a crossplatform way to build libduktape from source. Speaking of building duktape, let’s create a build.rs file:
// build.rs
extern crate cc;
fn main() {
cc::Build::new()
.file("vendor/duktape-2.3.0/src/duktape.c")
.compile("duktape");
}
What this magical looking script will do is: when building our crate it will compile vendor/duktape-2.3.0/src/duktape.c
into a static library called libduktape.a
inside our target
directory and also add a linker library search flag to the rust compiler so that it knows how to link the native library. That’s all we need to handle building and linking with the native code!
Let’s go and implement a Rust API that we can use in other crates:
// Crates
extern crate libc;
// Use statements
use std::error::Error;
use std::ffi::CString;
use std::fmt;
use std::os::raw::c_char;
use std::ptr;
// Aliases
use libc::c_void as void;
// Duktape constants
const DUK_COMPILE_EVAL: u32 = 1 << 3;
const DUK_COMPILE_SAFE: u32 = 1 << 7;
const DUK_COMPILE_NOSOURCE: u32 = 1 << 9;
const DUK_COMPILE_STRLEN: u32 = 1 << 10;
const DUK_COMPILE_NOFILENAME: u32 = 1 << 11;
// Define the FFI with duktape
extern "C" {
fn duk_create_heap(
arg1: *const void,
arg2: *const void,
arg3: *const void,
arg4: *const void,
arg5: *const void,
) -> *const void;
fn duk_destroy_heap(ctx: *const void);
fn duk_eval_raw(ctx: *const void, src: *const c_char, arg1: u32, arg2: u32) -> u32;
fn duk_get_int(ctx: *const void, idx: i32) -> i32;
}
// Create a DukHeap struct which just privately holds the pointer
// to the native duktape heap
pub struct DukHeap {
ctx: *const void,
}
impl DukHeap {
// Create a new default heap object
pub fn new() -> DukHeap {
unsafe {
let ctx = duk_create_heap(
ptr::null(),
ptr::null(),
ptr::null(),
ptr::null(),
ptr::null(),
);
DukHeap { ctx }
}
}
// Evaluates a JavaScript script within the current context
// Returns the i32 on top of the stack or an error
pub fn eval_script(&mut self, script: &str) -> Result<i32, Box<Error>> {
let code = CString::new(script)?;
let flags = 0
| DUK_COMPILE_EVAL
| DUK_COMPILE_NOSOURCE
| DUK_COMPILE_STRLEN
| DUK_COMPILE_NOFILENAME
| DUK_COMPILE_SAFE;
let res = unsafe { duk_eval_raw(self.ctx, code.as_ptr(), 0, flags) };
if res != 0 {
return Err(Box::new(DukError::new("Eval failed")));
}
let result = unsafe { duk_get_int(self.ctx, -1) };
Ok(result)
}
}
impl Drop for DukHeap {
// When our DukHeap is dropped we also destroy the native heap
fn drop(&mut self) {
unsafe {
duk_destroy_heap(self.ctx);
}
}
}
// A custom error type
#[derive(Debug)]
struct DukError {
details: String,
}
impl DukError {
fn new(msg: &str) -> DukError {
DukError {
details: msg.to_string(),
}
}
}
impl fmt::Display for DukError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl Error for DukError {
fn description(&self) -> &str {
&self.details
}
}
// A simple test
#[cfg(test)]
mod tests {
use super::DukHeap;
#[test]
fn my_test() {
let mut heap = DukHeap::new();
let script = "12 * 65 / 3";
let result = heap.eval_script(script);
assert_eq!(result.unwrap(), 260);
}
}
Running cargo test
tells us that things are working quite nicely!
The next step is to use this sys-crate in some consumer crate.
Let’s create a new crate called dukrs
:
$ cargo new --bin dukjs
We’ll add our duktape2-sys
crate as a dependency using the relative path as we won’t be publishing it to crates.io.
// cargo.toml
[package]
name = "dukjs"
version = "0.1.0"
authors = ["Matt Harrison <[email protected]>"]
[dependencies]
duktape2-sys = { path = "../duktape2-sys" }
Let’s write our main.rs
to load a script passed as an argument to our binary and then execute it and print the result.
// main.rs
extern crate duktape2_sys;
use std::env;
use std::fs::File;
use std::io::Read;
use duktape2_sys::DukHeap;
fn main() {
let filename = env::args()
.nth(1)
.expect("Provide a script name to execute");
let mut file = File::open(filename).expect("Cannot read file");
let mut script = String::new();
file.read_to_string(&mut script).expect("Can't read script");
let mut heap = DukHeap::new();
let result = heap.eval_script(&script).expect("Couldn't evaluate script");
println!("Result was {}", result);
}
Pretty simple, right? Let’s create a script to test this on, we’ll write a simple script using js to calculate the 20th fibonacci number:
// fib.js
function fibonacci(num) {
if (num == 0) return 0;
if (num == 1) return 1;
return fibonacci(num - 1) + fibonacci(num - 2);
}
fibonacci(20);
Let’s build our binary:
$ cargo build --release
Now let’s give it a spin to see if it all works:
➜ dukjs git:(master) ✗ ./target/release/dukjs fib.js
Result was 6765
That’s pretty sweet. So to recap: we created a sys-crate that can compile a native C library and link with a safe Rust wrapper API. We build a simple crate to consume that sys-crate which creates a binary that knows how to evaulate JavaScript.