WebAssembly JavaScript 內建函式
WebAssembly JavaScript 內建函式是 JavaScript 操作的 Wasm 等價物,它提供了一種在 Wasm 模組內部使用 JavaScript 功能的方法,而無需匯入 JavaScript 膠水程式碼來提供 JavaScript 和 WebAssembly 值以及呼叫約定之間的橋樑。
本文解釋了內建函式的工作原理和可用型別,然後提供了一個使用示例。
匯入 JavaScript 函式的問題
對於許多 JavaScript 功能,常規匯入工作正常。然而,為 String、ArrayBuffer 和 Map 等基本型別匯入膠水程式碼會帶來顯著的效能開銷。在這種情況下,WebAssembly 和大多數以其為目標的語言都期望一個緊密的內聯操作序列,而不是像常規匯入函式那樣工作的間接函式呼叫。
具體來說,將函式從 JavaScript 匯入到 WebAssembly 模組會因以下原因產生效能問題:
- 現有 API 需要進行轉換以處理
this值的差異,WebAssembly 函式import呼叫會將其保留為undefined。 - 某些基本型別使用 JavaScript 運算子,如
===和<,這些運算子無法匯入。 - 大多數 JavaScript 函式對其接受的值的型別極其寬容,而我們希望儘可能利用 WebAssembly 的型別系統來移除這些檢查和強制轉換。
考慮到這些問題,建立內建定義來將現有的 JavaScript 功能(例如 String 基本型別)適配到 WebAssembly,比匯入它並依賴間接函式呼叫更簡單且效能更好。
可用的 WebAssembly JavaScript 內建函式
以下各節詳細介紹了可用的內建函式。未來可能會支援其他內建函式。
字串操作
可用的 String 內建函式有:
"wasm:js-string" "cast"-
如果提供的值不是字串,則丟擲錯誤。大致相當於:
jsif (typeof obj !== "string") throw new WebAssembly.RuntimeError(); "wasm:js-string" "compare"-
比較兩個字串值並確定它們的順序。如果第一個字串小於第二個字串,返回
-1;如果第一個字串大於第二個字串,返回1;如果字串嚴格相等,返回0。 "wasm:js-string" "concat""wasm:js-string" "charCodeAt""wasm:js-string" "codePointAt""wasm:js-string" "equals"-
比較兩個字串值是否嚴格相等,如果相等則返回
1,否則返回0。注意:
"equals"函式是唯一一個對null輸入不丟擲異常的字串內建函式,因此 Wasm 模組在呼叫它之前不需要檢查null值。所有其他函式都沒有合理的方式處理null輸入,因此會對它們丟擲異常。 "wasm:js-string" "fromCharCode""wasm:js-string" "fromCharCodeArray"-
從一個 Wasm 的
i16值陣列建立一個字串。 "wasm:js-string" "fromCodePoint""wasm:js-string" "intoCharCodeArray"-
將一個字串的字元編碼寫入一個 Wasm 的
i16值陣列。 "wasm:js-string" "length""wasm:js-string" "substring""wasm:js-string" "test"-
如果提供的值不是字串,返回
0;如果是字串,返回1。大致相當於:jstypeof obj === "string";
如何使用內建函式?
內建函式的工作方式與從 JavaScript 匯入的函式類似,不同之處在於你使用的是在保留名稱空間(wasm:)中定義的、用於執行 JavaScript 操作的標準 Wasm 函式等價物。因此,瀏覽器可以為它們預測並生成最最佳化的程式碼。本節總結了如何使用它們。
JavaScript API
內建函式在編譯時透過在呼叫編譯和/或例項化模組的方法時指定 compileOptions.builtins 屬性作為引數來啟用。它的值是一個字串陣列,用於標識你想要啟用的內建函式集:
WebAssembly.compile(bytes, { builtins: ["js-string"] });
compileOptions 物件可用於以下函式:
WebAssembly 模組特性
在你的 WebAssembly 模組中,你現在可以從 wasm: 名稱空間匯入在 compileOptions 物件中指定的內建函式(在本例中是 concat() 函式;另請參閱等效的內建定義):
(func $concat (import "wasm:js-string" "concat")
(param externref externref) (result (ref extern)))
內建函式的特性檢測
使用內建函式時,型別檢查會比不使用時更嚴格——某些規則會強制應用於內建函式的匯入。
因此,要為內建函式編寫特性檢測程式碼,你可以定義一個在啟用該特性時無效,而在不啟用時有效的模組。然後在驗證失敗時返回 true,以表示支援。一個能實現此目的的基本模組如下:
(module
(function (import "wasm:js-string" "cast")))
在沒有內建函式的情況下,該模組是有效的,因為你可以匯入任何你想要的簽名的函式(在本例中:無引數和無返回值)。在有內建函式的情況下,該模組是無效的,因為現在被特殊處理的 "wasm:js-string" "cast" 函式必須具有特定的簽名(一個 externref 引數和一個不可為空的 (ref extern) 返回值)。
然後你可以嘗試使用 validate() 方法來驗證這個模組,但請注意結果是如何用 ! 運算子取反的——記住,如果模組無效,則表示支援內建函式:
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}`));
上述模組程式碼非常簡短,所以你可以直接驗證字面位元組,而無需下載模組。一個特性檢測函式可能如下所示:
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 檔案中)。
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物件,其中包含:builtins屬性,用於啟用字串內建函式。importedStringConstants屬性,用於啟用匯入的全域性字串常量。
- 使用
fetch()來獲取 Wasm 模組 (log-concat.wasm),使用Response.arrayBuffer將響應轉換為ArrayBuffer,然後使用WebAssembly.instantiate()編譯和例項化 Wasm 模組。 - 呼叫從 Wasm 模組匯出的
main()函式。
Wasm 模組
我們的 WebAssembly 模組程式碼的文字表示如下:
(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全域性字串常量作為引數。
要讓你的本地示例正常工作:
-
將上面顯示的 WebAssembly 模組程式碼儲存到一個名為
log-concat.wat的文字檔案中,與你的 HTML/JavaScript 檔案放在同一目錄中。 -
使用
wasm-as工具將其編譯成 WebAssembly 模組 (log-concat.wasm),該工具是 Binaryen 庫的一部分(請參閱構建說明)。你需要啟用引用型別和垃圾回收(GC)來執行wasm-as,以便這些示例成功編譯:shwasm-as --enable-reference-types -–enable-gc log-concat.wat或者你可以使用
-all標誌來代替--enable-reference-types -–enable-gc:shwasm-as -all log-concat.wat -
使用本地 HTTP 伺服器,在支援的瀏覽器中載入你的示例 HTML 頁面。
結果應該是一個空白網頁,並在 JavaScript 控制檯中記錄了 "hello world!",這是由一個匯出的 Wasm 函式生成的。日誌記錄是使用從 JavaScript 匯入的函式完成的,而兩個原始字串的連線則是由一個內建函式完成的。