Hello 模組


該是來正式談談 WebAssembly 文字格式的時候了,因為其副檔名通常都會是 .wat,接下來就簡稱 Wat 吧!

WebAssembly 是模組為單位,一個最簡單的 Wat 可以是:

(module)

這是一個合法的模組,在編譯過後可以載入瀏覽器而不會發生任何錯誤,當然它什麼事都不做,編譯產生的 .wasm 檔案,基本上會有以下內容(透過 WABTwat2wasm 時加上 -v 引數可得到):

0000000: 0061 736d                                 ; WASM_BINARY_MAGIC
0000004: 0100 0000                                 ; WASM_BINARY_VERSION

一開始的 0061736D 魔法數字,其實代表著 \0asm,接下來的 01000000 是版本號碼,WebAssembly 採用 Little-endian,低位元組排列在前,因此版本號目前是 1。

一個 WebAssembly 模組可以包含許多區段(Section),就 Wat 撰寫時會用到的語法來說,主要有以下幾個區段:

  • import
  • export
  • start
  • global
  • memory
  • data
  • table
  • elements
  • function 與 code

一下子要認識全部的區段沒有意義,這會是日後逐一說明的課題,底下先來增加幾個基本常用的區段,認識一下基本的模組概念,首先,來增加一個 main 函式區段:

(module
    (func $main)
)

這個函式沒有定義參數與結果型態,也沒有定義要執行的程式碼,編譯產生的 .wasm 會有以下內容:

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
; type 0
000000b: 60                                        ; func
000000c: 00                                        ; num params
000000d: 00                                        ; num results
0000009: 04                                        ; FIXUP section size
; section "Function" (3)
000000e: 03                                        ; section code
000000f: 00                                        ; section size (guess)
0000010: 01                                        ; num functions
0000011: 00                                        ; function 0 signature index
000000f: 02                                        ; FIXUP section size
; section "Code" (10)
0000012: 0a                                        ; section code
0000013: 00                                        ; section size (guess)
0000014: 01                                        ; num functions
; function body 0
0000015: 00                                        ; func body size (guess)
0000016: 00                                        ; local decl count
0000017: 0b                                        ; end
0000015: 02                                        ; FIXUP func body size
0000013: 04                                        ; FIXUP section size

在這邊可以看到 Type、Function、Code 等區段,各區段下還有各自的欄位。

實際上,Wat 有 type 可以定義 Type 區段,不過 Type 部份通常會自動根據 Wat 內容產生,可以使用 wasm2wat 將產生的 .wasm 轉為 .wat:

(module
  (type (;0;) (func))
  (func (;0;) (type 0)))

在這邊可以看到 (;0;),這是註解,夾在 (;;) 之間的文字是會被忽略的,兩個註解的意思都是索引 0。

(type (;0;) (func)) 表示函式的型態,沒有傳回,每個型態會有個索引,從 0 開始,在 .wasm 中就是底下這個部份:

; type 0
000000b: 60                                        ; func
000000c: 00                                        ; num params
000000d: 00                                        ; num results
0000009: 04                                        ; FIXUP section size

(func (;0;) (type 0)) 表示函式,每個函式會有個索引,從 0 開始,在 (func (;0;) (type 0)) 中的 (type 0) 表示,這個函式的型態是方才索引 0,也就是 (type (;0;) (func)) 定義之型態。

也就是說,Wat 中的 $main 這類名稱,只是開發者撰寫 Wat 時方便,在編譯過後實際上是透過索引來指定。

目前只有一個函式,因此型態與函式索引都是 0,現在來加個 import

(module
    (import "env" "helloworld" (func $helloworld))
    (func $main)
)

.wasm 的內容會是:

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
; type 0
000000b: 60                                        ; func
000000c: 00                                        ; num params
000000d: 00                                        ; num results
0000009: 04                                        ; FIXUP section size
; section "Import" (2)
000000e: 02                                        ; section code
000000f: 00                                        ; section size (guess)
0000010: 01                                        ; num imports
; import header 0
0000011: 03                                        ; string length
0000012: 656e 76                                  env  ; import module name
0000015: 0a                                        ; string length
0000016: 6865 6c6c 6f77 6f72 6c64                 helloworld  ; import field name
0000020: 00                                        ; import kind
0000021: 00                                        ; import signature index
000000f: 12                                        ; FIXUP section size
; section "Function" (3)
0000022: 03                                        ; section code
0000023: 00                                        ; section size (guess)
0000024: 01                                        ; num functions
0000025: 00                                        ; function 0 signature index
0000023: 02                                        ; FIXUP section size
; section "Code" (10)
0000026: 0a                                        ; section code
0000027: 00                                        ; section size (guess)
0000028: 01                                        ; num functions
; function body 0
0000029: 00                                        ; func body size (guess)
000002a: 00                                        ; local decl count
000002b: 0b                                        ; end
0000029: 02                                        ; FIXUP func body size
0000027: 04                                        ; FIXUP section size

可以看到有了 type 0 與 type 1 的欄位,相同型態只會使用定義一個 type x,type 0 是 helloworld 函式之型態,type 1 是 main 函式之型態,

然後上面也出現了 Import 區段以及相關欄位,因為被匯入的函式會是索引 0,因此 main 函式的部份就成了索引 1,來將 .wasm 轉為 .wat 看看:

(module
  (type (;0;) (func (param i32)))
  (type (;1;) (func))
  (import "env" "log" (func (;0;) (type 0)))
  (func (;1;) (type 1)))

透過這個方式,可以逐漸認識區段以及 .wasm 的二進位結構,二進位結構的說明,可以參考官方〈Binary Encoding〉的內容。

進一步地,來定義函式本體,以及 start 區段:

(module
    (import "env" "helloworld" (func $helloworld))
    (func $main
        call $helloworld
    )
    (start $main)
)

call 用來呼叫函式,可以指定名稱(如果有定義的話)或索引,因此 call 0 也是相同的;start 呼叫的函式,會在模組載入、初始化之後執行,也就是說上面的程式,在模組載入之後,就會呼叫 main 函式,而 main 進一步呼叫匯入的 helloworld 函式,start 之後也可以接上索引。

上面的程式編譯過後轉回 .wat,會有以下內容:

(module
  (type (;0;) (func))
  (import "env" "helloworld" (func (;0;) (type 0)))
  (func (;1;) (type 0)
    call 0)
  (start 1))

以上認識的基本的模組結構,更多的說明會是之後文件的課題,最後,搭配以上的程式,假設 .wasm 的主檔名是 helloworld,來個 Hello World 吧!

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <div id="console"></div>

    <script>

    const console = document.getElementById('console');
    const importObj = {
        env: {
            helloworld() {
                console.innerHTML = 'Hello World';
            }
        }
    };

    WebAssembly.instantiateStreaming(fetch('helloworld.wasm'), importObj);

    </script>
  </body>
</html>

要匯入函式,必須建立匯入物件來組織,由於 .wat 中使用 (import "env" "helloworld" (func $helloworld)),因此物件上必須有 env 特性,參考著一個物件作為名稱空間,被匯入的函式名稱要與 env 上組織的函式名稱相同。

在上例,helloworld 函式會將 <div id="console"></div>innerHTML 設定為 'Hello World',因此可以直接看到網頁上出現了 Hello World 的字樣。