"wasm 中 JS 物件" 的 Polyfill
wasm-bindgen
的主要目標之一是允許在 wasm 中使用和傳遞 JS 物件,但這在今天是不允許的!雖然確實如此,但這就是 polyfill 的用武之地。
這裡的問題是我們如何將 JS 物件硬塞到 Wasm 使用的 u32
中。目前這種方法的策略是在產生的 foo.js
檔案中維護一個模組區域變數:一個 heap
。
「堆疊」上的臨時 JS 物件
foo.js
中 heap
的第一個插槽被視為堆疊。這個堆疊,就像典型的程式執行堆疊一樣,向下成長。JS 物件被推到堆疊的底部,它們在堆疊中的索引是傳遞給 wasm 的識別符號。維護一個堆疊指標以找出下一個項目被推送的位置。
然後,JS 物件也僅從堆疊的底部移除。移除只是儲存 null 然後遞增計數器。由於此方案的「堆疊式」性質,它僅適用於 Wasm 不保留 JS 物件時(也就是說,它僅在 Rust 術語中獲得「參考」)。
讓我們來看一個例子。
#![allow(unused)] 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)] 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 物件(不是參考)被假定在 Wasm 模組內具有動態生命週期。因此,嚴格的堆疊推送/彈出將不起作用,我們需要更永久的儲存空間來儲存 JS 物件。為了應對這種情況,我們建立了自己的某種「平板分配器」。
一圖(或程式碼)勝過千言萬語,所以讓我們展示一下範例中發生的情況。
#![allow(unused)] fn main() { // foo.rs #[wasm_bindgen] pub fn foo(a: JsValue) { // ... } }
請注意,我們之前使用的 JsValue
前面缺少 &
,而在 Rust 術語中,這表示它正在取得 JS 值的所有權。匯出的 ES 模組介面與之前相同,但所有權機制略有不同。讓我們看看產生的 JS 的平板在作用中
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
作為平板分配器來獲取一個插槽來儲存物件,一旦找到該物件,就會在那裡放置一個結構。請注意,這是在陣列的右半部,不像堆疊位於左半部。這種規範大致反映了普通程式中的堆疊/堆積。
這個產生的模組另一個值得注意的方面是 __wbindgen_object_drop_ref
函式。這個函式實際上是被匯入到 wasm 中,而不是在這個模組中使用!這個函式用於標記 Rust 中 JsValue
的生命週期結束,換句話說,當它超出作用域時會被呼叫。除此之外,這個函式基本上只是一個通用的「slab 釋放」實作。
最後,我們再來看看 Rust 產生的程式碼。
#![allow(unused)] 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)] 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 傳遞過來的索引。這裡的解構子 (destructor) 會呼叫 __wbindgen_object_drop_ref
函式,以釋放我們對 JS 物件的參考計數,從而釋放我們在上面看到的 slab
中的槽位。
如果您還記得,當我們在上面使用 &JsValue
時,我們產生了一個圍繞本地綁定的 ManuallyDrop
包裝器,那是因為我們想避免在物件來自堆疊時呼叫這個解構子。
實際上使用 heap
上面的解釋與目前實際情況非常接近,但實際上存在一些差異,尤其是在處理諸如 undefined
、null
等常數值時。請務必查看實際產生的 JS 程式碼和產生程式碼以了解完整細節!