Exporting a struct to JS (original) (raw)

The `wasm-bindgen` Guide

Exporting a struct to JS

So far we've covered JS objects, importing functions, and exporting functions. This has given us quite a rich base to build on so far, and that's great! We sometimes, though, want to go even further and define a JS class in Rust. Or in other words, we want to expose an object with methods from Rust to JS rather than just importing/exporting free functions.

The #[wasm_bindgen] attribute can annotate both a struct and impl blocks to allow:

#![allow(unused)]
fn main() {
#[wasm_bindgen]
pub struct Foo {
    internal: i32,
}

#[wasm_bindgen]
impl Foo {
    #[wasm_bindgen(constructor)]
    pub fn new(val: i32) -> Foo {
        Foo { internal: val }
    }

    pub fn get(&self) -> i32 {
        self.internal
    }

    pub fn set(&mut self, val: i32) {
        self.internal = val;
    }
}
}

This is a typical Rust struct definition for a type with a constructor and a few methods. Annotating the struct with #[wasm_bindgen] means that we'll generate necessary trait impls to convert this type to/from the JS boundary. The annotated impl block here means that the functions inside will also be made available to JS through generated shims. If we take a look at the generated JS code for this we'll see:

import * as wasm from './js_hello_world_bg';

export class Foo {
    static __construct(ptr) {
        return new Foo(ptr);
    }

    constructor(ptr) {
        this.ptr = ptr;
    }

    free() {
        const ptr = this.ptr;
        this.ptr = 0;
        wasm.__wbg_foo_free(ptr);
    }

    static new(arg0) {
        const ret = wasm.foo_new(arg0);
        return Foo.__construct(ret)
    }

    get() {
        const ret = wasm.foo_get(this.ptr);
        return ret;
    }

    set(arg0) {
        const ret = wasm.foo_set(this.ptr, arg0);
        return ret;
    }
}

That's actually not much! We can see here though how we've translated from Rust to JS:

To be able to use new Foo(), you'd need to annotate new as #[wasm_bindgen(constructor)].

One important aspect to note here, though, is that once free is called the JS object is "neutered" in that its internal pointer is nulled out. This means that future usage of this object should trigger a panic in Rust.

The real trickery with these bindings ends up happening in Rust, however, so let's take a look at that.

#![allow(unused)]
fn main() {
// original input to `#[wasm_bindgen]` omitted ...

#[export_name = "foo_new"]
pub extern "C" fn __wasm_bindgen_generated_Foo_new(arg0: i32) -> u32 {
    let ret = Foo::new(arg0);
    Box::into_raw(Box::new(WasmRefCell::new(ret))) as u32
}

#[export_name = "foo_get"]
pub extern "C" fn __wasm_bindgen_generated_Foo_get(me: u32) -> i32 {
    let me = me as *mut WasmRefCell<Foo>;
    wasm_bindgen::__rt::assert_not_null(me);
    let me = unsafe { &*me };
    return me.borrow().get();
}

#[export_name = "foo_set"]
pub extern "C" fn __wasm_bindgen_generated_Foo_set(me: u32, arg1: i32) {
    let me = me as *mut WasmRefCell<Foo>;
    wasm_bindgen::__rt::assert_not_null(me);
    let me = unsafe { &*me };
    me.borrow_mut().set(arg1);
}

#[no_mangle]
pub unsafe extern "C" fn __wbindgen_foo_free(me: u32) {
    let me = me as *mut WasmRefCell<Foo>;
    wasm_bindgen::__rt::assert_not_null(me);
    (*me).borrow_mut(); // ensure no active borrows
    drop(Box::from_raw(me));
}
}

As with before this is cleaned up from the actual output but it's the same idea as to what's going on! Here we can see a shim for each function as well as a shim for deallocating an instance of Foo. Recall that the only valid wasm types today are numbers, so we're required to shoehorn all of Foo into au32, which is currently done via Box (like std::unique_ptr in C++). Note, though, that there's an extra layer here, WasmRefCell. This type is the same as RefCell and can be mostly glossed over.

The purpose for this type, if you're interested though, is to uphold Rust's guarantees about aliasing in a world where aliasing is rampant (JS). Specifically the &Foo type means that there can be as much aliasing as you'd like, but crucially &mut Foo means that it is the sole pointer to the data (no other &Foo to the same instance exists). The RefCell type in libstd is a way of dynamically enforcing this at runtime (as opposed to compile time where it usually happens). Baking in WasmRefCell is the same idea here, adding runtime checks for aliasing which are typically happening at compile time. This is currently a Rust-specific feature which isn't actually in thewasm-bindgen tool itself, it's just in the Rust-generated code (aka the#[wasm_bindgen] attribute).