WebAssembly 是什麼?來源為何?

自從網路成為不只用於文件,也用於應用程式的平台後,部分最先進的應用程式已將網頁瀏覽器推向極限。許多高階語言都會遇到「更貼近金屬」的做法,也就是透過與較低層級語言互動來提升效能。舉例來說,Java 有 Java Native Interface。就 JavaScript 而言,這個低階語言就是 WebAssembly。本文將說明組合語言的概念,以及為何在網路上使用組合語言,接著說明如何透過 asm.js 的暫時性解決方案建立 WebAssembly。

組合語言

您是否曾使用組合語言編寫程式?在電腦程式設計中,組合語言 (通常簡稱為組合語,通常縮寫為 ASM 或 asm) 是任何低階程式設計語言,其語言指令與架構的機器碼指令之間有非常密切的對應關係。

舉例來說,請參閱 Intel® 64 和 IA-32 架構 (PDF),MUL 指令 (用於multiplication) 會執行第一個運算元 (目的地運算元) 和第二個運算元 (來源運算元) 的無符號相乘運算,並將結果儲存在目的地運算元中。簡單來說,目的運算元是位於寄存器 AX 中的隱含運算元,來源運算元則位於 CX 等通用寄存器中。結果會再次儲存在註冊 AX 中。請參考以下 x86 程式碼範例:

mov ax, 5  ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx     ; Multiply the value of register AX (5)
           ; and the value of register CX (10), and
           ; store the result in register AX.

舉例來說,如果要將 5 和 10 相乘,您可能會在 JavaScript 中編寫類似下列的程式碼:

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

採用彙整路徑的優點是,這種低階且經過機器最佳化的程式碼,比高階且經過人類最佳化的程式碼更有效率。在上述情況中,這並無差別,但您可以想像,對於更複雜的作業,差異可能會相當大。

顧名思義,x86 程式碼會依賴 x86 架構。如果有一種方法可以編寫不依賴特定架構的彙整程式碼,但會繼承彙整的效能優勢,那麼該怎麼辦?

asm.js

如要編寫不含架構相依性的組合語程式碼,第一步就是使用 asm.js,這是 JavaScript 的嚴格子集,可用於編譯器的低階、高效的目標語言。這個子語言有效地描述了沙箱虛擬機器,可用於 C 或 C++ 等記憶體不安全的語言。結合靜態和動態驗證,可讓 JavaScript 引擎針對有效的 asm.js 程式碼採用提前最佳化編譯策略 (AOT)。使用靜態型別語言 (例如 C) 手動管理記憶體的程式碼,會由源到源編譯器 (例如 早期 Emscripten,以 LLVM 為基礎) 進行轉譯。

我們將語言功能限制為可使用 AOT 的功能,藉此提升效能。Firefox 22 是第一個支援 asm.js 的瀏覽器,並以 OdinMonkey 的名稱發布。Chrome 在 61 版中新增了 asm.js 支援。雖然 asm.js 仍可在瀏覽器中運作,但已被 WebAssembly 取代。目前使用 asm.js 的原因,是因為它是針對不支援 WebAssembly 的瀏覽器提供的替代方案。

WebAssembly

WebAssembly 是一種低階類似彙整的語言,採用精簡的二進位格式,可提供接近原生效能的執行效能,並提供 C/C++、Rust 等語言,以及許多其他語言,並提供編譯目標,以便在網路上執行。我們正在努力支援記憶體管理語言,例如 Java 和 Dart,應該很快就會推出,或是已經推出,例如 Kotlin/Wasm。WebAssembly 可與 JavaScript 搭配運作。

除了瀏覽器之外,WebAssembly 程式也能在其他執行階段執行,這要歸功於 WASI (WebAssembly 系統介面),也就是 WebAssembly 的模組化系統介面。WASI 的設計目的是為了讓程式碼可在不同作業系統間移植,並確保安全性和在沙箱環境中執行的功能。

