最近の MYOS は、 WebAssembly ランタイムエンジンの大規模改修などをしていました。
主な更新内容は、以下のように多岐にわたります。
WASM 2.0 の機能 (の一部) の追加サポート
開発初期は WebAssembly MVP (最初のリリース) が出ていたのでこのライブラリも MVP のサブセットとして開発が始まりました。 WebAssembly 自体はその後いくつかの拡張機能の提案をマージして現在では WebAssembly 2.0 がリリースされようとしています。 *1
このライブラリにも一部の拡張機能が実装されていましたが、今回取り込み可能なほとんどの拡張機能を取り込み、 正式に WebAssembly 2.0 のサブセットと名乗ることにしました。
浮動小数型命令のサポート
浮動小数型の命令は整数型の命令よりも種類が多くサポートが大変になること、その反面多くの分野では必須ではないこと、 浮動小数型命令をサポートしないホスト環境でも実行できるようにすること、などの理由により当ライブラリは当初は整数型の命令しかサポートしない予定で作られました。
しかし、その後想定ターゲットを変更してほとんどの浮動小数型の命令を標準サポートすることに変更したため、ほぼ全ての WebAssembly MVP 命令を実行可能になりました。
なお、SIMD拡張命令はスタック構造の見直しが必要でホスト側の移植難易度も上がるため、近い将来のサポート予定はありません。
インスタンス化処理の改良
最も重要な変更点は、モジュールのコンパイル (compile) とインポートのリンク (instantiate) を同時に行わないように変更し、リンクのエラーでコンパイルが中断されないようになりました。
これは JS の標準 API に一部準拠したものとなり、コンパイルとリンクの間にインポートテーブルやエクスポートテーブルを参照した別の処理を挟むことが可能になります。
メモリ (memory) のロック
WebAssembly のメモリオブジェクト (memory) の扱いが rust の所有権システムを遵守する実装になっていませんでした。
具体的には、メモリの読み書きは同時に行っても常識の範囲内の挙動ですが、 memory.grow
は他のメモリアクセスと排他制御しないとメモリ破壊の可能性があります。
現状はメモリ共有機能がないので同時アクセスは発生しませんが、高速化のためにメモリ内部のポインタを直接使う場合はやはり排他制御が必要になります。
そこで、通常のメモリアクセスと memory.grow
の実行を RwLock と似た仕組みを使って排他ロックするように変更しました。
std::sync::RwLock
は core
では使えないので、ロックに失敗するとインタプリタが中断する独自のクラスを使って実現しています。
*2
バイトコードデコード用 enum の改良
WebAssembly MVP には1バイトのバイトコードしか存在しなかったため、このライブラリも当初はバイトコードと1対1で対応する 8bit の enum で命令コードを管理していました。
WebAssembly 2.0 では2バイト以上の命令が追加され、既存の enum ではうまく管理できなくなりました。 また、命令数が増えるとフェッチ・デコード処理や enum の内容管理が大変になってきました。
今後の拡張を見据えると2バイト命令対応は必須となるため、 CSV https://github.com/neri/wami/blob/canary/misc/opcode.csv でバイトコードの一覧を定義して build.rs https://github.com/neri/wami/blob/canary/build.rs で命令コードの enum やフェッチ処理 https://github.com/neri/wami/blob/canary/src/_generated/opcode.rs を自動生成するようにしました。
ブロック命令の互換性強化
if
〜 else
命令や返り値を持つことができるブロック命令は制御フローやスタックの巻き戻し処理などが純粋なブロック命令より複雑になりますが、 rust でビルドしたバイナリではほとんど使われない機能だったため、今までサポート外でした。
しかし、他の処理系でビルドしたバイナリでは使われている可能性があるので動くよう対応強化しました。
グローバル変数の改良
当初 WebAssembly のグローバル変数はあまり重要ではないと判断していました。
しかし、実際には当初の想定より重要なことがわかってきたため、グローバル変数に関する処理を改良しました。 現時点ではまだダイナミックリンクをサポートしておらず、Rustの作法に適合しつつ実行効率の良い解決方法を模索中です。
コンパイルエラーの改良
WebAssembly のバイトコードはCPUが直接実行できる命令ではないため仕様上はコンパイル・検証でさまざまなエラーが発生する可能性があります。 WebAssembly は事前検証しながら中間コードに変換した方が効率よく実行できる命令が多いため、事前検証は実行に必要な工程となります。
実際のところ、通常の方法で生成したバイナリは検証エラーが発生するバイトコードは含まれておらずそのまま動作することが期待されますが、自作のライブラリではバグで検証エラーが発生する可能性があります。
いままでエラー発生時にエラーの大まかな理由を特定することはできましたが、バイナリのどの辺りを読み込んだエラーなのか通知する仕組みがありませんでした。 特に、バイトコードのコンパイルエラーは膨大なバイトコードの中のどの命令で問題が起きているのか特定するのが非常に困難でした。
そこで、バイトコードの検証エラーではどの命令の検証中だったかエラー情報が残るように拡張しました。
また Rust の標準的なエラー処理に合うように Box<dyn Error>
で受け取るように改良しました。
インポート・エクスポートの自動生成
WebAssembly 内部のバイトコードと外部のインタプリタで動的リンクを実現するためには、お互いの世界のルールが違うので呼び出す際に引数のチェックや型の変換を行う必要があります。
引数の組み合わせはそれぞれの関数によって異なり、事前に用意しようとすると無限の組み合わせが必要になるため、実装上は一旦すべてまとめた配列を経由して渡します。 そして、配列からデータを取り出す際にそれぞれの型に変換が必要ですが、従来は手動のグルーコードの実装が必要でした。
この作業は将来大きな間違いが起こる可能性が非常に高いため自動化したいと考えていました。
Rust にはマクロ機能が何種類かありますが、関数のシグネチャ *3 を認識するには proc_macro の機能が必要となり、 proc_macro は他のマクロと比べて導入の敷居が高いため最近まで手を付けていませんでした。
proc_macro は TokenStream に介入できるため高機能のマクロを提供できる一方で、トークンをパースする処理はコンパイラ内部と同じような文法処理が必要になります。 また、 proc_macro が干渉できるのはマクロ属性が付いたソースコードの一部分のトークンのみで、ソースコード全体に影響を与えたり、実際の型システムにアクセスすることはできません。
マクロの生成したコードで発生したエラーやバグで panic が発生した際のエラー情報も不親切でデバッグはかなり大変でしたが、なんとか対応できました。
WebAssembly は元々 JavaScript から利用する想定でダイナミックリンクのシグネチャチェックも緩いですが*4 手動でグルーコードを生成するより簡単に型チェックできるようになりました。
なお、現状 myos のシステムコールは可変長の引数を単一の関数で処理しているため、このマクロによる恩恵はほとんどありません(なんでや)
改良の結果
これらの改良を行った結果、例えば3D処理のような今まで実現が難しかったアプリも実行できるようになりました。
また、今までエラーになってインスタンス化できなかった他環境向けバイナリも読み込めるものがかなり増えました(実行できるとは言ってない)
次回予告?
さて、ここまで大規模な改修となったのは、今まであまり必要性がなかったために後回しにされていた機能が多く、今回あえて対応したのはそれらの機能が必要になったということでもあります。
しかし、いざ実装を始めてみると手を入れるところが多すぎて、当初の予定より何ヶ月も余分にかかってしまいました。(この日記の下書きを始めてからでさえすでに半年以上経過しています) そして、ランタイムの整備が終わる頃には、当初予定していた機能を組み込む熱意はほぼ失われていました。鉄は熱いうちに打て。
そのため、今回の改修によって MYOS のできることが増えたり使用感が変わるような変更はありません。 本当は何をやりたかったのか次回明らかに(なるといいな