將結構體匯出至 JS

到目前為止,我們已經涵蓋了 JS 物件、導入函式和匯出函式。這已經給了我們一個相當豐富的基礎,可以以此為基礎進行建構,這很棒!然而,有時我們會想要更進一步,並在 Rust 中定義一個 JS class。換句話說,我們想要從 Rust 向 JS 公開一個具有方法的物件,而不僅僅是導入/匯出自由函式。

#[wasm_bindgen] 屬性可以註解 structimpl 區塊,以允許

#![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] 屬性)。