ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

自作WASM Runtimeでグレースケール処理を動かすまで

あらすじ

udzura.hatenablog.jp

tl;dr

  • 自作WASM Runtimeでグレースケール処理を動かした
  • グレースケール処理はRustで書いたもの
  • WebAssembly specificationはテストケース付きで公開されてるので便利

やったこと

自作WASM Runtime(以下、 Wardite )でRustで書いたグレースケール処理がちゃんと動いた。

ちょっと前にWarditeでWASM 1.0の範囲の基本的な命令を一通り実装した (参照) んだけど、別の個人プロジェクトで作った、Rustで書いたWASMのグレースケール処理を動かそうとしたところ、普通に unreachable (Rustのpanicは最終的にunreachable命令になる)になってしまっていた。その時点で簡単なWASMプログラムなら正常動作していたのだが、この処理は動かずにぐぬぬとなっていた。

その後、ゆるゆる調べて、

  • そもそも色々正しく動いてなかった。たとえば関数のローカル変数定義を正しく読めてなかったので直した (コミット) とか
  • 一番手間取ったのが、 memory.grow を呼んだ時に自分のメモリの大きさを更新していなかったバグで、まるっきり気づいていなかったのだが、観察したら変なとこにmemsetするような動きをして線形メモリを破壊していたので直した (コミット
    • これが全然わからず、ブラウザ側で同じグレースケール処理を動かして、関数エントリポイントごとにステップ実行して変数を確認して... ということをしていた。今思うと先に全部specを通しても良かった
  • ここまで直してメモリが壊れなくなったので、一旦WASM側の処理を一つ一つ動かしてどこでpanicになっているのか当たりをつけることにした

WASM側の処理はざっくりいうと以下のようになっていて、

DataURL形式でPNGのデータを受け取る
DataURLからbase64 decodeしてBlobデータを得る
バイナリのPNGを image クレートでピクセルにする
ピクセルを全部iterateして、RGBの平均でグレースケールにする
ピクセルをPNGに戻す
PNGのBlobをbase64 encodeする

調べると、base64 decode/encodeは正常に動きそう。ただ、PNGを読み込むところでエラーになっており、具体的なエラー文字列を引っ張り出したら DistanceTooFarBack と書かれていた。

github.com

つまりPNGの中のdeflateの処理のようなんだけど、コードを見るとビットシフト操作が多めのようだった。

別件で、WebAssembly specificationにはテストケースがついており、それを実行してちゃんと実装されているか自動テストできそうと知った。

zenn.dev

この記事で知った。ゴリラさんと言い、WebAssemblyランタイム自作勢の積極的なアウトプットに感謝(リスペクト)...。

試みに wg-1.0 のspecをもとにやっていくかな〜と調べた。

github.com

このspecにある i32.wast に対し wast2json を使えばテスト用のwasmバイナリと、テストケースを記述した i32.json が生成される。

様子

そのjsonをもとにRubyのtest-unitを使って自動でテストを回すようにしたものが以下:

github.com

これはめちゃくちゃ適当な書き捨てなのでぼちぼち整理するとして、一旦i32をどうにかしようと思った。

そうするとspecを読めばすぐわかるような実装ミスがいくつかあり、特にビットシフト周りが荒ぶっていたので一通り修正。たとえばこんなね...

最終的にi32の正常系は通すようにした。

Finished in 0.226758 seconds.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
442 tests, 368 assertions, 0 failures, 0 errors, 0 pendings, 83 omissions, 0 notifications
100% passed
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
1949.21 tests/s, 1622.88 assertions/s

ただ、typeが assert_invalid とか assert_malformed になっているテストケースはそもそも関数がexportされておらず、多分ロードした時に静的検査をして(たとえば引数の数が)壊れているのを判定するテストなのだと思う。静的検査はおいおいやるということにして一旦omitしてある。

この状態でgrayscale.wasmの再実行に挑戦したら、無事グレースケール処理が動いて自分のアイコンがグレースケールになった。

original grayscaled

今後のやっていき

正しく動いたので偉い。が遅い。ある程度はそりゃ遅いんだろうけど、

YJITで3倍になった(本当に3x3じゃん)と喜んでいる場合でもない。ただ、この感じだと割と大きな見落としをしてるだけだろうので、粛々計測して直せばいいだろう。多分...。

そしてそもそも WASI preview 1 をサポートするというのも目標の一つだったのだ。これもなんか、頑張ってやるだけ(そうかな?)なのでやる。

そうこうしていれば、 ruby.wasm が WASM on ruby で動くという面白い(?)ことを達成できるかもしれない。ruby.wasmのImportセクションが wasi_snapshot_preview1 だけなことと、ruby.wasmで使ってそうな命令は一通りWarditeで実装できてそうなことは見ているが果たして。

あと、Runtimeを実装して、VMの気持ちが1ミクロンわかった気がしたので、他のVMRubyに移植したりもしたいなと思った。またいずれお話しするでしょう...。