使用 WebAssembly JavaScript API

如果您已經使用 Emscripten 等工具從其他語言編譯了一個模組,或自己載入並運行了程式碼,下一步就是學習更多關於使用 WebAssembly JavaScript API 的其他功能。本文將教你你需要了解的內容。

注意:如果您不熟悉本文中提到的基本概念,需要更多解釋,請先閱讀WebAssembly 概念,然後再回來。

一些簡單的示例

讓我們瀏覽一些示例,這些示例說明了如何使用 WebAssembly JavaScript API,以及如何使用它在網頁中載入 Wasm 模組。

注意:您可以在我們的webassembly-examples GitHub 儲存庫中找到示例程式碼。

準備示例

  1. 首先我們需要一個 Wasm 模組!獲取我們的simple.wasm 檔案並在本地計算機上的新目錄中儲存副本。
  2. 接下來,讓我們在與 Wasm 檔案相同的目錄中建立一個名為 index.html 的簡單 HTML 檔案(如果您沒有很容易獲得的 HTML 檔案,可以使用我們的簡單模板)。
  3. 現在,為了幫助我們理解這裡發生了什麼,讓我們看看 Wasm 模組的文字表示(我們也在將 WebAssembly 格式轉換為 Wasm中遇到過)。
    wasm
    (module
      (func $i (import "my_namespace" "imported_func") (param i32))
      (func (export "exported_func")
        i32.const 42
        call $i))
    
  4. 在第二行,您將看到匯入具有兩級名稱空間——內部函式 $imy_namespace.imported_func 匯入。在編寫要匯入 Wasm 模組的物件時,我們需要在 JavaScript 中反映此兩級名稱空間。在您的 HTML 檔案中建立一個 <script></script> 元素,並在其中新增以下程式碼
    js
    const 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 函式匯入其中,編譯並例項化它,以及訪問其匯出的函式——所有這些都在一步完成。

將以下內容新增到您的指令碼中,位於第一個程式碼塊下方

js
WebAssembly.instantiateStreaming(fetch("simple.wasm"), importObject).then(
  (obj) => obj.instance.exports.exported_func(),
);

最終結果是我們呼叫匯出的 WebAssembly 函式 exported_func,該函式依次呼叫匯入的 JavaScript 函式 imported_func,該函式將 WebAssembly 例項(42)中提供的 value 值記錄到控制檯。如果您現在儲存示例程式碼並在支援 WebAssembly 的瀏覽器中載入它,您將看到它的執行效果!

注意:這是一個複雜且冗長的示例,它實現的功能很少,但它確實說明了可能性——在 Web 應用程式中將 WebAssembly 程式碼與 JavaScript 一起使用。正如我們在其他地方所說的,WebAssembly 的目標不是取代 JavaScript;兩者可以協同工作,利用彼此的優勢。

在不進行流式傳輸的情況下載入我們的 Wasm 模組

如果您無法或不想使用上面描述的流式傳輸方法,則可以使用非流式傳輸方法WebAssembly.compile() / WebAssembly.instantiate()

這些方法不會直接訪問位元組碼,因此需要額外的步驟才能將響應轉換為ArrayBuffer,然後才能編譯/例項化 Wasm 模組。

等效程式碼如下所示

js
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://”條目。

Developer tools debugger panel highlighting a module.

除了將 WebAssembly 視為文字之外,開發人員還可以使用文字格式除錯(放置斷點、檢查呼叫棧、單步執行等)WebAssembly。

記憶體

在 WebAssembly 的低階記憶體模型中,記憶體表示為稱為線性記憶體的連續無型別位元組範圍,由模組內部的載入和儲存指令讀取和寫入。在此記憶體模型中,任何載入或儲存都可以訪問整個線性記憶體中的任何位元組,這對於忠實地表示 C/C++ 概念(如指標)是必要的。

但是,與本機 C/C++ 程式不同,在 C/C++ 程式中,可用記憶體範圍跨越整個程序,特定 WebAssembly 例項可訪問的記憶體僅限於 WebAssembly 記憶體物件包含的一個特定(可能非常小)範圍。這允許單個 Web 應用程式使用多個獨立的庫(每個庫在內部使用 WebAssembly)來擁有彼此完全隔離的單獨記憶體。此外,較新的實現還可以建立共享記憶體,這些記憶體可以使用postMessage()在 Window 和 Worker 上下文之間傳輸,並在多個位置使用。

