該是來正式談談 WebAssembly 文字格式的時候了,因為其副檔名通常都會是 .wat,接下來就簡稱 Wat 吧!
WebAssembly 是模組為單位,一個最簡單的 Wat 可以是:
(module)
這是一個合法的模組,在編譯過後可以載入瀏覽器而不會發生任何錯誤,當然它什麼事都不做,編譯產生的 .wasm 檔案,基本上會有以下內容(透過 WABT 的 wat2wasm
時加上 -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 的字樣。