WebAssembly 是什麼?來源為何?

自從網路變成文件和應用程式的平台後,一些最先進的應用程式都突破了網路瀏覽器的限制。但為了提升效能,許多高階語言都具備「更接近金屬」的方法。舉例來說,Java 具有 Java 原生介面。在 JavaScript 中,這個層級較低的語言為 WebAssembly。本文將介紹組譯語言,並說明這個語言在網路上的用途,接著介紹我們如何透過 asm.js 的暫時性解決方案建立 WebAssembly。

組合語言

你是否曾使用組合語言程式?在電腦程式設計中,組譯語言通常簡稱為「組合語言」,通常簡稱為 ASM 或 Asm,指的是任何低階程式設計語言,根據語言指示和架構的機器碼操作說明之間的對應關係。

舉例來說,查看 Intel® 64 和 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) 編寫的靜態類型語言 (例如 C) 程式碼是由來源對原始碼編譯器的翻譯,例如 early 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 System 介面 (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 引擎中執行 (即使未最佳化),WebAssemb 需要所有瀏覽器供應商都同意的新功能。2015 年宣布,首次在 2017 年 3 月發布,WebAssembly 已於 2019 年 12 月 5 日成為 W3C 的建議。W3C 具有所有主要瀏覽器廠商及其他相關各方貢獻的貢獻,載明這項標準。自 2017 年起,瀏覽器功能已相當普及。

WebAssembly 有兩種表示法:「文字」和「二進位」。以上都是文字的表示法。

文字表示法

文字表示法是以 S-expressions 為基礎,通常使用副檔名 .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 等 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 審查。