少し前からWASMのRuntimeを書いている。もちろん、完全に趣味だ。実装にはRustを使っている。

WASMに関心があるのにずっと手を出しかねていた。その理由の一つは「WASMを生成するツールの使いこなしノウハウ」がまず必要だからで、そこがあまり楽しそうじゃなかったから。かなり遠回りだけど、自分でランタイムを書くと理解が深まりそうだな、と思いついたのだ。しばらく触ってなくてすっかり忘れてしまったRustを思い出したいのもある。

コード

ここにある → Raftik

現時点では、wasmバイナリをパースして結果を表示する、までは動いているはず。

方針

できるだけ依存関係少なく、自前で実装する。いまのところ、外部の依存としてはパーサコンビネータのnomだけ使っている。そして速度や効率よりもコードの素直さ・読みやすさを重視する。

ここまでできたこと

ようやく、バイナリのパースができたところだ。現在の2.0 Draftに書かれている範囲は全部実装しているはずだ。validationは別途行う方針だが、各sectionのパースが終わった時、sectionに指定されたサイズを全て読み終わっているか、などごく一部の一貫性はチェックしている。パース結果は読み込んだバイナリ列と同じライフタイムを持っていて、data sectionやコード列は元のバイナリ列から切り出したスライスを持っている(つまり、instructionはパースしていない)。

得た知識

WASMでの整数表現は原則としてLEB128という可変長エンコードを使っている。LEB128というエンコードの存在は今回初めて知った。例えばu32のデータは、1バイトから3バイトで表現できる。3バイトになると固定長より長いんだけれども、7bitで表せる範囲なら1バイトにおさまるので、全体的にはバイナリが圧縮できる傾向にある。LEB128のデコーダはさほど難しくないので、自前で実装した。ただテストが不十分だしバグがあるかもしれない、と今これを書いていて思った。あとでテストを追加していこう。

テキスト表現(WAT)とバイナリ表現が結構違うのも面白い。例えばfunctionは、type section (関数の型定義)、function section (どの型か、というだけの情報)、code section(ローカル変数定義と命令列)の3箇所に分散している。memoryはバイナリ上は複数memory sectionに置ける仕様だが、現在のWATではひとつしかmemoryを置けない。data segmentではmemoryのインデックスを指定する仕様があるが、現仕様のWATではmemoryがひとつしか置けないのでこの指定は必ず0になる。

element sectionが一番パースの実装が難しかった。まず意味がわからない(ろくにWASM書かずにいきなりランタイムの実装始めるからだ)。しばらく調べて、関数参照をテーブルに配置したりするためのものだということを理解した。動的ディスパッチなどに使われる。さらにここでWAT上はanyfuncっていうワードが出てくるけれど、それはバイナリの仕様上はfuncrefになる。これはどうも歴史的経緯っぽいが深追いしていない。

今後の方針

このあと、validationとexecutionを実装していく。validationのフェーズでは、原則としてこのパース済みデータをそのまま使うつもりでいる。execution時に、instruction列にも型を与えて構造を持たせる予定だ。もちろん実装していくと変わっていくとは思う。とりあえずの目標はWASIなどを含まないWASMを読み込んで実行できること。実行時に使える対話環境はほしいと思っている。validationは後回しにして、まずi32.addだけでも実行できる方向で実装をはじめる、かもしれない。まだ決めかねている。

もう一つ、今のバイナリパーサ実装は異常時に何が起きたか何もわからないので、エラーの改善もどこかのタイミングでやっていきたい。