"wasm 中 JS 物件" 的 Polyfill

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

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

「堆疊」上的臨時 JS 物件

foo.jsheap 的第一個插槽被視為堆疊。這個堆疊,就像典型的程式執行堆疊一樣,向下成長。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。此函式將使用 heapheap_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

上面的解釋與目前實際情況非常接近,但實際上存在一些差異,尤其是在處理諸如 undefinednull 等常數值時。請務必查看實際產生的 JS 程式碼和產生程式碼以了解完整細節!