最近はWebAssemblyに興味があり、勉強していたんだけど仕様を読み始めても頭に入らないのでインタプリタを作ってみることにした。わからないものは作ってみるのが一番よい。github.comまだ残された課題は多いのだけれども、一つ目標にしていた「Goで書いたゲームボーイエミュレータを動かす」を達成できたのでここに書いておく。2020年2月24日こツイートに貼られているのは残念ながら、静止画ではなく、動画でありパフォーマンスが悲しいことになっていることを示している。あまりに遅くてプレイ画面まで到達できない。今後これは改善していきたい。Goでゲームボーイエミュレータを書いた話は以下の記事を参照のこと。blog.bokuweb.meblog.bokuweb.meインタプリタを作る仕様書を読みながら実装していくのだけど、日本語でさくっと全体を知るのに以下のリポジトリがとても参考になった。wasm開発は最小限のバイナリを実行できるようにし、そこに命令を足していく方針で進めた。Projectを開くとプロジェクトが作成されいくつかのファイルが生成される。この中のmain.watを書き換えて実行したり、バイナリを覗いたりすることができる。*.watというのはWebAssemblyのテキストフォーマットでWebAssembly/wabtで*.wasmに変換したり、ランタイムの一つであるwasmtimeで実行できたりする。シンプルな42を返すだけの関数を定義してみるとこうなる。instance.exports.hello();}).catch(console.error);これを実行すると42が返ってくることがわかる。Explorerという機能でプロジェクトの成果物である*.wasmを解析し表示してくれる。今回のサンプルであれば以下のようなものが表示される。mousehoverで詳細も表示してくれる。先頭の橙色はマジックナンバー。これはセクション番号とそのサイズを表している。Codeセクションが含まれていることがわかる。Codeセクションについて見ていく。ExportセクションExportは以下のようなデータで表現されており、先頭の0x07はセクション番号、次の0x09はセクションのデータサイズを示す。紫部分の先頭の0x01はExportするエントリ数。今回はhelloという関数のみをExportしているので0x01。次の0x05はExport名の長さを示す。0x6fを見るとこれがhelloであることがわかる。0x00だが、前者はExport種別を表す。今回は関数をExportしているので0x00だが、MemoryやTableなどもExportできる。最後尾はインデックスで今回の場合はモジュールが持っている関数一つの中から先頭の関数をExportしているので0x00になる。詳細は省いているが、Rustで書くと以下のようになった。}}LEB128順番が前後するが、例えばExport数を表すcountや最後尾のExportするエントリのインデックスの型はなにかと言うとu8やu32ではなくvaruint32になっている。これはLEB128という可変長の整数エンコード形式で表現された型で、値の大きさと表現にバイト数が比例する。具体的にはMSBがデータの継続を表すフラグとして使用されており、実データ部は8bitのうち7bitになる。100であれば1byteで表現できるし、255であれば表現に2byte必要になる。u32において4byteで表現されていたデータを表現するのに5byte必要になることもあるが、多くの場合で固定で4byte確保するよりバイナリを小さくできる。デバッグ用ファイルフォーマットのDWARFのために設計されたものらしい。wasmファイルをごにょごにょするものを作るなら早い段階でLEB128を読めるようにする必要がある。実装は愚直にMSBをチェックしながら結合していく実装になっている。効率的な読み方があったら教えて下さい。}}Exportはこれで完了で、つまり外部からhelloが呼ばれたら関数インデックス0の関数を実行すればいいということがわかる。Codeセクションじゃあ関数がどのようになっているかとCodeセクションを見れば良い。0x06はExportのときと同様セクション番号とサイズだ。0x00の0x01はエントリ数、0x04はエントリのBodyサイズ、0x00はローカル変数の数を表す。42に該当する部分になる。0x41はi32.const、0x2Aは42を表しており、0x0Bはendを表している。複雑だけれども、やっていることはExportセクションのデコードと変わらず愚直に読んでいくことになると思う。42)を実行すれば、なんとかなりそうなのがわかった。簡素化して書くと以下のようなイメージ。}wasmの実行はスタックマシンとして定義されているので、値用のスタック(この例ではstackという変数)を用意し、そこに値を出し入れするようにしている。i32.constはもらった値をスタックに積むだけの命令なのでstack.push(arg)して終了。stackに残った値(この場合42)が返り値となる。端折ったけど、ミニマムなものはだいたい、これくらいでできると思う。あとは命令やセクションを拡充しながらテストを通していくことになると思う。テストwasmにはtestsuiteが用意されているのでそれを使用するのがよい。github.comここでは以下のようなテストが定義されており、これを実行していくことでテストができる。2))assert_returnなどはテスト用のテキスト表現でwasm自体のこの、命令が備わっているわけではない。テストを実行するのはこのテキストフォーマットもparseして実行してやる必要がある。そのあたりを自分で実装するのはさすがに嫌だったので今回はwabtのRustバインディングを使った。これを使うと以下のようにテストが書ける。}テストが実行できるようになったらあとはテストが落ちたところを直していけばよい。このテストを走らすこと自体結構たいへんだったので、もしこの記事を見て試してみる人がいたら序盤からテストの実行について考慮することをおすすめしたい。ゲームボーイエミュレータテストが通るようになり、Goで書いたゲームボーイエミュレータを動かそうと試みた。方針としてはGo側には手を加えずブラウザで動くwasmファイルをそのまま動かそう、という方針。これは今からGo側を変更するのが面倒だったし、手を加えたことで、問題が発生した場合の切り分けをしたくなかったからなんだけど、これはこれであまりいい方針ではなかったような気がしてる。wasmを吐く場合、たとえばGoから以下のようにブラウザのwindowに対して値をセットできる。"world")これを実行するとwindowに値がセットされていることが確認できる。worldwasm-JS間は数値でしかやり取りできないのにこのようなことができるのか調べる必要があった。これはグルーコードとして提供されているwasm_exec.jsを見るとだいたい分かる。github.comJS側では以下のように値を保持しておき、配列のindexをリニアメモリ経由で指定することで値のやりとりをしているよう。"world")を例にとると、まずsyscall/js.stringValを呼んで"world"をthis._valuesに登録する。簡素化すると以下のようになっていて、"world"をthis._valuesに登録したあとthis._valuesの最後尾のindexをメモリに書き戻しているためGo側で"world"がthis._values格納されたindexを知ることができる。true);};次に、リニアメモリに"hello"を書き込みつつ、先に得られた"world"格納先のindexとwindowが格納されたindexをリニアメモリに書き込みyscall/js.valueSetを呼び出す。するとRefrect.setによりwindow.helloに"world"が代入されるという仕組みだ。);}これは一番簡単な例だが、関数呼び出しなどもこの応用でできている。あとは、Rustでなんとかwasm_exec.jsに擬態することにした。分かってはいたがwasm_exec.jsはReflectを多用しており、Rustとの相性はよくない。いい方針ではなかったと前述したのはこれが理由。アドホックでひどい作りな自覚があるのだけれども、なんとか擬態することはでき、syscall/js.copyBytesToJS経由でVRAMの内容を受け取りSDL2で描画することに成功した。"syscall/js.valueSet"は以下のようになった。}全体が気になる方はこちらgithub.com他のランタイムはどうしているのかと見てみたら、wasmerは専用のGoライブラリを用意しているようだった。github.comパフォーマンスはまだひどいけど、ひとまず目標の一つであるゲームボーイエミュレータの起動は達成できた。これからまずはWASIに対応したい。WASIに対応すればpythonが動かせるはずなのでこれはこれで面白い。合わせてパフォーマンスの改善。インタプリタではかなり速いと言われるwasm3の実装を見ていいところを取り込みたい。github.comまた、[no_std]に対応してベアメタル、強いて言うならARM7で動かしたい。JITやAOTの機構の検討もしたいが、ベアメタルでの動作を考慮にいれるとこのリポジトリでは採用することはなさそう。今回実装した機能はMVPで、あとにはGCやThreadなども控えているので勉強の題材としてはよさそう。11:00関連記事2020-04-14Rustから目覚めるぼくらのゲームボーイ!Conway氏についてですが、公式にアナウンスがでたようです。Autumnで発表させていただきまし...2019-01-11Denoを読む(1)正月にDenoを読んでたメモです。2018の21日目です。12引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。