将结构体导出到 JS

到目前为止,我们已经介绍了 JS 对象、导入函数和导出函数。这已经为我们提供了非常丰富的构建基础,这很棒!然而,我们有时希望更进一步,在 Rust 中定义一个 JS class。换句话说,我们希望从 Rust 向 JS 公开一个带有方法的对象,而不仅仅是导入/导出自由函数。

#[wasm_bindgen] 属性可以注释 structimpl 代码块,以允许


# #![allow(unused_variables)]
#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;
    }
}
#}

这是一个典型的 Rust struct 定义,用于定义具有构造函数和一些方法的类型。用 #[wasm_bindgen] 注释该结构体意味着我们将生成必要的 trait impls 来将此类型与 JS 边界之间进行转换。这里注释的 impl 代码块意味着内部的函数也将通过生成的 shim 提供给 JS。如果我们看一下为此生成的 JS 代码,我们会看到

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;
    }
}

实际上并没有多少!但是,我们可以在这里看到我们是如何从 Rust 转换为 JS 的

  • Rust 中的关联函数(那些没有 self 的函数)会转换为 JS 中的 static 函数。
  • Rust 中的方法会转换为 wasm 中的方法。
  • 手动内存管理也在 JS 中公开。需要调用 free 函数来释放 Rust 侧的资源。

要能够使用 new Foo(),你需要将 new 注释为 #[wasm_bindgen(constructor)]

但是,这里需要注意一个重要的方面,一旦调用 free,JS 对象就会被“阉割”,因为它的内部指针被置为空。这意味着将来使用此对象应该会在 Rust 中触发 panic。

然而,这些绑定的真正技巧最终发生在 Rust 中,所以让我们看一下。


# #![allow(unused_variables)]
#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));
}
#}

与之前一样,这已经从实际输出中清理过了,但它的思想与正在发生的事情相同!在这里,我们可以看到每个函数的 shim,以及用于释放 Foo 实例的 shim。回想一下,今天唯一有效的 wasm 类型是数字,因此我们需要将所有 Foo 都塞入 u32 中,这目前是通过 Box(如 C++ 中的 std::unique_ptr)完成的。但是请注意,这里还有一层额外的层,WasmRefCell。此类型与 RefCell 相同,并且可以大多忽略。

如果你感兴趣,此类型的目的是在别名猖獗的世界(JS)中维护 Rust 关于别名的保证。具体而言,&Foo 类型意味着可以根据你的意愿进行尽可能多的别名,但至关重要的是,&mut Foo 意味着它是数据的唯一指针(没有其他对同一实例存在的 &Foo)。libstd 中的 RefCell 类型是一种在运行时动态强制执行此操作的方法(而不是通常在编译时发生)。嵌入 WasmRefCell 这里的想法相同,为通常在编译时发生的别名添加运行时检查。这目前是一个 Rust 特定的功能,实际上并不在 wasm-bindgen 工具本身中,它只是在 Rust 生成的代码中(又名 #[wasm_bindgen] 属性)。