sosukesuzuki.dev

August, 09 2024

WebKitにWasm Type Reflection API(の一部)を実装してみた

WebAssembly Type Reflection JavaScript APIの一部をWebKitに実装したので紹介します。

WebAssembly Type Reflection JavaScript API とは

WebAssembly Type Reflection JavaScript APIは、Memory・Table・Global・関数の型に関する情報をJavaScriptから取得するAPIを追加する提案です。https://github.com/WebAssembly/js-types で管理されています。

たとえば、MemoryやTableであればサイズの制限、Globalであれば値の型とミュータビリティ、FunctionであればそのシグネチャをJavaScriptから取得できます。

この提案は、既存のAPIに対して3つの変更を加えます。

1. Memory、Global、Tableに対するtypeメソッドの追加

以下の3つの関数が追加されます:

  • WebAssembly.Memory.prototype.type
  • WebAssembly.Table.prototype.type
  • WebAssembly.Global.prototype.type

これらのメソッドを呼び出すことでそれぞれの型の情報を取得できます。以下に具体的な使い方を示します:

const memory = new WebAssembly.Memory({ initial: 10, maximum: 100 });
console.log(memory.type()); // {"maximum":100,"minimum":10,"shared":false}

const table = new WebAssembly.Table({ initial: 10, element: "anyfunc" });
console.log(table.type()); // {"minimum":10,"element":"funcref"}

const global = new WebAssembly.Global({ value: "i32", mutable: true }, 0);
console.log(global.type()); // {"mutable":true,"value":"i32"}

2. Module.importsとModule.exportsの返り値の要素のtypeプロパティの追加

以下の2つの既存の関数の返り値が変更されます:

  • WebAssembly.Module.imports
  • WebAssembly.Module.exports

この2つの関数は、WebAssemblyのモジュールのインスタンスを引数にとって、そのモジュールがimportあるいはexportしているそれぞれの値に関する情報を配列として返します。

以下に具体的な例を示します:

// このwatに対応するバイナリ:
//   (module
//     (import "mod" "fn1" (func $log1 (param i64) (param i32) (result i32)))
//   )
const buffer = new Uint8Array([0,97,115,109,1,0,0,0,1,7,1,96,2,126,127,1,127,2,11,1,3,109,111,100,3,102,110,49,0,0]);

const module = new WebAssembly.Module(buffer);
const imported = WebAssembly.Module.imports(module);
console.log(imported[0]);
// {
//   "module":"mod",
//   "name":"fn1",
//   "kind":"function",
//   "type":{"parameters":["i64","i32"],"results":["i32"]}
// }

この提案以前は、モジュールの名前を表す module、インポートする値の名前を表す name、インポートする値の種類を表す kind という3つのプロパティがありました。この提案は、さらに型の情報を表す type というプロパティを追加します。

type プロパティの値は、この例で示したようにインポートした値が関数であればこの例のようにそのシグネチャになります。インポートした値がMemoryやTable、GlobalであればそれぞれWebAssembly.Memory.prototype.typeWebAssembly.Table.prototype.typeWebAssembly.Global.prototype.typeの返り値の型の値になります。

3. MemoryとTableのコンストラクタの引数の変更

WebAssembly.MemoryWebAssembly.Table のコンストラクタが受け取るオブジェクトのプロパティの名前として、initial の代わりに minimum が使えるようになります。

WebKitでの実装

このType Reflection JavaScript APIは4年ほど前から存在していて、ChromiumとFirefoxには実装されています。WebKitには1と3は実装されていましたが、2は実装されていませんでした。

RubyのWasm周りやSwiftWasmのメンテナーである@kateinoigakukunと話していたときに「Type Reflection JS APIの一部がWebKitにだけなくて困って、polyfil書いたんだよね~~」という話を聞きました[1]

筆者はここ半年くらいWebKitに継続的に貢献していますが、WebAssemblyに関連する部分は全く触ったことがなかったし、そもそもWebAssemblyのことを全く知りませんでした。いずれWebAssemblyもやりたいと思ってはいたのですが、きっかけを上手く見つけられずにいたところだったので、この機会に実装してみることにしました。

