將結構體匯出至 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 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 物件就會被「中立化」,因為它的內部指標會被設為 null。這表示未來使用此物件應該會觸發 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
型別是一種在執行時動態強制執行此操作的方法(而不是像通常發生的編譯時)。在 WasmRefCell
中加入是這裡相同的概念,為別名添加運行時檢查,這些別名通常在編譯時發生。這目前是一個 Rust 特定的功能,實際上並不在 wasm-bindgen
工具本身中,它僅在 Rust 產生的程式碼中(又名 #[wasm_bindgen]
屬性)。