使用 WebAssembly JavaScript API
如果您已經使用 Emscripten 等工具從其他語言編譯了一個模組,或者自行載入並運行了程式碼,那麼下一步就是學習如何使用 WebAssembly JavaScript API 的其他功能。本文將教您所需瞭解的知識。
注意:如果您不熟悉本文中提到的基本概念,需要更多解釋,請先閱讀WebAssembly 概念,然後再回來閱讀本文。
一些示例
讓我們透過一些示例來解釋如何使用 WebAssembly JavaScript API,以及如何在網頁中載入 Wasm 模組。
注意:您可以在我們的 webassembly-examples GitHub 倉庫中找到示例程式碼。
準備示例
-
首先我們需要一個 Wasm 模組!獲取我們的
simple.wasm檔案並將其副本儲存在本地機器上的一個新目錄中。 -
接下來,在與 Wasm 檔案相同的目錄中建立一個名為
index.html的簡單 HTML 檔案(如果沒有方便可用的,可以使用我們的 簡單模板)。 -
現在,為了幫助我們理解這裡發生了什麼,讓我們看一下 Wasm 模組的文字表示(我們在將 WebAssembly 格式轉換為 Wasm 中也曾遇到過)
wat(module (func $i (import "my_namespace" "imported_func") (param i32)) (func (export "exported_func") i32.const 42 call $i)) -
在第二行,您會看到匯入具有兩級名稱空間——內部函式
$i是從my_namespace.imported_func匯入的。在編寫要匯入到 Wasm 模組中的物件時,我們需要在 JavaScript 中反映這種兩級名稱空間。在您的 HTML 檔案中建立一個<script></script>元素,並向其中新增以下程式碼jsconst importObject = { my_namespace: { imported_func: (arg) => console.log(arg) }, };
流式傳輸 WebAssembly 模組
Firefox 58 中新增了直接從底層源編譯和例項化 WebAssembly 模組的功能。這是透過 WebAssembly.compileStreaming() 和 WebAssembly.instantiateStreaming() 方法實現的。這些方法比其非流式對應方法更容易使用,因為它們可以直接將位元組碼轉換為 Module/Instance 例項,無需單獨將 Response 放入 ArrayBuffer 中。
此示例(請參閱我們 GitHub 上的 instantiate-streaming.html 演示,並即時檢視)展示瞭如何使用 instantiateStreaming() 獲取 Wasm 模組,將 JavaScript 函式匯入其中,編譯並例項化它,以及訪問其匯出的函式——所有這些都在一步完成。
將以下內容新增到您的指令碼中,在第一個程式碼塊下方
WebAssembly.instantiateStreaming(fetch("simple.wasm"), importObject).then(
(obj) => obj.instance.exports.exported_func(),
);
最終結果是,我們呼叫了匯出的 WebAssembly 函式 exported_func,該函式又呼叫了匯入的 JavaScript 函式 imported_func,該函式將 WebAssembly 例項中提供的值 (42) 記錄到控制檯。如果您現在儲存示例程式碼並在支援 WebAssembly 的瀏覽器中載入它,您將看到它的實際效果!
注意:這是一個複雜的、冗長的示例,幾乎沒有實現任何功能,但它確實說明了可能性——在您的 Web 應用程式中將 WebAssembly 程式碼與 JavaScript 一起使用。正如我們所說的,WebAssembly 旨在不替代 JavaScript;兩者可以協同工作,相互借鑑優勢。
不使用流式傳輸載入我們的 Wasm 模組
如果您不能或不想使用上面描述的流式方法,則可以使用非流式方法 WebAssembly.compile() / WebAssembly.instantiate()。
這些方法不直接訪問位元組碼,因此在編譯/例項化 Wasm 模組之前,需要額外一步將響應轉換為 ArrayBuffer。
等效的程式碼如下所示
fetch("simple.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject))
.then((results) => {
results.instance.exports.exported_func();
});
在開發者工具中檢視 Wasm
在 Firefox 54+ 中,開發者工具偵錯程式面板具有顯示網頁中包含的任何 Wasm 程式碼的文字表示的功能。要檢視它,您可以轉到偵錯程式面板並單擊“wasm://”條目。

除了以文字形式檢視 WebAssembly,開發人員還可以使用文字格式除錯 WebAssembly(設定斷點、檢查呼叫堆疊、單步執行等)。
記憶體
在 WebAssembly 的低階記憶體模型中,記憶體表示為一段連續的無型別位元組範圍,稱為線性記憶體,模組內部的載入和儲存指令會對其進行讀寫。在這個記憶體模型中,任何載入或儲存都可以訪問整個線性記憶體中的任何位元組,這對於忠實地表示 C/C++ 概念(如指標)是必需的。
然而,與原生 C/C++ 程式不同,原生 C/C++ 程式的可用記憶體範圍跨越整個程序,WebAssembly 例項可訪問的記憶體被限制在一個特定的(可能非常小)範圍,該範圍由 WebAssembly Memory 物件包含。這允許單個 Web 應用程式使用多個獨立的庫(每個庫在內部使用 WebAssembly),以擁有彼此完全隔離的獨立記憶體。此外,更新的實現還可以建立共享記憶體,可以透過 postMessage() 在 Window 和 Worker 上下文之間傳輸,並在多個地方使用。
在 JavaScript 中,一個 Memory 例項可以被看作是一個可調整大小的 ArrayBuffer(或者在共享記憶體的情況下,是一個 SharedArrayBuffer),並且與 ArrayBuffer 一樣,單個 Web 應用程式可以建立許多獨立的 Memory 物件。您可以使用 WebAssembly.Memory() 建構函式建立一個 Memory 例項,該建構函式接受初始大小和(可選地)最大大小以及一個 shared 屬性作為引數,該屬性指示它是否是共享記憶體。
讓我們從一個快速示例開始探索。
-
建立另一個新的簡單 HTML 頁面(複製我們的簡單模板),並將其命名為
memory.html。向頁面新增一個<script></script>元素。 -
現在將以下行新增到指令碼頂部,以建立記憶體例項
jsconst memory = new WebAssembly.Memory({ initial: 10, maximum: 100 });initial和maximum的單位是 WebAssembly 頁面——它們的大小固定為 64KB。這意味著上述記憶體例項的初始大小為 640KB,最大大小為 6.4MB。WebAssembly 記憶體透過提供一個返回 ArrayBuffer 的緩衝區 getter/setter 來公開其位元組。例如,要將 42 直接寫入線性記憶體的第一個字,您可以這樣做
jsconst data = new DataView(memory.buffer); data.setUint32(0, 42, true);請注意使用
true,這強制使用小端序讀寫,因為 WebAssembly 記憶體始終是小端序。然後可以使用以下方法返回相同的值jsdata.getUint32(0, true); -
現在在您的演示中嘗試一下——儲存您目前新增的內容,在瀏覽器中載入它,然後嘗試在 JavaScript 控制檯中輸入上面兩行。
增長記憶體
記憶體例項可以透過呼叫 Memory.prototype.grow() 來增長,其中引數再次以 WebAssembly 頁面的單位指定。
memory.grow(1);
如果在建立記憶體例項時提供了最大值,則嘗試超出此最大值進行增長將丟擲 RangeError 異常。引擎會利用這個提供的上限預留記憶體,這可以使調整大小更高效。
注意:由於 ArrayBuffer 的 byteLength 是不可變的,所以在成功執行 Memory.prototype.grow() 操作後,緩衝區 getter 將返回一個新的 ArrayBuffer 物件(具有新的 byteLength),並且任何先前的 ArrayBuffer 物件都將“分離”,或與它們之前指向的底層記憶體斷開連線。
就像函式一樣,線性記憶體可以在模組內部定義或匯入。同樣,模組也可以選擇性地匯出其記憶體。這意味著 JavaScript 可以透過建立新的 WebAssembly.Memory 並將其作為匯入傳入,或者透過接收記憶體匯出(透過 Instance.prototype.exports)來訪問 WebAssembly 例項的記憶體。
更復雜的記憶體示例
讓我們透過一個更復雜的記憶體示例來闡明上述斷言——一個 WebAssembly 模組匯入我們之前定義的記憶體例項,用一個整數陣列填充它,然後對它們求和。您可以在memory.wasm找到它。
-
在與之前相同的目錄中製作
memory.wasm的本地副本。注意:您可以在memory.wat檢視模組的文字表示。
-
回到您的
memory.html示例檔案,並像以前一樣獲取、編譯和例項化您的 Wasm 模組——將以下內容新增到指令碼底部jsWebAssembly.instantiateStreaming(fetch("memory.wasm"), { js: { mem: memory }, }).then((results) => { // add code here }); -
由於此模組匯出了其記憶體,給定此模組的一個例項(稱為 instance),我們可以使用匯出的函式
accumulate()直接在模組例項的線性記憶體 (mem) 中建立和填充輸入陣列。將以下內容新增到您的程式碼中,在指定位置jsconst summands = new DataView(memory.buffer); for (let i = 0; i < 10; i++) { summands.setUint32(i * 4, i, true); } const sum = results.instance.exports.accumulate(0, 10); console.log(sum);
請注意我們是如何在 Memory 物件的緩衝區上(Memory.prototype.buffer),而不是在 Memory 本身建立 DataView 檢視的。
記憶體匯入的工作原理與函式匯入類似,只是 Memory 物件作為值傳遞而不是 JavaScript 函式。記憶體匯入有兩個有用的原因
- 它們允許 JavaScript 在模組編譯之前或同時獲取並建立記憶體的初始內容。
- 它們允許單個 Memory 物件被多個模組例項匯入,這是在 WebAssembly 中實現動態連結的關鍵構建塊。
注意:您可以在memory.html找到我們的完整演示(也可線上檢視)。
表格
WebAssembly Table 是一個可調整大小的引用型別陣列,可由 JavaScript 和 WebAssembly 程式碼訪問。雖然 Memory 提供了一個可調整大小的原始位元組型別陣列,但將引用儲存在 Memory 中是不安全的,因為引用是引擎信任的值,出於安全、可移植性和穩定性原因,其位元組不能由內容直接讀取或寫入。
表格具有一個元素型別,它限制了可以儲存在表格中的引用型別。在 WebAssembly 的當前迭代中,WebAssembly 程式碼只需要一種引用型別——函式——因此只有一種有效的元素型別。在未來的迭代中,將新增更多元素型別。
函式引用對於編譯像 C/C++ 這樣具有函式指標的語言是必需的。在 C/C++ 的本地實現中,函式指標由程序虛擬地址空間中函式程式碼的原始地址表示,因此,出於上述安全原因,不能直接儲存線上性記憶體中。相反,函式引用儲存在表中,它們的索引(整數,可以儲存線上性記憶體中)被傳遞。
當需要呼叫函式指標時,WebAssembly 呼叫者提供索引,然後可以安全地對照表進行邊界檢查,然後再索引和呼叫索引的函式引用。因此,表目前是一個相當底層的原語,用於安全且可移植地編譯低階程式語言特性。
表格可以透過 Table.prototype.set() 進行修改,該方法更新表格中的一個值,以及 Table.prototype.grow(),該方法增加可以儲存在表格中的值的數量。這允許間接可呼叫函式集隨時間變化,這對於動態連結技術是必需的。這些修改可以透過 JavaScript 中的 Table.prototype.get() 和 Wasm 模組立即訪問。
表格示例
讓我們看一個簡單的表格示例——一個 WebAssembly 模組,它建立並匯出一個包含兩個元素的表格:元素 0 返回 13,元素 1 返回 42。您可以在 table.wasm 找到它。
-
在新目錄中製作
table.wasm的本地副本。注意:您可以在table.wat檢視模組的文字表示。
-
在同一目錄中建立一個我們HTML 模板的新副本,並將其命名為
table.html。 -
和以前一樣,獲取、編譯和例項化您的 Wasm 模組——將以下內容新增到您的 HTML body 底部的
<script>元素中jsWebAssembly.instantiateStreaming(fetch("table.wasm")).then((results) => { // add code here }); -
現在讓我們訪問表格中的資料——將以下行新增到您的程式碼中,在指定位置
jsconst tbl = results.instance.exports.tbl; console.log(tbl.get(0)()); // 13 console.log(tbl.get(1)()); // 42
此程式碼依次訪問儲存在表格中的每個函式引用,並例項化它們以將它們持有的值列印到控制檯——請注意每個函式引用是如何透過 Table.prototype.get() 呼叫檢索的,然後我們添加了一組額外的括號來實際呼叫該函式。
注意:您可以在table.html找到我們的完整演示(也可線上檢視)。
全域性變數
WebAssembly 能夠建立全域性變數例項,這些例項可以從 JavaScript 訪問,並且可以在一個或多個 WebAssembly.Module 例項之間匯入/匯出。這非常有用,因為它允許動態連結多個模組。
要從 JavaScript 中建立 WebAssembly 全域性例項,您可以使用 WebAssembly.Global() 建構函式,它看起來像這樣
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 0);
您可以看到這需要兩個引數
-
一個包含兩個屬性的物件,描述全域性變數
value:其資料型別,可以是 WebAssembly 模組中接受的任何資料型別——i32、i64、f32或f64。mutable:一個布林值,定義該值是否可變。
-
一個包含變數實際值的值。這可以是任何值,只要其型別與指定的資料型別匹配。
那麼我們如何使用它呢?在下面的示例中,我們將一個全域性變數定義為可變的 i32 型別,值為 0。
然後,全域性變數的值被更改,首先使用 Global.value 屬性更改為 42,然後使用從 global.wasm 模組匯出的 incGlobal() 函式更改為 43(該函式將 1 新增到給定值,然後返回新值)。
const output = document.getElementById("output");
function assertEq(msg, got, expected) {
const result =
got === expected
? `SUCCESS! Got: ${got}\n`
: `FAIL!\nGot: ${got}\nExpected: ${expected}\n`;
output.innerText += `Testing ${msg}: ${result}`;
}
assertEq("WebAssembly.Global exists", typeof WebAssembly.Global, "function");
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 0);
WebAssembly.instantiateStreaming(fetch("global.wasm"), { js: { global } }).then(
({ instance }) => {
assertEq(
"getting initial value from wasm",
instance.exports.getGlobal(),
0,
);
global.value = 42;
assertEq(
"getting JS-updated value from wasm",
instance.exports.getGlobal(),
42,
);
instance.exports.incGlobal();
assertEq("getting wasm-updated value from JS", global.value, 43);
},
);
注意:您可以在 GitHub 上即時執行示例;另請參閱原始碼。
多重性
現在我們已經演示了主要 WebAssembly 構建塊的用法,這是一個提及多重性概念的好地方。這為 WebAssembly 在架構效率方面提供了許多進步
- 一個模組可以有 N 個例項,就像一個函式字面量可以產生 N 個閉包值一樣。
- 一個模組例項可以使用 0-1 個記憶體例項,這些例項提供例項的“地址空間”。WebAssembly 的未來版本可能允許每個模組例項有 0-N 個記憶體例項(參見多重記憶體)。
- 一個模組例項可以使用 0-1 個表格例項——這是例項的“函式地址空間”,用於實現 C 函式指標。WebAssembly 的未來版本可能允許每個模組例項有 0-N 個表格例項。
- 一個記憶體或表格例項可以被 0-N 個模組例項使用——這些例項都共享相同的地址空間,從而允許動態連結。
您可以在我們的《理解文字格式》文章中看到多重性的實際應用——請參閱修改表格和動態連結部分。
總結
本文帶您瞭解了使用 WebAssembly JavaScript API 將 WebAssembly 模組包含在 JavaScript 上下文中並利用其函式的基本知識,以及如何在 JavaScript 中使用 WebAssembly 記憶體和表格。我們還觸及了多重性概念。