コードリーディングと実装

ここからはWebKitのコードリーディングと具体的な実装の手順の説明になります。

WebAssembly.Module.importsWebAssembly.Module.exportsの返り値の要素に追加するべきであるtypeプロパティが持っている情報は、明らかにモジュールをコンパイルするときにすでにわかっている情報です。なので、引数として渡されるWebAssemblyモジュールインスタンスから取得できると考えました。

importsexportsで考えることはほとんど変わらないので、とりあえずimportsについて説明します。

WebKitで、WebAssembly.Module.imports関数の実装に対応するコードは Source/JavaScriptCore/wasm/js/WebAssemblyModuleConstructor.cpp#L202-L234 です。ここでは引数として渡されたモジュールインスタンスを JSWebAssemblyModule 型の変数 module として扱っています。なので、ここではこの module 変数から上手くデータをひっぱってきて返り値の type プロパティにつっこんでやれば良いわけです。さらに、周辺のコードを読むとモジュールに関するデータは module->moduleInformation() で取得できることがわかります。

ではどうやって module->moduleInformation() からひっぱってくるべきデータを探すのかということになります。WebAssemblyモジュールのバイナリフォーマットのインポートセクションを解析する部分があり、そこでモジュールのデータをセットしているはずだと考えました。

実際、Source/JavaScriptCore/wasm/WasmSectionParser.cpp#L147-L231でモジュールのインポートセクションを解析して、kind に応じて m_info というフィールドに色々とセットしていることがわかりました。module->moduleInformation() は基本的に WasmSectionParser.cpp で作られた m_info を返すだけなので、そこでセットされた情報を WebAssembly.Module.imports の方から読み出せば良いということになります。

ということで、module->moduleInformation() からほしいデータを取得してオブジェクトを作成する createTypeReflectionObject という関数を書いて、type プロパティにセットするようにしたら、意図した通りに動作しました。コードは Source/JavaScriptCore/wasm/js/WebAssemblyModuleConstructor.cpp#L104-L200 にあります。

ちなみにこの createTypeReflectionObject 関数は WebAssembly.Module.importsWebAssembly.Module.exports の両方のために使うことを想定して作っています。WebKit でインポートのエントリを表す型は Wasm::Import で、エクスポートのエントリを表す型で Wasm::Export で、それらは異なる型です。

WebKitではC++20を使うことができるのでコンセプトという機能を使って、Wasm::ImportWasm::Exportのどちらかのみを受け付けるようにしてみました。コンセプトって初めて使いましたがTypeScriptみたいで面白いですね。

template<typename T>
concept IsImportOrExport = std::same_as<T, Wasm::Import> || std::same_as<T, Wasm::Export>;

template <IsImportOrExport T>
static JSObject* createTypeReflectionObject(JSGlobalObject* globalObject, JSWebAssemblyModule* module, const T& impOrExp)
{
    // ...
}

Pull Request は https://github.com/WebKit/WebKit/pull/31702 です。

Function Referenceとの関係

ここに書いたところまでは割とサクッとできたのですが、このあと難しい箇所がありました。

Function Referenceという別の提案の方に、Type Reflectionとの相互運用についての言及があります[2]。WebKitはすでにFunction Referenceをフラグ付きでサポートしているので、Type Reflectionを実装するなら、そこを考慮する必要がありました。

そのための実装自体は前例があったから簡単だったものの、この提案のことは理解できていないので、もうちょっとちゃんと勉強する必要がありそうです。

また、このFunction Referenceで追加された機能を使ったwatを、wabtを使ってwasmにしようとしたら構文エラーが起きてしまいました。どうやらまだFunction Referenceをちゃんとサポートできていないようだったので途中からwasm-toolsに切り替えました。

おわりに

WebAssemblyの世界に入門できた気がするのでやってみて良かったと思います。今回はJavaScript APIの実装でしたが、WebAssemblyのランタイム自体も触っていけるようになると良いですね。


  1. https://zenn.dev/katei/articles/wasm-js-types-polyfill ↩︎

  2. https://github.com/WebAssembly/function-references/blob/main/proposals/function-references/Overview.md#type-reflection ↩︎