從 Wat 到 WebAssembly


WebAssembly 是二進位格式,然而,提供了文字格式,便於人類撰寫與閱讀,在〈從 C 到 WebAssembly〉中看過 C 編譯為 WebAssembly,後者對應的文字格式,如果接觸過 Lisp,可能會覺得有點熟悉,因為 WebAssembly 是基於 S-expression,每個文字格式檔案,就是一個巨大的 S-expression。

在線上工具部份,WasmFiddle 可以將 C 編譯為 WebAssembly,而且會顯示對應的文字格式,然而沒辦法編輯文字格式,另一個線上工具 WasmExplorer,可以編輯 C,也可以編輯 WebAssembly 文字格式(然而只提供編譯結果下載,沒有線上編寫 JavaScript 的介面)。

從 Wat 到 WebAssembly

在上圖中,Wat 窗格是編寫 WebAssembly 文字格式的地方,WebAssembly 文字格式的副檔名是 .wat,因此常簡稱為 Wat,之後的文件就也這麼稱呼好了。

在 Wat 窗格編寫完程式,按下「ASSEMBLE」按鈕,就可以編譯為二進位格式,按下「DOWNLOAD」可以下載 .wasm 檔案(至於最右邊的窗格,來自於 Firefox 的 WebAssembly 引擎產生的 .x86 檔案,為WebAssembly 反組譯後的輸出)。

雖然還沒正式談過 Wat 的撰寫,不過,從 modulefuncparamresultexport 等關鍵字,大致上可以看出來,上圖 Wat 窗格中撰寫的程式碼中,定義了一個模組,一個 add 函式並將之匯出:

(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
    get_local $lhs
    get_local $rhs
    i32.add)
  (export "add" (func $add))
)

get_local 取得指定參數的值,並放至堆疊之中,因為要進行加法,因此先取得左運算元($lhs 表示 Left Hand Side)置入堆疊,取得右運算元($rhs 表示 Right Hand Side)置入堆疊,它們都是 i32,也就是 32 位整數,因此進行 i32.add,也就是 32 位元整數加法,這會從堆疊頂端分別取出兩個值,相加後的結果置回堆疊,作為函式的呼叫結果。

下載 .wasm 檔案之後,想要呼叫這個模組匯出的 add 函式,可以使用〈從 C 到 WebAssembly〉第一個 HTML 中撰寫的 JavaScript:

<!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>

如果不想要在線上撰寫、編譯 Wat,可以使用 The WebAssembly Binary Toolkit,簡稱 wabt,依其中文件完成編譯之後,基本使用方式是,使用 wat2wasm 將 Wat 編譯為 .wasm,或者使用 wasm2wat 將 .wasm 反編譯為 .wat。

如果你使用 Visual Studio Code,可以安裝 Dmitriy Tsvettsikh 的 WebAssembly Toolkit for VSCode 外掛,它提供了編輯器語法著色,可以在 .wat 上按右鍵另存 .wasm 檔案,或者在 .wasm 上按右鍵,另存或顯示文字格式。

從 Wat 到 WebAssembly

上圖中,我使用的是 Node.js 並配合簡單的 http-server

如果要在 WebAssembly 文字格式中,匯入、呼叫 JavaScript 環境匯入之函式,可以使用 import。例如,底下是個等效於〈從 C 到 WebAssembly〉中,匯入 log 的 Wat 寫法:

(module
  (import "env" "log" (func $log (param i32)))
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
    (local $result i32)
    get_local $lhs
    get_local $rhs
    i32.add
    tee_local $result
    call $log
    get_local $result)
  (export "add" (func $add))
)

這當中包含了更多 Wat 的細節,之後會再依序說明。

談到 Node.js,它現在也支援 WebAssembly,以 LTS v8.11.4 為例,可以撰寫以下的 .js:

const fs = require('fs');
const buffer = fs.readFileSync(('program.wasm'));

WebAssembly.instantiate(buffer)
.then(prog => {
    console.log(prog.instance.exports.add(1, 2)); 
});

然後執行這個 .js 檔案就會看到 3 的結果輸出。