kaffeerost

Experimental utilities for integrating Rust with WebAssembly


License
MIT
Install
npm install kaffeerost@0.0.1

Documentation

WASM experiments

Some experiments after seeing Rust PR #45905.

Getting Rust to compile to WASM

It's quite easy now! Assuming you've installed Rust with rustup (highly recommended), it's just:

rustup update
rustup target add wasm32-unknown-unknown --toolchain nightly
rustc +nightly --target wasm32-unknown-unknown -O hello.rs

Using the wrap function

While this repo first started as a playground, over time, a generic wrapper tool was developed. Its goal is to allow you to easily call Rust functions in WASM from JS.

Let's see how we can use it to handle some trivial functions.

Boilerplate

First, we'll need a way to load Rust code as WASM:

const loadRust = require("../src/compile");

You can also load your own .wasm file, of course. Next, the wrapper helper:

const wrap = require("../src/wrap");

We'll be calling this a lot.

And, to test that this actually works (yes, this code is tested), let's import the handy ava library.

const { test } = require("ava");

Easy as 1 2 3

Okay, we should be good to go. Let's see if this works:

test("it works!", async (t) => {
    const instance = await loadRust(`
        #[no_mangle]
        pub fn is_answer(a: i32) -> bool {
            a == 42
        }
    `);
    const isAnswer = wrap(instance.exports, "is_answer", ["i32"], "bool");
    t.deepEqual(isAnswer(42), true);
});

Looking good! The thing with this wrapper tool is that it needs to be able to convert between Rust and JS types. Primitives like integers are easy, but stuff like strings or arrays are a bit more complicated.

A-B-C? Actually not that easy

Let's try to write a function that returns a Rust String:

test("get the time", async (t) => {
    const instance = await loadRust(`
        #[no_mangle]
        pub extern "C" fn time() -> String {
            String::from("Beer o'clock")
        }
    `);
    const time = wrap(instance.exports, "time", [], "String");

Surprisingly, we cannot wrap this! The error message will tell you why:

    const error = await t.throws(() => {
        const now = time();
    });
    t.is(error.message, "Ensure failed: You need to export an `alloc` function to get strings from WASM");
});

So, to get a Rust String (or any other non-primitive type basically), we need to allocate memory. And not even to store the string itself (which Rust stores on the heap), just to be able to tell the WASM function where to write the stack data of the String to (It's passed as an out paramter as a String needs three words of storage).

Let's give this another go:

test("really get the time", async (t) => {
    const instance = await loadRust(`
        use std::mem;
        use std::os::raw::c_void;

        #[no_mangle]
        pub extern "C" fn alloc(size: usize) -> *mut c_void {
            let mut buf = Vec::with_capacity(size);
            let ptr = buf.as_mut_ptr();
            mem::forget(buf); // This is JS' responsibility now
            return ptr as *mut c_void;
        }

        #[no_mangle]
        pub extern "C" fn time() -> String {
            String::from("Beer o'clock")
        }
    `);
    const time = wrap(instance.exports, "time", [], "String");
    t.deepEqual(time(), "Beer o'clock");
});

For now, this alloc export is something you'll just have to do. Sorry. It'll get better, I promise. (Deallocating memory is still an open issue -- chime in here!)

Writing code yourself is way to hard, let's just use Cargo

Okay, how about we try to do something useful? Like, I don't know, calculate a SHA1 has for some bytes. Do you know how to write implement SHA1 by hand? Well, me neither. So, let's use a crate (Rust library)! Here, I found one!

How to use it? Let's split this up a bit. Here is the Rust code to load the sha1 crate, use it to munch some string slice, and return a fancy String that contains the human-readable hash.

test("digest some yummy bytes", async (t) => {
    const rustCode = `
        extern crate sha1;
        use sha1::Sha1;

        #[no_mangle]
        pub extern "C" fn digest(data: &str) -> String {
            let mut m = Sha1::new();
            m.update(data.as_bytes());
            m.digest().to_string()
        }

Oh, and don't forget the alloc dance:

        use std::mem;
        use std::os::raw::c_void;

        #[no_mangle]
        pub extern "C" fn alloc(size: usize) -> *mut c_void {
            let mut buf = Vec::with_capacity(size);
            let ptr = buf.as_mut_ptr();
            mem::forget(buf); // This is JS' responsibility now
            return ptr as *mut c_void;
        }
    `;

and then we can compile this to WASM with a newly added list of dependencies (the format is { name: "version" }):

    const instance = await loadRust(rustCode, { sha1: "0.2" });

Et voilá, we can SHA1 all the strings now!

    const digest = wrap(instance.exports, "digest", ["&str"], "String");
    t.deepEqual(digest("foobar"), "8843d7f92416211de9ebb963ff4ce28125932878");
});

Debugging

You can enable more logging output by setting the environment variable LOG_LEVEL. The levels are error (0), warn, info, debug, trace (5). The default is 2 (info).