通常は拡張子 .wat で表される、wasmのS式について
バイナリ形式をwasm,人間が読めるS式のものをwatと呼ぶ。
#S式
(module (memory 1) (func))
watの基本的なコードの単位はモジュールで、S式はモジュールのツリー構造を表現する。
一つのモジュールは一つの(...)
にあたる。括弧の中の1つ目のラベルmodule
はこのモジュールの種類を、それ以下のラベル(memory 1)
と(func)
がモジュールの子ノードにあたる。
この例ではモジュールが入れ子構造になっているが、子ノードは存在しなくてもよい。そのため、最小構成は以下のようになる。
(module)
ちなみに上の最小構成をバイナリに変換すると、(参考:WebAssembly テキストフォーマットから wasm に変換する)8バイトで表現されるモジュールヘッダのみになる。
0000000: 0061 736d ; WASM_BINARY_MAGIC
0000004: 0100 0000 ; WASM_BINARY_VERSION
#watモジュール
モジュールの括弧(...)
の先頭は、モジュールの種類を表す。
以下の種類がある。
説明 | |
---|---|
type | 関数の型を宣言する |
import | 外部から関数やデータを読み込み、wasm内で使えるように定義する |
func | 関数を定義する |
global | グローバル変数を宣言する |
export | 外部に関数やデータをエクスポート |
elem | テーブルの値を定義する |
data | メモリの初期値を定義する |
funcモジュール
wasm内での関数定義は、以下のような形式になる。
( func <signature> <locals> <body> )
シグネチャ
signatureは、単体パラメータを(param i32)、戻り値を(return i32)のように書く。
したがって、2つの32ビット整数を引数にとり、64ビット浮動小数点数を返すバイナリ関数は次のように記述する:
(func (param i32) (param i32) (result f64) ... )
;;はコメントを表す。以下のS式でも有効
(func (param i32 i32) (result f64) ... )
また、現在のwasmでは、以下の4つの型が有効である。
-
i32
32bit整数 -
i64
64bit整数 -
f32
32bit浮動小数点数 -
f64
64bit浮動小数点数
ローカル変数
シグネチャの後に、(local i32)
のように型付けされたローカル変数の並びが続く。
スタックマシン
関数が呼ばれると、空のスタックからスタートして、命令(local.getなど)にスタックにデータが積まれていく。
(func (param $p i32)
local.get $p
local.get $p
i32.add)
local.get $p
で$p
が指し示すものをスタックに詰め込む。それを2回行なった後にi32.addを行なっているので、スタックには$p + $p
が詰め込まれている。
本体で有効なオペコードの例
-
local.get 0
ローカル変数(0番目)からスタックへ -
local.set 0
スタックからローカル変数(0番目)へ -
i32.const 19
19という定数(型はi32)を定義してスタックへ -
i32.add
スタックの最上の2つを足して消し、結果をスタックに書き込む
全て見るには、以下を参照。
https://webassembly.org/docs/semantics/
関数を呼ぶ
外部(Javascriptなど)から関数を呼び出すには、exportを用いる。
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
get_local $lhs
get_local $rhs
i32.add)
(export "add" (func $add))
)
一方、定義した関数をwasmモジュール内で呼び出すには、callを用いる。
(module
(func $getAnswer (result i32)
i32.const 42)
(func (export "getAnswerPlus1") (result i32)
call $getAnswer
i32.const 1
i32.add))
memoryの使い方
文字列や複雑なデータ型を用いるのに、wasmではメモリを用いる。i32.loadやi32.storeを用いて、線形メモリの読み書きをする。javascriptと文字列などをやり取りするのは、メモリを通して行う。
//メモリを作る
var memory = new WebAssembly.Memory({initial:1});
//メモリのオフセットと長さから文字列を書き出す関数を作る
function consoleLogString(offset, length) {
var bytes = new Uint8Array(memory.buffer, offset, length);
var string = new TextDecoder('utf8').decode(bytes);
console.log(string);
}
//wasmに渡す関数とメモリのデータを作る。
var importObject = { console: { log: consoleLogString }, js: { mem: memory } };
WebAssembly.instantiateStreaming(fetch('logger2.wasm'), importObject)
.then(obj => {
obj.instance.exports.writeHi();
});
wasm内では、以下のようにしてメモリを利用する。
(module
(import "console" "log" (func $log (param i32 i32)))
(import "js" "mem" (memory 1))
(data (i32.const 0) "Hi")
(func (export "writeHi")
i32.const 0 ;; pass offset 0 to log
i32.const 2 ;; pass length 2 to log
call $log))
(import "js" "mem" (memory 1))
でメモリを読み込む。(memory 1)
で、1ページ(64kB)分のメモリを読み込んでいる。
(data (i32.const 0) "Hi")
では、i32.constで指定されたオフセットであるメモリの0番目以降に文字列"Hi"を書き込んでいる。オフセット0と長さ2から"Hi"を出力できる"writeHi"をexportする。
tableの使い方
テーブルは、以下のように定義する。
(module
(table 2 anyfunc)
(elem (i32.const 0) $f1 $f2)
(func $f1 (result i32) ;; 42を返す関数
i32.const 42)
(func $f2 (result i32) ;; 13を返す関数
i32.const 13)
...
)
i32.const 0
はオフセットをあらわし、この場合はテーブルのindexが0から始める。0が関数$f1
,1が関数$f2
に対応する。
テーブルで定義した関数を使うには、以下のようにする。
(type $return_i32 (func (result i32))) ;; if this was f32, type checking would fail
(func (export "callByIndex") (param $i i32) (result i32)
local.get $i
call_indirect (type $return_i32))
これにより、定義したテーブルの$i
番目の関数($i
はスタックに置かれた値)をcall_indirectで呼び出すことができる。関数の型は(type $return_i32)
に一致しているかどうか実行時にチェックされる。一致すると、WebAssembly.RuntimeError 例外がスローされる。
以上2つを合わせると、以下のようにしてテーブルで定義した関数を呼び出すことが出来る。
(module
(table 2 anyfunc)
(func $f1 (result i32)
i32.const 42)
(func $f2 (result i32)
i32.const 13)
(elem (i32.const 0) $f1 $f2)
(type $return_i32 (func (result i32)))
(func (export "callByIndex") (param $i i32) (result i32)
get_local $i
call_indirect (type $return_i32))
)
tableはミュータブルなので、Javascriptからは動的に操作できる(←やばい)