WebAssembly 程式碼 (二進位程式碼,也就是位元碼) 旨在於可攜式虛擬堆疊機器 (VM) 上執行。位元碼的設計目的是讓剖析和執行速度比 JavaScript 更快,並提供精簡的程式碼表示法。

概念上,指令的執行程序會透過傳統的程式計數器,依序執行指令。實際上,大多數 Wasm 引擎會將 Wasm 位元碼編譯為機器碼,然後執行該程式碼。指令分為以下兩類:

  • 控制指示語會形成控制結構體,並將引數值彈出堆疊,可能會變更程式計數器,並將結果值推送至堆疊。
  • 簡單指令:從堆疊中彈出引數值、對值套用運算子,然後將結果值推送至堆疊,接著隱含地推進程式計數器。

回到先前的範例,下列 WebAssembly 程式碼等同於文章開頭的 x86 程式碼:

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

雖然 asm.js 是完全在軟體中實作,也就是其程式碼可在任何 JavaScript 引擎中執行 (即使未經最佳化),但 WebAssembly 需要所有瀏覽器供應商同意的新功能。WebAssembly 於 2015 年宣布,並於 2017 年 3 月首次發布,並於 2019 年 12 月 5 日成為 W3C 建議。W3C 會維護這個標準,並接受所有主要瀏覽器供應商和其他相關人士的貢獻。自 2017 年起,瀏覽器支援功能已全面開放。

WebAssembly 有兩種表示法:文字二進位。上方顯示的是文字表示法。

文字表示

文字表示法以 S 運算式為基礎,通常會使用 .wat 副檔名 (適用於 WebAssembly text 格式)。如果你真的想這樣做,可以手寫。以上述乘法範例為例,如果不再將因數硬式編碼,就能讓程式碼更實用,您可能會理解以下程式碼:

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

二進位表示法

使用 .wasm 副檔名的二進位格式不適合人類使用,更不用說人類創作。您可以使用 wat2wasm 等工具,將上述程式碼轉換為以下二進位表示法。(註解通常不是二進位表示法的一部分,而是由 wat2wasm 工具新增,以利於提升可讀性)。

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

編譯為 WebAssembly

如您所見,.wat.wasm 都不是很友善。這時,Emscripten 等編譯器就能派上用場。您可以使用這項工具,從 C 和 C++ 等較高層級語言進行編譯。此外,還有許多其他編譯器可用於 Rust 等其他語言。請考慮以下 C 程式碼:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

通常,您會使用編譯器 gcc 編譯此 C 程式。

$ gcc hello.c -o hello

安裝 Emscripten 後,您可以使用 emcc 指令和幾乎相同的引數,將 Emscripten 編譯為 WebAssembly:

$ emcc hello.c -o hello.html

這會建立 hello.wasm 檔案和 HTML 包裝函式檔案 hello.html。從網路伺服器提供檔案 hello.html 時,您會看到 "Hello World" 印到開發人員工具控制台。

您也可以不使用 HTML 包裝函式,直接編譯為 WebAssembly:

$ emcc hello.c -o hello.js

這會建立 hello.wasm 檔案,但這次是 hello.js 檔案,而非 HTML 包裝函式。如要進行測試,請使用 Node.js 之類的工具執行產生的 JavaScript 檔案 hello.js

$ node hello.js
Hello World

瞭解詳情

這篇文章簡要介紹 WebAssembly,但這只是冰山一角。如要進一步瞭解 WebAssembly,請參閱 MDN 的 WebAssembly 說明文件,並查閱 Emscripten 說明文件。老實說,使用 WebAssembly 時,你可能會覺得有點像是畫貓頭鷹迷因的過程,尤其是網頁開發人員熟悉 HTML、CSS 和 JavaScript,但不一定精通 C 等待編譯的語言。幸好,您可以透過 StackOverflow 的 webassembly 標記等管道,向專家提出問題,他們通常都很樂意提供協助。

特別銘謝

本文由 Jakob KummerowDerek SchuffRachel Andrew 共同審查。