WebAssembly 是什麼?來源為何?

自從網路成為平台,不僅能處理文件,還能執行應用程式,一些最先進的應用程式就開始挑戰瀏覽器的極限。許多高階語言都會採用「更接近硬體」的方法,也就是與低階語言介面互動,藉此提升效能。舉例來說,Java 有 Java Native Interface。對 JavaScript 而言,這個低階語言是 WebAssembly。本文將說明什麼是組合語言,以及組合語言在網路上有何用處,然後瞭解如何透過 asm.js 的暫時解決方案建立 WebAssembly。

組合語言

您是否曾使用組合語言進行程式設計?在電腦程式設計中,組合語言通常簡稱為「組合」,縮寫為 ASM 或 asm,是任何低階程式設計語言,語言中的指令與架構的機器碼指令之間有很強的對應關係。

舉例來說,在「Intel® 64 and IA-32 Architectures」(PDF) 中,MUL 指令 (用於法) 會對第一個運算元 (目的地運算元) 和第二個運算元 (來源運算元) 執行不帶正負號的乘法,並將結果儲存在目的地運算元中。簡單來說,目的地運算元是位於暫存器 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 指令和幾乎相同的引數,將其編譯為 WebAssembly:

$ emcc hello.c -o hello.html

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

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

$ emcc hello.c -o hello.js

和先前一樣,這會建立 hello.wasm 檔案,但這次是 hello.js 檔案,而不是 HTML 包裝函式。如要測試,請執行產生的 JavaScript 檔案 hello.js,例如使用 Node.js:

$ node hello.js
Hello World

瞭解詳情

這份 WebAssembly 簡介只是冰山一角。 如要進一步瞭解 WebAssembly,請參閱 MDN 上的 WebAssembly 說明文件,並參閱 Emscripten 說明文件。說實話,使用 WebAssembly 可能會有點像如何畫貓頭鷹的迷因,特別是熟悉 HTML、CSS 和 JavaScript 的網頁開發人員,不一定精通 C 等要編譯的語言。幸好有 StackOverflow 的 webassembly 標記等管道,只要禮貌發問,專家通常都很樂意提供協助。

特別銘謝

本文由 Jakob KummerowDerek SchuffRachel Andrew 審查。