将结构体导出到 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]` 注释结构体意味着我们将生成必要的特征实现,以将此类型转换为/从 JS 边界转换。这里注释的 `impl` 块意味着其中的函数也将通过生成的垫片提供给 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)); } }
和以前一样,这从实际输出中清理掉了,但它与正在发生的事情是一样的!在这里,我们可以看到每个函数的垫片,以及释放 `Foo` 实例的垫片。回想一下,目前唯一有效的 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]` 属性)中。