將結構體匯出到 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] 註解結構體表示我們將產生必要的特徵實作,以將此類型轉換為/從 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_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] 屬性)。