自從網路成為不只用於文件,也用於應用程式的平台後,部分最先進的應用程式已將網路瀏覽器推向極限。許多高階語言都會採用「更貼近金屬」的做法,透過與低階語言互動來提升效能。舉例來說,Java 有 Java Native Interface。就 JavaScript 而言,這個低階語言就是 WebAssembly。本文將說明組合語言的概念,以及為何在網路上使用組合語言,接著說明如何透過 asm.js 的暫時性解決方案建立 WebAssembly。
組合語言
您是否曾使用組合語言編寫程式?在電腦程式設計中,組合語言 (通常簡稱為組合語,通常縮寫為 ASM 或 asm) 是任何低階程式設計語言,其語言指令與架構的機器碼指令之間有非常強的對應關係。
舉例來說,如果查看 Intel® 64 和 IA-32 架構 (PDF),MUL
指令 (適用於「等量」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 有兩種表示法:文字和二進位。上方顯示的是文字表示法。
文字表示
文字表示法是以 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 包裝函式。如要進行測試,請使用 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 共同審查。