WebAssembly JavaScript 內建函式

WebAssembly JavaScript 內建函式是 JavaScript 操作的 Wasm 等價物,它提供了一種在 Wasm 模組內部使用 JavaScript 功能的方法,而無需匯入 JavaScript 膠水程式碼來提供 JavaScript 和 WebAssembly 值以及呼叫約定之間的橋樑。

本文解釋了內建函式的工作原理和可用型別,然後提供了一個使用示例。

匯入 JavaScript 函式的問題

對於許多 JavaScript 功能,常規匯入工作正常。然而,為 StringArrayBufferMap 等基本型別匯入膠水程式碼會帶來顯著的效能開銷。在這種情況下,WebAssembly 和大多數以其為目標的語言都期望一個緊密的內聯操作序列,而不是像常規匯入函式那樣工作的間接函式呼叫。

具體來說,將函式從 JavaScript 匯入到 WebAssembly 模組會因以下原因產生效能問題:

  • 現有 API 需要進行轉換以處理 this 值的差異,WebAssembly 函式 import 呼叫會將其保留為 undefined
  • 某些基本型別使用 JavaScript 運算子,如 ===<,這些運算子無法匯入。
  • 大多數 JavaScript 函式對其接受的值的型別極其寬容,而我們希望儘可能利用 WebAssembly 的型別系統來移除這些檢查和強制轉換。

考慮到這些問題,建立內建定義來將現有的 JavaScript 功能(例如 String 基本型別)適配到 WebAssembly,比匯入它並依賴間接函式呼叫更簡單且效能更好。

可用的 WebAssembly JavaScript 內建函式

以下各節詳細介紹了可用的內建函式。未來可能會支援其他內建函式。

字串操作

可用的 String 內建函式有:

"wasm:js-string" "cast"

如果提供的值不是字串,則丟擲錯誤。大致相當於:

js
if (typeof obj !== "string") throw new WebAssembly.RuntimeError();
"wasm:js-string" "compare"

比較兩個字串值並確定它們的順序。如果第一個字串小於第二個字串,返回 -1;如果第一個字串大於第二個字串,返回 1;如果字串嚴格相等,返回 0

"wasm:js-string" "concat"

等同於 String.prototype.concat()

"wasm:js-string" "charCodeAt"

等同於 String.prototype.charCodeAt()

"wasm:js-string" "codePointAt"

等同於 String.prototype.codePointAt()

"wasm:js-string" "equals"

比較兩個字串值是否嚴格相等,如果相等則返回 1,否則返回 0

注意:"equals" 函式是唯一一個對 null 輸入不丟擲異常的字串內建函式,因此 Wasm 模組在呼叫它之前不需要檢查 null 值。所有其他函式都沒有合理的方式處理 null 輸入,因此會對它們丟擲異常。

"wasm:js-string" "fromCharCode"

等同於 String.fromCharCode()

"wasm:js-string" "fromCharCodeArray"

從一個 Wasm 的 i16 值陣列建立一個字串。

"wasm:js-string" "fromCodePoint"

等同於 String.fromCodePoint()

"wasm:js-string" "intoCharCodeArray"

將一個字串的字元編碼寫入一個 Wasm 的 i16 值陣列。

"wasm:js-string" "length"

等同於 String.prototype.length

"wasm:js-string" "substring"

等同於 String.prototype.substring()

"wasm:js-string" "test"

如果提供的值不是字串,返回 0;如果是字串,返回 1。大致相當於:

js
typeof obj === "string";

如何使用內建函式?

內建函式的工作方式與從 JavaScript 匯入的函式類似,不同之處在於你使用的是在保留名稱空間(wasm:)中定義的、用於執行 JavaScript 操作的標準 Wasm 函式等價物。因此,瀏覽器可以為它們預測並生成最最佳化的程式碼。本節總結了如何使用它們。

JavaScript API

內建函式在編譯時透過在呼叫編譯和/或例項化模組的方法時指定 compileOptions.builtins 屬性作為引數來啟用。它的值是一個字串陣列,用於標識你想要啟用的內建函式集:

js
WebAssembly.compile(bytes, { builtins: ["js-string"] });

compileOptions 物件可用於以下函式:

WebAssembly 模組特性

在你的 WebAssembly 模組中,你現在可以從 wasm: 名稱空間匯入在 compileOptions 物件中指定的內建函式(在本例中是 concat() 函式;另請參閱等效的內建定義):

wat
(func $concat (import "wasm:js-string" "concat")
    (param externref externref) (result (ref extern)))

內建函式的特性檢測

使用內建函式時,型別檢查會比不使用時更嚴格——某些規則會強制應用於內建函式的匯入。

因此,要為內建函式編寫特性檢測程式碼,你可以定義一個在啟用該特性時無效,而在不啟用時有效的模組。然後在驗證失敗時返回 true,以表示支援。一個能實現此目的的基本模組如下:

wat
(module
  (function (import "wasm:js-string" "cast")))

在沒有內建函式的情況下,該模組是有效的,因為你可以匯入任何你想要的簽名的函式(在本例中:無引數和無返回值)。在有內建函式的情況下,該模組是無效的,因為現在被特殊處理的 "wasm:js-string" "cast" 函式必須具有特定的簽名(一個 externref 引數和一個不可為空的 (ref extern) 返回值)。

然後你可以嘗試使用 validate() 方法來驗證這個模組,但請注意結果是如何用 ! 運算子取反的——記住,如果模組無效,則表示支援內建函式:

