"wasm 中的 JS 物件" 的 Polyfill

`wasm-bindgen` 的主要目標之一是允許在 wasm 中使用和傳遞 JS 物件,但這在今天是不允許的!雖然的確如此,但這就是 polyfill 發揮作用的地方。

這裡的問題是我們如何將 JS 物件硬塞到一個 u32 中供 Wasm 使用。目前這種方法的策略是在生成的 foo.js 檔案中維護一個模組局部變數:一個 heap

「堆疊」上的臨時 JS 物件

foo.jsheap 的第一個位置被視為堆疊。這個堆疊,就像典型的程式執行堆疊一樣,向下增長。JS 物件被推送到堆疊的底部,而它們在堆疊中的索引就是傳遞給 wasm 的識別符號。維護一個堆疊指標以找出下一個項目被推入的位置。

JS 物件然後也僅從堆疊底部移除。移除只是簡單地儲存 null 然後遞增計數器。由於這種方案的「堆疊」性質,它僅在 Wasm 不保留 JS 物件時才有效(也就是說,它只會在 Rust 語法中取得「參考」)。

讓我們來看一個範例。


# #![allow(unused_variables)]
#fn main() {
// foo.rs
#[wasm_bindgen]
pub fn foo(a: &JsValue) {
    // ...
}
#}

在這裡,我們正在使用 `wasm-bindgen` 函式庫本身的特殊 `JsValue` 型別。我們匯出的函式 `foo` 採用物件的*參考*。這特別表示它無法在這次函式呼叫的生命週期之外持久化該物件。

現在我們實際想要產生的是一個看起來像(在 TypeScript 語法中)的 JS 模組

// foo.d.ts
export function foo(a: any);

而我們實際產生的看起來像這樣

// foo.js
import * as wasm from './foo_bg';

const heap = new Array(32);
heap.push(undefined, null, true, false);
let stack_pointer = 32;

function addBorrowedObject(obj) {
  stack_pointer -= 1;
  heap[stack_pointer] = obj;
  return stack_pointer;
}

export function foo(arg0) {
  const idx0 = addBorrowedObject(arg0);
  try {
    wasm.foo(idx0);
  } finally {
    heap[stack_pointer++] = undefined;
  }
}

在這裡,我們可以看見幾個值得注意的動作點

  • Wasm 檔案已重新命名為 `foo_bg.wasm`,我們可以看見這裡產生的 JS 模組是如何從 Wasm 檔案匯入的。
  • 接下來,我們可以看到我們的 `heap` 模組變數,它是用來儲存所有可從 wasm 參考的 JS 值。
  • 我們匯出的函式 `foo` 採用任意引數 `arg0`,該引數使用 `addBorrowedObject` 物件函式轉換為索引。然後將該索引傳遞給 Wasm,以便 Wasm 可以使用它。
  • 最後,我們有一個 `finally`,它會釋放堆疊位置,因為它不再使用,彈出在函式開頭推送的值。

深入研究 Rust 端以了解那裡發生了什麼也很有幫助!讓我們看看 `#[wasm_bindgen]` 在 Rust 中產生的程式碼


# #![allow(unused_variables)]
#fn main() {
// what the user wrote
pub fn foo(a: &JsValue) {
    // ...
}

#[export_name = "foo"]
pub extern "C" fn __wasm_bindgen_generated_foo(arg0: u32) {
    let arg0 = unsafe {
        ManuallyDrop::new(JsValue::__from_idx(arg0))
    };
    let arg0 = &*arg0;
    foo(arg0);
}
#}

與 JS 一樣,這裡值得注意的重點是

  • 原始函式 `foo` 在輸出中未修改
  • 這裡產生的函式(具有唯一名稱)是實際從 Wasm 模組匯出的函式
  • 我們產生的函式採用一個整數引數(我們的索引),然後將它包裝在 `JsValue` 中。這裡有一些小技巧不值得現在深入探討,但我們稍後會看到底層發生了什麼。

長期存在的 JS 物件

