WebAssembly JavaScript Interfaceについて - W3C WD要約と所感
はじめに
この記事は、WebAssembly / Wasm Advent Calendar 2024の8日目の記事です。
WebAssembly JavaScript Interfaceとはなにか
W3C Working Draft を読みました。
最近、Google ChromeのV8エンジンに実験的サポートとして組み込まれた JSPI(JavaScript-Promise Integration) について、以下の発表を聴いてから気になっていたのですが、調べていくうちにそもそもWebAssembly JavaScript Interfaceとはなんぞや、に行き着いたため、W3C WDの概要をかい摘みつつ、関連して調べたことをメモ的にまとめてみます。
W3C WDを読む前に : 知っておくとよさそうなこと
WebAssemblyが目指すもの
WebAssembly/design では、高レベル目標を以下と定義しています(要約)
- ポータブルで効率的なバイナリ形式の定義
- 幅広いプラットフォーム(モバイルやIoT)で動作する効率的なバイナリ形式を設計し、ネイティブ速度で動作可能にする
- 段階的な仕様と実装
- C/C++を対象に、MVPを開発する
- 追加機能としてスレッド、例外処理、SIMDなどを導入し、C/C++以外の他言語対応も進める
- 既存のWebプラットフォームと統合し、うまく実行されるようにデザインすること
- feature-tested と後方互換性の維持
- JavaScriptと同じセマンティックな世界で実行する
- JavaScriptとの同期呼び出しを可能にする
- 同一オリジンや権限管理のセキュリティポリシーを強制する
- JavaScriptが利用可能なWeb APIを通じてブラウザ機能にアクセスできるようにする
- バイナリ形式と相互変換可能な人間可読のテキスト形式を定義し、「View Source」機能をサポートする
- ブラウザ以外の実行環境への対応
- ツールサポート
- LLVMバックエンドとclangポートの開発
- 他のコンパイラやツールも支援
そもそも、WebAssembly Interface Typesってなにが嬉しいの?
和訳記事はこちらです。
WebAssemblyとJSが対話するために、解決すべき問題
言語ごとに異なる型システムが存在するのに対して、WebAssemblyとあらゆる言語が連携するためには、どのような選択肢があるのでしょうか。
- WebAssemblyの実行環境からJSへのパイプラインをハードコードする
エンジン(ここでは WebAssemblyの実行環境を指します)内に、 WebAssemblyからJavaScript、そして JavaScriptからWeb IDLへのようなマッピングをハードコードする方法です。
この方法の課題は、各プログラミング言語ごとに特定のマッピングを作成する必要があることです。エンジンがこれらのマッピングを明示的にサポートする必要があり、言語の仕様が変わるたびにそれに合わせて更新しなければならないため、スケーラブルな方法とは言えません。
初期のコンパイラも、各ソース言語から各マシンコード言語へのパイプラインが必要でした。
一方で、現代的なコンパイラのアーキテクチャは、フロントエンドとバックエンドに分かれています。
フロントエンドがソース言語を抽象中間言語 (abstract intermediate language, IR)に変換し、バックエンドがこのIRをターゲットの機械語に変換します。
Web IDL のアイディアに立ち返ります。
ここでいうWeb IDLとは、Webブラウザで実装するインタフェースを記述するために利用する、インタフェース記述言語です。
Web IDLは、Webブラウザ上のほぼすべてのAPI仕様で使用されています。
JS(ここでは、ECMAScript言語仕様[ECMA-262]により定義されるJavaScriptを指す) に特化しており、Web IDLで定義された型や関数がJSにおいてどのような振る舞いをするかも、明確に定義づけられ(マッピングされ)ています。
現在、WebAssemblyとWeb IDLの間にマッピングはありません。そのため、数値などの単純な型の場合でも、関数呼び出しはJSを経由する必要があります。
JSを経由する必要があるため、WebAssemblyからDOMの呼び出しは速度上の課題もあります。
この課題に対処するための取り組みの一つが、GC proposalです。この提案は、WebAssembly内でGCオブジェクト(例えば構造体や配列)を作成し操作できるようにすることを目的としています。一方で、RustやC++のようにガベージコレクションを使わず、線形メモリ領域を利用する言語にとっては、この機能を簡単に活用するのは難しいという課題があります。
これに対して、Web IDLの概念を基にして、より抽象的で柔軟な型システムを提供し、異なるプログラミング言語やランタイム間のデータ交換を効率化することを目指したのが、WebAssembly interface types proposalです。
W3C WD要約
以下から気になった章を何章か抜粋してまとめます。
2. Sample API Usage
WebAssemblyモジュールをインポートしてインスタンス化するためのサンプルコードを見てみます。
fetch('module.wasm')
.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes))
.then(wasmModule => {
const { add } = wasmModule.instance.exports;
console.log(add(1, 2));
})
.catch(err => console.error("Error instantiating WebAssembly module:", err));
fetch()
メソッドで、Wasmモジュール(module.wasm)を取得しています。このファイルは。WebAssemblyバイナリとして保存されています。
次に、arrayBuffer()
を使って、取得したレスポンスをバイナリ形式のデータに変換します。
WebAssembly.instantiate()
メソッドを使って、そのバイナリデータをインスタンス化します。モジュールのバイトコードを解析し、実行可能なインスタンスに変換しています。
インスタンス化された後、wasmModule.instance.exports
からエクスポートされた関数(この場合は add)にアクセスできます。これを使って、Wasm内の関数をJavaScriptから呼び出します。
var importObj = {js: {
import1: () => console.log("hello,"),
import2: () => console.log("world!")
}};
fetch('demo.wasm').then(response =>
response.arrayBuffer()
).then(buffer =>
WebAssembly.instantiate(buffer, importObj)
).then(({module, instance}) =>
instance.exports.f()
);
非同期処理を行うために、then()
メソッドを使って順番に操作を行い、最終的な結果を得ています。このように、WebAssemblyモジュールをJavaScriptからインポートし、インスタンス化して、インスタンス内で定義された関数を呼び出しています。
5. The WebAssembly Namespace
この節では、WebAssemblyにアクセスするための主要なオブジェクトや関数が含まれるNamespaceを定義しています。
これには、WebAssemblyモジュールの作成や、モジュールから関数や変数をインポート/エクスポートするための方法が記述されています。
dictionary WebAssemblyInstantiatedSource {
required Module module;
required Instance instance;
};
[Exposed=*]
namespace WebAssembly {
boolean validate(BufferSource bytes);
Promise<Module> compile(BufferSource bytes);
Promise<WebAssemblyInstantiatedSource> instantiate(
BufferSource bytes, optional object importObject);
Promise<Instance> instantiate(
Module moduleObject, optional object importObject);
};
WebAssembly.instantiate()
では、非同期でWebAssemblyモジュールのインスタンス化を行い、モジュールのエクスポートされた関数を呼び出しています。
7. Implementation-defined Limits
WebAssemblyのコア仕様では、実装がモジュールの構文構造の制限を定義することができます。
各WebAssemblyの実装は独自に制限を定義することができますが、ドキュメント中で説明されている標準のWebAssembly JavaScriptインターフェースでは、以下のような正確な制限を設けています。
The maximum size of a module is 1,073,741,824 bytes (1 GiB).
The maximum number of types defined in the types section is 1,000,000.
The maximum number of functions defined in a module is 1,000,000.
The maximum number of imports declared in a module is 100,000.
The maximum number of exports declared in a module is 100,000.
The maximum number of globals defined in a module is 1,000,000.
The maximum number of data segments defined in a module is 100,000.
The maximum number of tables, including declared or imported tables, is 100,000.
The maximum size of a table is 10,000,000.
The maximum number of table entries in any table initialization is 10,000,000.
The maximum number of memories, including declared or imported memories, is 1.
The maximum number of parameters to any function or block is 1,000.
The maximum number of return values for any function or block is 1,000.
The maximum size of a function body, including locals declarations, is 7,654,321 bytes.
The maximum number of locals declared in a function, including implicitly declared as parameters, is 50,000.
この制限を超えるモジュールに対して、実装はCompileError
を返さなければならないとされています。
実際には、これらの制限を下回ると、有効なモジュールのリソースが不足する可能性があります。
テーブルの最大サイズは10,000,000、メモリの最大ページ数は65,536です
要するに、WebAssemblyの実装は、仕様に基づく制限を守りつつ、実際のリソース制限に応じてモジュールを処理することが求められますが、予測可能性を確保するためには、標準仕様に記載された制限を厳密に守る必要 があります。
おわりに
W3C WDについては、後程少々加筆します(12/6現在)。
Discussion