将结构体导出到 JS
到目前为止,我们已经介绍了 JS 对象、导入函数和导出函数。这已经为我们提供了相当丰富的基础,非常棒!但是,有时候我们希望更进一步,在 Rust 中定义一个 JS class
。或者换句话说,我们希望从 Rust 向 JS 公开一个带有方法的对象,而不是仅仅导入/导出自由函数。
#[wasm_bindgen]
属性可以注释 struct
和 impl
代码块,以允许
#![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; } } }
这是一个典型的 Rust struct
定义,其中包含构造函数和一些方法。用 #[wasm_bindgen]
注释结构体意味着我们将生成必要的 trait 实现,以便将此类型与 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)] 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
类型是一种在运行时动态强制执行此操作的方法(而不是通常在编译时发生的操作)。Baking in WasmRefCell
的想法是相同的,为通常在编译时发生的别名添加运行时检查。这目前是一个 Rust 特定的功能,实际上并不在 wasm-bindgen
工具本身中,而只是在 Rust 生成的代码中(即 #[wasm_bindgen]
属性)。