"wasm 中的 JS 物件" 的 Polyfill
`wasm-bindgen` 的主要目標之一是允許在 wasm 中使用和傳遞 JS 物件,但這在今天是不允許的!雖然的確如此,但這就是 polyfill 發揮作用的地方。
這裡的問題是我們如何將 JS 物件硬塞到一個 u32
中供 Wasm 使用。目前這種方法的策略是在生成的 foo.js
檔案中維護一個模組局部變數:一個 heap
。
「堆疊」上的臨時 JS 物件
foo.js
中 heap
的第一個位置被視為堆疊。這個堆疊,就像典型的程式執行堆疊一樣,向下增長。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
。這個函數會使用 heap
和 heap_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
上面的解釋與今天發生的情況非常接近,但實際上有一些差異,特別是關於處理像 undefined
、null
等常數值。請務必查看實際產生的 JS 和產生程式碼以了解完整細節!