WebAssembly?怎麼會來玩這個?就大部份開發者而言,直接玩 WebAssembly 沒什麼太大意義,基本上,未來應該會有更多便利的工具,可以從你熟悉的某語言,編譯至 WebAssembly。
現今常見的例子就是 Go、Kotlin、Rust 等語言,在〈Awesome WebAssembly Languages〉有個清單,則列出了可以編譯為 WebAssembly,或虛擬機可支援 WebAssembly 的語言。
對於前端開發者而言,甚至還有 AssemblyScript,它的語法與 TypeScript 一致,雖然有更嚴格的型態限制,然而對於熟悉 JavaScript 的開發者而言,仍然是較易入門與編寫程式碼,並編譯為 WebAssembly 的語言選項。
會玩 WebAssembly,是因為我土炮過 ToyLang,它直接剖析、運行在 JavaScript 環境中,未來若有機會實作其他語言,會想試試將之編譯為位元碼,我想漸漸熟悉位元碼的編寫邏輯,而 WebAssembly 有文字格式,算是主要運算在堆疊上進行的語言,是個不錯的學習對象。
總之,想玩 WebAssembly,現在已經有許多選項,就連工具鏈也是,可以將 C 編譯為 asm.js,基於 LLVM 的 Emscripten,現在也可以編譯為 WebAssembly 了。
將 C 編譯為 WebAssembly 是最常見的,現在更有些線上網站,可以直接輸入 C,編譯為 WebAssembly 後提供下載,例如 WasmFiddle:
你可以在左上輸入 C 程式碼,按下「Build」之後會編譯為 WebAssembly,左下是對應於 WebAssembly 的文字格式,這也會是後面我想要學習的主要對象,右上是呼叫 WebAssembly 時的 JavaScript API,在第 3 行的部份,呼叫了 C 程式碼定義的 add
函式,可以按下「Run」觀看執行結果。
將 C 編譯為 WebAssembly,會讓人誤以為 WebAssembly 執行是很快速的,這是 WebAssembly 的理想,還不是現況,不管你原始撰寫的語言有多快,最後執行的只是 WebAssembly。
WasmFiddle 右上提供的「JS」窗格可以直接編寫 JavaScript,若想自行編寫網頁,可以在 WasmFiddle 按下「Wasm」下載已經編譯完成的位元碼,預設會是 program.wasm 檔名,然後撰寫一個 HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
WebAssembly.instantiateStreaming(fetch('program.wasm'))
.then(prog => {
console.log(prog.instance.exports.add(1, 2));
});
</script>
</body>
</html>
就目前看到的文件來說,未來的 script
標籤,可以使用 type = "module"
來載入 WebAssembly(因為 WebAssembly 是以模組為單位),就現在來說,最簡單的方式是使用 Fetch API(基本上,要使用 XMLHttpRequest
也可以),接著可以直接將 Fetch 取得的結果,交給 WebAssembly JavaScript API 的 WebAssembly.instantiateStreaming
函式來編譯位元組碼,並使用編譯後的結果建立可執行的實例。
WebAssembly.instantiateStreaming
傳回 Promise
,在處理完成後會得到一個物件,其中有 module
與 instance
兩個特性,前者是 WebAssembly.Module
的實例,代表編譯過後的位元組碼,後者是 WebAssembly.Instance
的實例,代表著模組的可執行形式,具體而言就是包含了模組中被匯出、可被呼叫的函式。
(WebAssembly.Instance
的 prototype
並不是 WebAssembly.Module
實例,只是建構 WebAssembly.Instance
實例時,是根據 WebAssembly.Module
實例的資訊。)
在這邊的範例中,主要關心 add
函式,可以透過 WebAssembly.Instance
實例的 exports
取得模組匯出的函式,因此上面的範例,會在主控台中顯示 3 的結果,記得,因為使用 Fetch API 來取得 .wasm 檔案,你必須有個簡單的 HTTP 伺服器,使用瀏覽器連接伺服器上的 HTML 網頁來執行。
WebAssembly 也可以呼叫 JavaScript 環境匯入之函式,例如,可以在 WasmFiddle 中輸入程式碼:
void log(int n);
int add(int n1, int n2) {
int result = n1 + n2;
log(result);
return result;
}
在按下「Build」之後,留意右下,它會產生 WebAssembly 的文字格式:
(module
(type $FUNCSIG$vi (func (param i32)))
(import "env" "log" (func $log (param i32)))
(table 0 anyfunc)
(memory $0 1)
(export "memory" (memory $0))
(export "add" (func $add))
(func $add (; 1 ;) (param $0 i32) (param $1 i32) (result i32)
(call $log
(tee_local $1
(i32.add
(get_local $1)
(get_local $0)
)
)
)
(get_local $1)
)
)
目前只需要先關心 import
那行,其中 "env"
表示匯入物件的名稱,"log"
表示匯入之函式名稱,"env"
這名稱其實是可以自訂的,不過因為現在是透過 WasmFiddle,從 C 編譯為 WebAssembly,暫時得使用 "env"
這名稱。
因此在下載 program.wasm 之後,你的 HTML 頁面必須撰寫:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
const importObj = {
env: {
log: n => console.log(n)
}
};
WebAssembly.instantiateStreaming(fetch('program.wasm'), importObj)
.then(prog => {
console.log(prog.instance.exports.add(1, 2));
});
</script>
</body>
</html>
這麼一來,執行後你的主控台就會顯示兩個 3,一個是在 add
函式中呼叫 log
函式的輸出,一個是 add
函式執行後,傳回給 console.log
的輸出。
從 C 到 WebAssembly 的這個過程,主要是用來瞭解使用 WebAssembly 的基本流程:
- 使用某語言撰寫程式
- 編譯為 .wasm 檔案
- 瀏覽器載入 .wasm 檔案
- WebAssembly JavaScript API 編譯與實例化
- 使用 JavaScript 與模組互操作
後續會使用 WebAssembly 文字格式來撰寫程式,然而,使用 C 的這個過程還是有用的,因為有時,可以觀察 C 轉換後的 WebAssembly 文字格式,來瞭解到某個 WebAssembly 指令如何撰寫。
現代瀏覽器如 Chrome、Firefox 已支援 WebAssembly 的除錯,除錯時可轉為 WebAssembly 文字格式,這也是要認識 WebAssembly 文字格式的原因之一,後續的文件中也會談到。