在 JavaScript 中,記憶體例項可以被認為是可調整大小的ArrayBuffer(或SharedArrayBuffer,在共享記憶體的情況下),並且與 ArrayBuffers 一樣,單個 Web 應用程式可以建立許多獨立的記憶體物件。您可以使用WebAssembly.Memory() 建構函式建立一個,該建構函式以初始大小和(可選)最大大小以及表示其是否為共享記憶體的 shared 屬性作為引數。

讓我們透過檢視一個快速示例開始探索這一點。

  1. 建立另一個新的簡單 HTML 頁面(複製我們的簡單模板)並將其命名為 memory.html。向頁面新增一個 <script></script> 元素。
  2. 現在將以下行新增到指令碼的頂部,以建立記憶體例項
    js
    const memory = new WebAssembly.Memory({ initial: 10, maximum: 100 });
    
    initialmaximum 的單位是 WebAssembly 頁面——這些頁面的大小固定為 64KB。這意味著上述記憶體例項的初始大小為 640KB,最大大小為 6.4MB。WebAssembly 記憶體透過提供返回 ArrayBuffer 的緩衝區 getter/setter 來公開其位元組。例如,要將 42 直接寫入線性記憶體的第一個字,您可以這樣做
    js
    const data = new DataView(memory.buffer);
    data.setUint32(0, 42, true);
    
    請注意 true 的使用,它強制執行小端讀取和寫入,因為 WebAssembly 記憶體始終是小端。然後,您可以使用以下方法返回相同的值
    js
    data.getUint32(0, true);
    
  3. 現在在您的演示中嘗試一下——儲存您新增的內容,在瀏覽器中載入它,然後嘗試在 JavaScript 控制檯中輸入上述兩行。

擴充套件記憶體

可以透過呼叫Memory.prototype.grow() 來擴充套件記憶體例項,其中引數再次以 WebAssembly 頁面的單位指定

js
memory.grow(1);

如果在建立記憶體例項時提供了最大值,則嘗試超過此最大值的擴充套件將丟擲RangeError 異常。引擎利用此提供的上限提前預留記憶體,這可以使調整大小更高效。

注意:由於ArrayBuffer 的 byteLength 是不可變的,因此在成功執行Memory.prototype.grow() 操作後,緩衝區 getter 將返回一個新的 ArrayBuffer 物件(具有新的 byteLength),並且任何以前的 ArrayBuffer 物件都將“分離”或與它們先前指向的底層記憶體斷開連線。

就像函式一樣,線性記憶體可以在模組內部定義或匯入。類似地,模組還可以選擇匯出其記憶體。這意味著 JavaScript 可以透過建立新的 WebAssembly.Memory 並將其作為匯入傳遞,或者透過接收記憶體匯出(透過Instance.prototype.exports)來訪問 WebAssembly 例項的記憶體。

更復雜的記憶體示例

讓我們透過檢視一個更復雜的記憶體示例來使上述斷言更清楚——一個匯入我們之前定義的記憶體例項、用整數陣列填充它,然後對它們求和的 WebAssembly 模組。您可以在memory.wasm 中找到它。

  1. 在與之前相同的目錄中建立 memory.wasm 的本地副本。

    注意: 您可以在 memory.wat 中檢視模組的文字表示形式。

  2. 返回您的 memory.html 示例檔案,並像以前一樣獲取、編譯和例項化您的 Wasm 模組 - 將以下內容新增到指令碼底部
    js
    WebAssembly.instantiateStreaming(fetch("memory.wasm"), {
      js: { mem: memory },
    }).then((results) => {
      // add code here
    });
    
  3. 由於此模組匯出了其記憶體,因此,給定此模組的例項(稱為 instance),我們可以使用匯出的函式 accumulate() 直接在模組例項的線性記憶體 (mem) 中建立和填充輸入陣列。將以下內容新增到您的程式碼中,在指示的位置
    js
    const 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) 上建立 DataView 檢視,而不是在 Memory 本身上。

