WebAssembly 是什麼?來源為何?

自從 Web 平台除了文件和應用程式之外,許多最先進的應用程式也使網路瀏覽器不斷突破極限。為提升系統效能,許多高階語言都面臨一種讓低階語言互動的管道,試圖「更接近金屬」。例如,Java 擁有 Java 原生介面。對 JavaScript 來說,這種較低層級的語言是 WebAssembly。本文將說明如何透過 asm.js 的臨時解決方案建立 WebAssembly,以及瞭解組合語言。

組合語言

你是否曾使用組譯語言編寫程式?在電腦程式設計中,組譯語言通常簡稱為 Assembly,且通常縮寫為 ASM 或 asm,是任何低階程式設計語言,且在語言和架構機器碼操作說明中的對應指示高度相似。

舉例來說,如果查看 Intel® 64 和 IA-32 架構 (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 引擎能預先 (AOT) 針對有效的 asm.js 程式碼採用最佳化編譯策略。使用手動記憶體管理 (例如 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 有兩種表示形式:textualbinary。上方顯示的是文字表示法。

文字表示法

文字表示法是以 S 運算式為基礎,通常會使用副檔名 .wat (適用於 WebAsembly 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"

您也可以在不使用 HTML 包裝函式的情況下,編譯至 WebAssembly:

$ 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 時,與 WebAssembly 合作的感覺有點像如何畫出貓頭鷹,特別是因為熟悉 HTML、CSS 和 JavaScript 的網頁開發人員,不見得熟悉 C 等語言自行編譯而成。幸好有像 StackOverflow 的 webassembly 標記這樣的管道,專家通常很樂意為您提供協助。

特別銘謝

本文評論者為 Jakob KummerowDerek SchuffRachel Andrew