當 JS 物件僅在 Rust 中暫時使用時,例如僅在一次函式呼叫期間,上述策略很有用。但是,有時物件可能具有動態生命週期,或需要儲存在 Rust 的堆積上。為了應對這種情況,還有 JS 物件管理的第二部分,自然地對應於 JS `heap` 陣列的另一側。

傳遞給 Wasm 的 JS 物件若非參考(reference),會被假定在 Wasm 模組內部具有動態生命週期。因此,嚴格的堆疊(stack)推入/彈出(push/pop)機制將無法運作,我們需要為 JS 物件提供更永久的儲存空間。為了應對此情況,我們建立了自己的「slab 分配器」。

一圖勝千言(或是一段程式碼),所以讓我們用一個例子來說明會發生什麼事。


# #![allow(unused_variables)]
#fn main() {
// foo.rs
#[wasm_bindgen]
pub fn foo(a: JsValue) {
    // ...
}
#}

請注意,之前的 JsValue 前面缺少了 &,在 Rust 的術語中,這表示它正在取得 JS 值的所有權。匯出的 ES 模組介面與之前相同,但所有權機制略有不同。讓我們看看產生的 JS 的 slab 如何運作

import * as wasm from './foo_bg'; // imports from Wasm file

const heap = new Array(32);
heap.push(undefined, null, true, false);
let heap_next = 36;

function addHeapObject(obj) {
  if (heap_next === heap.length)
    heap.push(heap.length + 1);
  const idx = heap_next;
  heap_next = heap[idx];
  heap[idx] = obj;
  return idx;
}

export function foo(arg0) {
  const idx0 = addHeapObject(arg0);
  wasm.foo(idx0);
}

export function __wbindgen_object_drop_ref(idx) {
  heap[idx ] = heap_next;
  heap_next = idx;
}

與之前不同,我們現在是對 foo 的參數呼叫 addHeapObject,而不是 addBorrowedObject。這個函數會使用 heapheap_next 作為 slab 分配器,取得一個儲存物件的插槽,一旦找到就會在那裡放置一個結構。請注意,這是在陣列的右半部分進行的,不像位於左半部分的堆疊。這種方式大致反映了正常程式中的堆疊/堆積。

這個產生的模組另一個值得注意的地方是 __wbindgen_object_drop_ref 函數。這個函數實際上是匯入到 wasm 而不是在這個模組中使用的!這個函數用於表示 Rust 中 JsValue 的生命週期結束,換句話說,當它超出範圍時。除此之外,這個函數基本上只是一個通用的「slab 釋放」實作。

最後,讓我們再次看看產生的 Rust 程式碼


# #![allow(unused_variables)]
#fn main() {
// what the user wrote
pub fn foo(a: JsValue) {
    // ...
}

#[export_name = "foo"]
pub extern "C" fn __wasm_bindgen_generated_foo(arg0: u32) {
    let arg0 = unsafe {
        JsValue::__from_idx(arg0)
    };
    foo(arg0);
}
#}

啊,看起來熟悉多了!這裡沒有發生太多有趣的事情,所以讓我們繼續來看...

JsValue 的解剖

目前,JsValue 結構在 Rust 中實際上非常簡單,它是


# #![allow(unused_variables)]
#fn main() {
pub struct JsValue {
    idx: u32,
}

// "private" constructors

impl Drop for JsValue {
    fn drop(&mut self) {
        unsafe {
            __wbindgen_object_drop_ref(self.idx);
        }
    }
}
#}

換句話說,它是 u32 的 newtype 包裝,即我們從 wasm 傳遞過來的索引。這裡的解構子會呼叫 __wbindgen_object_drop_ref 函數,以放棄我們對 JS 物件的參考計數,釋放我們在上面看到的 slab 中的插槽。

如果您還記得,當我們在上面取得 &JsValue 時,我們在本地綁定周圍產生了一個 ManuallyDrop 的包裝,那是因為我們想避免在物件來自堆疊時呼叫此解構子。

實際上使用 heap

上面的解釋與今天發生的情況非常接近,但實際上有一些差異,特別是關於處理像 undefinednull 等常數值。請務必查看實際產生的 JS 和產生程式碼以了解完整細節!