自從網路成為不只用於文件,也用於應用程式的平台後,部分最先進的應用程式已將網頁瀏覽器推向極限。許多高階語言都會遇到「更貼近金屬」的做法,也就是透過與較低層級語言互動來提升效能。舉例來說,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 Kummerow、Derek Schuff 和 Rachel Andrew 共同審查。