Getting started with Rust/WebAssembly

I’ve been using the Rust programming language at work quite a lot recently, and really like it.

In this week’s issue of This Week in Rust, I saw that WebAssembly was now supported natively in rustc (previously it was supported via Emscripten).

WebAssembly is a binary executable format designed to give (near-)native performance of code embedded in webpages.

Although I don’t do web development as work, I do sometimes do it for side projects, and it’s always fun to try something new!

Setup was easy (following these instructions over as Hello, Rust!) and I got a basic Javascript to Rust/WebAssembly function call working (following this tutorial).

Calling Javascript from Rust

So we have Javascript calling Rust.  Now let’s try getting Rust calling back into Javascript.

To this, first we have to tweak add.rs (now renamed to call-js.rs) to add an “extern” function, and call it from add_one rather than returning the value – not that the call to javascript_fn needs to be marked unsafe, because calling out of Rust always is.

extern "C" {
  pub fn javascript_fn(num: i32);
}

#[no_mangle]
pub fn add_one(x: i32) {
  unsafe {
    javascript_fn(x + 1);
  }
}

And then provide the Javascript function for Rust to call as an import.  The name of the function must match the name in the Rust code above.  It appears that “env” as the key is hard-coded from Rust – if anyone knows how to change this, please let me know!

var env = {
  javascript_fn: num => { alert(num); },
}

fetch('call-js.wasm')
.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, {env: env}))
.then(results => { results.instance.exports.add_one(41); });

This then gives the same behavior as the simple add example, just with a lot more code – great!

Passing Strings to Javascript

Of course, the next step is to pass a string.  This is a little more difficult, because strings aren’t a primitive type in WebAssembly, so there’s no convention for passing them.

To pass the string from Rust to Javascript, we can pass a pointer and a length, as follows:

extern "C" {
  pub fn javascript_fn(ptr: *const u8, len: u32);
}

#[no_mangle]
pub fn add_one(x: i32) {
  unsafe {
    let msg = format!("Hello world: {}", x + 1);
    javascript_fn(msg.as_ptr(), msg.len() as u32);
  }
}

Now we need to change the Javascript code to parse the pointer and length back into a string.  The “pointer” that’s passed to Javascript is actually just a number – an index into a special “memory” export (of type WebAssembly.Memory).  We can turn the relevant section of this memory into a Uint8Array, and then use the TextDecoder.decode() method to turn that into a string.

var mod;
var env = {
  javascript_fn: (ptr, len) => {
    var buf = new Uint8Array(mod.instance.exports.memory.buffer, ptr, len)
    var msg = new TextDecoder('utf8').decode(buf);
    alert(msg);
  }
}

fetch('pass_string.wasm')
.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, {env: env}))
.then(module => {
  mod = module;
  module.instance.exports.add_one(41);
});

Note that, in order to access the memory export, we also had to save a reference to the loaded module (in the “mod” variable).

If you run this, you now get an alert saying “Hello world: 42”.  The string is being constructed in Rust, and passed through to Javascript.

This is all a bit long-winded, but I haven’t found a better way – any observations on how to do it better would be appreciated!

Next Steps

Next step is to get Javascript to be able to pass strings into Rust… and then interwork between other Javascript and Rust objects.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s