記憶體匯入的工作方式與函式匯入相同,只是 Memory 物件作為值傳遞而不是 JavaScript 函式。記憶體匯入有兩個用途

  • 它們允許 JavaScript 在模組編譯之前或與模組編譯同時獲取和建立記憶體的初始內容。
  • 它們允許單個 Memory 物件被多個模組例項匯入,這是在 WebAssembly 中實現動態連結的關鍵構建塊。

注意: 您可以在 memory.html 中找到我們的完整演示 (也可以線上檢視)。

WebAssembly 表格是一個可調整大小的、型別化的引用陣列,JavaScript 和 WebAssembly 程式碼都可以訪問。雖然 Memory 提供了一個可調整大小的、型別化的原始位元組陣列,但將引用儲存在 Memory 中是不安全的,因為引用是引擎信任的值,出於安全、可移植性和穩定性原因,其位元組不得由內容直接讀取或寫入。

表格具有元素型別,該型別限制了可以儲存在表格中的引用的型別。在 WebAssembly 的當前迭代中,WebAssembly 程式碼只需要一種型別的引用 - 函式 - 因此只有一種有效的元素型別。在未來的迭代中,將新增更多元素型別。

函式引用對於編譯像 C/C++ 這樣的具有函式指標的語言是必要的。在 C/C++ 的原生實現中,函式指標由函式程式碼在程序虛擬地址空間中的原始地址表示,因此,出於上述安全原因,不能直接儲存線上性記憶體中。相反,函式引用儲存在表格中,它們的索引(整數,可以儲存線上性記憶體中)則被傳遞。

當需要呼叫函式指標時,WebAssembly 呼叫方提供索引,然後可以在索引和呼叫索引的函式引用之前,針對表格進行安全邊界檢查。因此,表格目前是一個相當底層的原語,用於安全且可移植地編譯低階程式語言特性。

表格可以透過 Table.prototype.set() 進行修改,該方法更新表格中的一個值,以及 Table.prototype.grow(),該方法增加表格中可以儲存的值的數量。這允許間接可呼叫的函式集隨時間變化,這對於 動態連結技術 來說是必要的。這些修改可以透過 JavaScript 和 Wasm 模組中的 Table.prototype.get() 立即訪問。

表格示例

讓我們來看一個簡單的表格示例 - 一個建立並匯出包含兩個元素的表格的 WebAssembly 模組:元素 0 返回 13,元素 1 返回 42。您可以在 table.wasm 中找到它。

  1. 在新的目錄中建立 table.wasm 的本地副本。

    注意: 您可以在 table.wat 中檢視模組的文字表示形式。

  2. 在同一目錄中建立我們 HTML 模板 的新副本,並將其命名為 table.html
  3. 與之前一樣,獲取、編譯並例項化您的 Wasm 模組 - 將以下內容新增到 HTML 主體底部的 <script> 元素中
    js
    WebAssembly.instantiateStreaming(fetch("table.wasm")).then((results) => {
      // add code here
    });
    
  4. 現在讓我們訪問表格中的資料 - 將以下行新增到程式碼中,在指示的位置
    js
    const 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() 建構函式,其外觀如下

js
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 0);

您可以看到它接受兩個引數

  • 一個包含兩個屬性的物件,用於描述全域性變數
    • value:其資料型別,可以是 WebAssembly 模組中接受的任何資料型別 - i32i64f32f64
    • mutable:一個布林值,用於定義該值是否可變。
  • 一個包含變數實際值的 value。只要其型別與指定的資料型別匹配,它就可以是任何值。

那麼我們如何使用它呢?在以下示例中,我們將全域性變數定義為可變的 i32 型別,其值為 0。

然後更改全域性變數的值,首先使用 Global.value 屬性將其更改為 42,然後使用 global.wasm 模組匯出的 incGlobal() 函式將其更改為 43(這會將給定值加 1,然後返回新值)。

js
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 在 JavaScript 上下文中包含 WebAssembly 模組並使用其函式的基本知識,以及如何在 JavaScript 中使用 WebAssembly 記憶體和表格。我們還簡要介紹了多重性的概念。

另請參閱