js
const compileOptions = {
  builtins: ["js-string"],
};

fetch("module.wasm")
  .then((response) => response.arrayBuffer())
  .then((bytes) => WebAssembly.validate(bytes, compileOptions))
  .then((result) => console.log(`Builtins available: ${!result}`));

上述模組程式碼非常簡短,所以你可以直接驗證字面位元組,而無需下載模組。一個特性檢測函式可能如下所示:

js
function JsStringBuiltinsSupported() {
  let bytes = new Uint8Array([
    0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 2, 23, 1, 14, 119, 97, 115,
    109, 58, 106, 115, 45, 115, 116, 114, 105, 110, 103, 4, 99, 97, 115, 116, 0,
    0,
  ]);
  return !WebAssembly.validate(bytes, { builtins: ["js-string"] });
}

注意: 在許多情況下,有替代內建函式特性檢測的方法。另一種選擇可以是與內建函式一起提供常規匯入,支援的瀏覽器會忽略這些後備方案。

內建函式示例

讓我們來看一個基礎但完整的例子,以展示如何使用內建函式。這個例子將在一個 Wasm 模組中定義一個函式,該函式將兩個字串連線在一起並將結果列印到控制檯,然後匯出它。接著我們將從 JavaScript 中呼叫這個匯出的函式。

我們將引用的示例在網頁上使用 WebAssembly.instantiate() 函式來處理編譯和例項化;你可以在我們的 webassembly-examples 倉庫中找到這個和其他示例——請參閱 js-builtin-examples

你可以按照以下步驟構建這個例子。此外,你還可以看到它即時執行——開啟你瀏覽器的 JavaScript 控制檯以檢視示例輸出。

JavaScript

示例的 JavaScript 程式碼如下所示。要在本地測試,請使用你選擇的方法將其包含在 HTML 頁面中(例如,放在 <script> 標籤內,或在透過 <script src=""> 引用的外部 .js 檔案中)。

js
const importObject = {
  // Regular import
  m: {
    log: console.log,
  },
};

const compileOptions = {
  builtins: ["js-string"], // Enable JavaScript string builtins
  importedStringConstants: "string_constants", // Enable imported global string constants
};

fetch("log-concat.wasm")
  .then((response) => response.arrayBuffer())
  .then((bytes) => WebAssembly.instantiate(bytes, importObject, compileOptions))
  .then((result) => result.instance.exports.main());

JavaScript 程式碼

  • 定義一個 importObject,它在名稱空間 "m" 下指定一個函式 "log",以便在例項化期間匯入到 Wasm 模組中。它就是 console.log() 函式。
  • 定義一個 compileOptions 物件,其中包含:
  • 使用 fetch() 來獲取 Wasm 模組 (log-concat.wasm),使用 Response.arrayBuffer 將響應轉換為 ArrayBuffer,然後使用 WebAssembly.instantiate() 編譯和例項化 Wasm 模組。
  • 呼叫從 Wasm 模組匯出的 main() 函式。

Wasm 模組

我們的 WebAssembly 模組程式碼的文字表示如下:

wat
(module
  (global $h (import "string_constants" "hello ") externref)
  (global $w (import "string_constants" "world!") externref)
  (func $concat (import "wasm:js-string" "concat")
    (param externref externref) (result (ref extern)))
  (func $log (import "m" "log") (param externref))
  (func (export "main")
    (call $log (call $concat (global.get $h) (global.get $w))))
)

此程式碼:

  • 匯入兩個全域性字串常量 "hello ""world!",使用 JavaScript 中指定的 "string_constants" 名稱空間。它們被命名為 $h$w
  • wasm: 名稱空間匯入 concat 內建函式,將其命名為 $concat 並指定它有兩個引數和一個返回值。
  • "m" 名稱空間匯入已匯入的 "log" 函式,如 JavaScript 的 importObject 物件中所指定,將其命名為 $log 並指定它有一個引數。我們決定在示例中同時包含常規匯入和內建函式,以便向你展示兩種方法的比較。
  • 定義一個將以名稱 "main" 匯出的函式。此函式呼叫 $log,並將一個 $concat 呼叫作為引數傳遞給它。$concat 呼叫則接收 $h$w 全域性字串常量作為引數。

要讓你的本地示例正常工作:

  1. 將上面顯示的 WebAssembly 模組程式碼儲存到一個名為 log-concat.wat 的文字檔案中,與你的 HTML/JavaScript 檔案放在同一目錄中。

  2. 使用 wasm-as 工具將其編譯成 WebAssembly 模組 (log-concat.wasm),該工具是 Binaryen 庫的一部分(請參閱構建說明)。你需要啟用引用型別和垃圾回收(GC)來執行 wasm-as,以便這些示例成功編譯:

    sh
    wasm-as --enable-reference-types -–enable-gc log-concat.wat
    

    或者你可以使用 -all 標誌來代替 --enable-reference-types -–enable-gc

    sh
    wasm-as -all log-concat.wat
    
  3. 使用本地 HTTP 伺服器,在支援的瀏覽器中載入你的示例 HTML 頁面。

結果應該是一個空白網頁,並在 JavaScript 控制檯中記錄了 "hello world!",這是由一個匯出的 Wasm 函式生成的。日誌記錄是使用從 JavaScript 匯入的函式完成的,而兩個原始字串的連線則是由一個內建函式完成的。