WebAssembly JavaScript API の使用

これまでに Emscripten などのツールを使用して他の言語からモジュールをコンパイルしたり自分自身のコードを読み込んでして実行したりしました。次のステップは他の WebAssembly JavaScript API の使い方について学ぶことです。この記事では知る必要があることを説明します。

メモ: もし、この記事で説明している基本的な概念がよくわからない場合、 WebAssembly の概要を先に読んでからこの記事に戻ってきてください。

シンプルな例

WebAssembly JavaScript API の使用方法と、wasm モジュールを読み込んでウェブページ内で使用する方法を、ステップバイステップの例を通して実行してみましょう。

メモ: サンプルコードは webassembly-examples GitHub リポジトリーから参照してください。

例の準備

  1. まずは、 wasm モジュールが必要です! simple.wasm をコピーしてローカルマシンの新しいディレクトリーの中に保存してください。

  2. 次に、 wasm ファイルと同じディレクトリーに index.html という名前でシンプルな HTML ファイルを作成しましょう(簡単に利用できるテンプレートを持っていないのであれば、単純なテンプレートが利用できます)。

  3. ここで、何が起こっているのか理解を助けるために、 wasm モジュールのテキスト表現を見てみましょう(WebAssembly 形式から wasm への変換も参照してください)。

    wasm
    (module
      (func $i (import "imports" "imported_func") (param i32))
      (func (export "exported_func")
        i32.const 42
        call $i))
    
  4. 2 行目に 2 階層の名前空間を持つインポートの宣言があります。 — 内部関数 $iimports.imported_func からインポートされています。wasm モジュールにインポートするオブジェクトを記述するときに、この 2 階層の名前空間を JavaScript に反映させる必要があります。 要素を HTML 内に作成して、次のコードを追加してください。

    js
    const importObject = {
      imports: { imported_func: (arg) => console.log(arg) },
    };
    

WebAssembly モジュールをストリーミングする

Firefox 58 の新機能として、 WebAssembly モジュールを基礎となるソースから直接コンパイルおよびインスタンス化する機能があります。これは WebAssembly.compileStreaming()WebAssembly.instantiateStreaming() メソッドを使用して実現します。これらのメソッドは、バイトコードを直接 Module/Instance インスタンスに変換することができるので、ResponseArrayBuffer に別途格納する必要がないため、ストリーミングではない対応するメソッドよりも簡単になっています。

この例(GitHub の instantiate-streaming.html デモや、ライブ版も参照してください)では、 instantiateStreaming() を使って wasm モジュールを取得し、そこに JavaScript 関数をインポートしてコンパイルしてインスタンス化し、そのエクスポート関数にアクセスするまで、すべて一度に行っています。

スクリプトに以下の 1 ブロックを加えてください。

js
WebAssembly.instantiateStreaming(fetch("simple.wasm"), importObject).then(
  (obj) => obj.instance.exports.exported_func(),
);

この結果、エクスポートした WebAssembly の exported_func 関数を呼び出し、インポートした JavaScript の imported_func 関数を呼び出し、WebAssembly インスタンスの中で提供した値 (42) をコンソールに記録することになりました。サンプルのコードを保存して、WebAssembly に対応しているブラウザーで読み込むと、これが実際に動作しているのがわかります。

メモ: これは複雑で長い例のほんの一部ですが、ウェブアプリケーション内で WebAssembly をどのように JavaScript と組み合わせて動作させることができるかを説明しています。別の場所でも言及していますが、 WebAssembly は JavaScript の置き換えを目指しているわけではありません。両方が協力して、お互いの強みを活かすことができます。

ストリーミングせずに wasm モジュールを読み込む

上記のようなストリーミングメソッドを使用できない、または使用したくない場合は、代わりにストリーミングメソッドではない WebAssembly.compile() / WebAssembly.instantiate() を使用することができます。

これらのメソッドはバイトコードに直接アクセスしないので、 wasm モジュールをコンパイル/インスタンス化する前にレスポンスを ArrayBuffer に変換する余分な手順が必要になります。

同等のコードは次のようになります。

js
fetch("simple.wasm")
  .then((response) => response.arrayBuffer())
  .then((bytes) => WebAssembly.instantiate(bytes, importObject))
  .then((results) => {
    results.instance.exports.exported_func();
  });

開発者ツールで wasm を見る

Firefox 54 以降では、開発者ツールのデバッガーパネルでウェブページに含まれる wasm コードのテキスト表現を表示する機能があります。これを表示するためには、デバッガーパネルに移動して、 "wasm://" 項目をクリックしてください。

WebAssembly をテキストとして表示するだけでなく、 WebAssembly のテキスト表現を使用してすぐにデバッグを開始することができます(ブレークポイント、コールスタックの検査、ステップ実行など)。

メモリー

WebAssembly の低レベルのメモリーモデルでは、メモリーは線形メモリーと呼ばれる型のない連続したバイト列として表現され、モジュール内のロード、ストア命令を使用して読み書きされます。このメモリーモデルでは、任意のロード、ストア命令は線形メモリー全体の任意のバイトにアクセスすることができます。これはポインターなどの C/C++ の概念を忠実に表現するために必要なものです。

しかし、利用可能なメモリー範囲がプロセス全体に及ぶネイティブの C/C++ プログラムとは異なり、特定の WebAssembly インスタンスがアクセスできるメモリーは、 WebAssembly Memory オブジェクトが含む特定の(潜在的に非常に小さな)範囲に制限されています。これにより、単一のウェブアプリで複数の独立したライブラリー(それぞれが内部で WebAssembly を使用している)を使用し、互いに完全に分離された個別のメモリーを持つことができます。さらに、新しい実装では共有メモリーを作成することもでき、これは postMessage() によってウィンドウとワーカーコンテキスト間で転送して複数の場所で使用することが可能です。

JavaScript では、Memory インスタンスはリサイズ可能な ArrayBuffer (または共有メモリーの場合は SharedArrayBuffer) とみなすことができます。ArrayBuffer と同様に、単一のウェブアプリケーションで多くの独立した Memory オブジェクトを作成することができます。Memory オブジェクトは初期サイズと最大サイズ (省略可) を指定して、WebAssembly.Memory() コンストラクターから作成することができます。

簡単な例を見ながら、探索を始めましょう。

  1. もう 1 つのシンプルな HTML ページを(単純なテンプレートをコピーして)作成し、 memory.html という名前を付けてください。このページに 要素を追加してください。

  2. メモリーインスタンスを作成するために、次の行をスクリプトに追加します。

    js
    const memory = new WebAssembly.Memory({ initial: 10, maximum: 100 });
    

    initialmaximum の単位は WebAssembly ページです。これらは 64KB に固定されています。上の例では、メモリーインスタンスは初期サイズが 640KB、最大サイズが 6.4MB であることを意味しています。

    WebAssembly メモリーが持つバイト列は ArrayBuffer として buffer ゲッター/セッターから公開されています。例えば、線形メモリーの先頭ワードに直接、 42 を書き込むには次のようにします。

    js
    new Uint32Array(memory.buffer)[0] = 42;
    

    その後で同じ値を返すことができます。

    js
    new Uint32Array(memory.buffer)[0];
    
  3. デモで試してみましょう。これまでに追加した内容を保存してブラウザーで読み込んだ後、JavaScript コンソールで上の 2 行を入力してみてください。

メモリーの拡張

メモリーインスタンスは Memory.prototype.grow() を呼び出すことで拡張することができます。引数は WebAssembly ページ単位で指定します。

js
memory.grow(1);

Memory インスタンスの作成時に最大値が指定していて、この最大値を超えて拡張しようとすると RangeError 例外が発生します。エンジンは提供された上限を利用してメモリーを事前に確保しておくことで、より効率的なリサイズが可能になります。

注: ArrayBuffer の byteLength は変更不可であるため、 Memory.prototype.grow() 操作が成功した後、buffer ゲッターは新しい (新しい byteLength で) ArrayBufferを返します。そして、前の ArrayBuffer は「切り離された状態」になるか、メモリーから切り離されます。

関数と同様に、線形メモリーはモジュール内で定義することもインポートすることもできます。同じようにモジュールは任意でメモリーをエクスポートすることも可能です。これは JavaScript が WebAssembly インスタンスに対して新しく作成した WebAssembly.Memory をインポートで渡したり、Memory のエクスポートから (Instance.prototype.exports を介して) 受け取れることを意味しています。

より複雑なメモリーの例

より複雑なメモリーの例を見て、上記のことを明確にしましょう。先に定義したメモリーインスタンスをインポートし、それを整数の配列で埋め込んで、それらを合計する WebAssembly モジュールです。これは memory.wasm で見ることができます。

  1. memory.wasm のローカルコピーを以前と同じディレクトリーに作成します。

    メモ: モジュールのテキスト表現は memory.wat を参照してください。

  2. memory.html サンプルファイルに戻って、以前と同じように wasm モジュールを読み取り、コンパイル、インスタンス化します。以下のものをスクリプトの最後に追加してください。

    js
    WebAssembly.instantiateStreaming(fetch("memory.wasm"), {
      js: { mem: memory },
    }).then((results) => {
      // ここにコードを追加
    });
    
  3. このモジュールはモジュール内部のメモリーをエクスポートします。instance という名前でモジュールの Instance が取得され、エクスポートされた関数 accumulate() を使用してモジュールの線形メモリー (mem) に直接入力された配列を合計する事ができます。指定された場所に、次のコードを追加してみましょう。

    js
    const i32 = new Uint32Array(memory.buffer);
    
    for (let i = 0; i < 10; i++) {
      i32[i] = i;
    }
    
    const sum = results.instance.exports.accumulate(0, 10);
    console.log(sum);
    

Memory オブジェクト自体でなく、Memory オブジェクトの buffer (Memory.prototype.buffer) から Uint32Array ビューを作成していることに注意してください。

メモリーのインポートは関数のインポートと同じように機能します。JavaScript 関数の代わりに Memory オブジェクトを渡すだけです。メモリーのインポートは 2 つの理由で役に立ちます。

  • モジュールをコンパイルする前、もしくは並行して、メモリーの初期コンテンツを JavaScript で読み取り、または作成することができます。
  • 単一の Memory オブジェクトを複数のモジュールインスタンスにインポートすることができます。これは WebAssembly で動的リンクを実装するための重要な構成要素です。

メモ: 完全なデモは memory.html (動作例) を参照してください。

テーブル

WebAssembly Table は JavaScript と WebAssembly コードの両方でアクセスできるリサイズ可能な 参照 の型付き配列です。Memory はリサイズ可能な生のバイト列を提供しますが、参照はエンジンに保証された値(このバイト列は安全性、移植性、安定性の理由からコンテンツによって直接読み書きしてはいけない)であるため、参照を格納するために使用することは安全ではありません。

テーブルは要素の型を持ち、テーブルに格納できる参照の型が制限されます。WebAssembly の現バージョンでは WebAssembly コード内で必要な参照の型は関数型の1つだけです。そして、これが唯一の正しい要素の型となります。将来のバージョンでは、さらに多くの要素の型が追加される予定です。

関数参照は関数ポインターを持つ C/C++ のような言語をコンパイルするために必要です。C/C++ のネイティブ実装では、関数ポインターはプロセスの仮想アドレス空間内の関数のコードの生のアドレスで表現されるため、安全性の理由から線形メモリーに直接格納することはできません。代わりに、関数参照はテーブルに格納されます。整数値のインデックスは線形メモリーに格納することができます。

関数ポインターを呼び出すときは、WebAssembly を呼び出す側でインデックスを指定します。インデックスを付けたり、インデックス付けされた関数参照を呼び出す前に安全な境界のチェックをすることができます。したがって、テーブルは現在、安全かつ移植可能に低レベルのプログラミング言語の機能をコンパイルするために使用される、低レベルのプリミティブです。

テーブルは Table.prototype.set() を通してテーブル内の値を1つ更新することができます。さらに、Table.prototype.grow() でテーブルに格納できる値の数を増やすことができます。時間の経過とともに間接呼び出しされる関数を変更することを許容し、これは 動的リンク技術 のために必要なものです。変化した値に対して JavaScript では Table.prototype.get() を通してすぐにアクセスできます。wasm モジュールからも同様です。

テーブルの例

テーブルのシンプルな例を見てみましょう。紹介する WebAssembly モジュールは2つの要素 (要素0は13、要素1は42を返します) を持つテーブルをエクスポートするものです。モジュールは table.wasm から見つけられます。

  1. table.wasm をローカルの新しいディレクトリーにコピーします。

    メモ: このモジュールのテキスト表現は table.wat を参照してください。

  2. HTML templatetable.html という名前で同じディレクトリーにコピーします。

  3. 前と同じように、wasm モジュールを読み取り、コンパイル、インスタンス化します。次のコードを HTML の body の末尾の 要素を HTML 内に作成して、次のコードを追加してください。\n

    js
    const importObject = {\n  imports: { imported_func: (arg) => console.log(arg) },\n};\n\n\n"}},{"type":"prose","value":{"id":"webassembly_モジュールをストリーミングする","title":"WebAssembly モジュールをストリーミングする","isH3":true,"content":"

    Firefox 58 の新機能として、 WebAssembly モジュールを基礎となるソースから直接コンパイルおよびインスタンス化する機能があります。これは WebAssembly.compileStreaming() と WebAssembly.instantiateStreaming() メソッドを使用して実現します。これらのメソッドは、バイトコードを直接 Module/Instance インスタンスに変換することができるので、Response を ArrayBuffer に別途格納する必要がないため、ストリーミングではない対応するメソッドよりも簡単になっています。\n

    この例(GitHub の instantiate-streaming.html デモや、ライブ版も参照してください)では、 instantiateStreaming() を使って wasm モジュールを取得し、そこに JavaScript 関数をインポートしてコンパイルしてインスタンス化し、そのエクスポート関数にアクセスするまで、すべて一度に行っています。\n

    スクリプトに以下の 1 ブロックを加えてください。\n

    js
    WebAssembly.instantiateStreaming(fetch(\"simple.wasm\"), importObject).then(\n  (obj) => obj.instance.exports.exported_func(),\n);\n\n

    この結果、エクスポートした WebAssembly の exported_func 関数を呼び出し、インポートした JavaScript の imported_func 関数を呼び出し、WebAssembly インスタンスの中で提供した値 (42) をコンソールに記録することになりました。サンプルのコードを保存して、WebAssembly に対応しているブラウザーで読み込むと、これが実際に動作しているのがわかります。\n

    \n

    メモ:\nこれは複雑で長い例のほんの一部ですが、ウェブアプリケーション内で WebAssembly をどのように JavaScript と組み合わせて動作させることができるかを説明しています。別の場所でも言及していますが、 WebAssembly は JavaScript の置き換えを目指しているわけではありません。両方が協力して、お互いの強みを活かすことができます。\n"}},{"type":"prose","value":{"id":"ストリーミングせずに_wasm_モジュールを読み込む","title":"ストリーミングせずに wasm モジュールを読み込む","isH3":true,"content":"

    上記のようなストリーミングメソッドを使用できない、または使用したくない場合は、代わりにストリーミングメソッドではない WebAssembly.compile() / WebAssembly.instantiate() を使用することができます。\n

    これらのメソッドはバイトコードに直接アクセスしないので、 wasm モジュールをコンパイル/インスタンス化する前にレスポンスを ArrayBuffer に変換する余分な手順が必要になります。\n

    同等のコードは次のようになります。\n

    js
    fetch(\"simple.wasm\")\n  .then((response) => response.arrayBuffer())\n  .then((bytes) => WebAssembly.instantiate(bytes, importObject))\n  .then((results) => {\n    results.instance.exports.exported_func();\n  });\n"}},{"type":"prose","value":{"id":"開発者ツールで_wasm_を見る","title":"開発者ツールで wasm を見る","isH3":true,"content":"

    Firefox 54 以降では、開発者ツールのデバッガーパネルでウェブページに含まれる wasm コードのテキスト表現を表示する機能があります。これを表示するためには、デバッガーパネルに移動して、 \"wasm://\" 項目をクリックしてください。\n

    \"\"\n

    WebAssembly をテキストとして表示するだけでなく、 WebAssembly のテキスト表現を使用してすぐにデバッグを開始することができます(ブレークポイント、コールスタックの検査、ステップ実行など)。"}},{"type":"prose","value":{"id":"メモリー","title":"メモリー","isH3":false,"content":"

    WebAssembly の低レベルのメモリーモデルでは、メモリーは線形メモリーと呼ばれる型のない連続したバイト列として表現され、モジュール内のロード、ストア命令を使用して読み書きされます。このメモリーモデルでは、任意のロード、ストア命令は線形メモリー全体の任意のバイトにアクセスすることができます。これはポインターなどの C/C++ の概念を忠実に表現するために必要なものです。\n

    しかし、利用可能なメモリー範囲がプロセス全体に及ぶネイティブの C/C++ プログラムとは異なり、特定の WebAssembly インスタンスがアクセスできるメモリーは、 WebAssembly Memory オブジェクトが含む特定の(潜在的に非常に小さな)範囲に制限されています。これにより、単一のウェブアプリで複数の独立したライブラリー(それぞれが内部で WebAssembly を使用している)を使用し、互いに完全に分離された個別のメモリーを持つことができます。さらに、新しい実装では共有メモリーを作成することもでき、これは postMessage() によってウィンドウとワーカーコンテキスト間で転送して複数の場所で使用することが可能です。\n

    JavaScript では、Memory インスタンスはリサイズ可能な ArrayBuffer (または共有メモリーの場合は SharedArrayBuffer) とみなすことができます。ArrayBuffer と同様に、単一のウェブアプリケーションで多くの独立した Memory オブジェクトを作成することができます。Memory オブジェクトは初期サイズと最大サイズ (省略可) を指定して、WebAssembly.Memory() コンストラクターから作成することができます。\n

    簡単な例を見ながら、探索を始めましょう。\n

      \n
    1. \n

      もう 1 つのシンプルな HTML ページを(単純なテンプレートをコピーして)作成し、 memory.html という名前を付けてください。このページに 要素を追加してください。\n\n

    2. \n

      メモリーインスタンスを作成するために、次の行をスクリプトに追加します。\n

      js
      const memory = new WebAssembly.Memory({ initial: 10, maximum: 100 });\n\n

      initial と maximum の単位は WebAssembly ページです。これらは 64KB に固定されています。上の例では、メモリーインスタンスは初期サイズが 640KB、最大サイズが 6.4MB であることを意味しています。\n

      WebAssembly メモリーが持つバイト列は ArrayBuffer として buffer ゲッター/セッターから公開されています。例えば、線形メモリーの先頭ワードに直接、 42 を書き込むには次のようにします。\n

      js
      new Uint32Array(memory.buffer)[0] = 42;\n\n

      その後で同じ値を返すことができます。\n

      js
      new Uint32Array(memory.buffer)[0];\n\n\n
    3. \n

      デモで試してみましょう。これまでに追加した内容を保存してブラウザーで読み込んだ後、JavaScript コンソールで上の 2 行を入力してみてください。\n\n"}},{"type":"prose","value":{"id":"メモリーの拡張","title":"メモリーの拡張","isH3":true,"content":"

      メモリーインスタンスは Memory.prototype.grow() を呼び出すことで拡張することができます。引数は WebAssembly ページ単位で指定します。\n

      js
      memory.grow(1);\n\n

      Memory インスタンスの作成時に最大値が指定していて、この最大値を超えて拡張しようとすると RangeError 例外が発生します。エンジンは提供された上限を利用してメモリーを事前に確保しておくことで、より効率的なリサイズが可能になります。\n

      注: ArrayBuffer の byteLength は変更不可であるため、 Memory.prototype.grow() 操作が成功した後、buffer ゲッターは新しい (新しい byteLength で) ArrayBufferを返します。そして、前の ArrayBuffer は「切り離された状態」になるか、メモリーから切り離されます。\n

      関数と同様に、線形メモリーはモジュール内で定義することもインポートすることもできます。同じようにモジュールは任意でメモリーをエクスポートすることも可能です。これは JavaScript が WebAssembly インスタンスに対して新しく作成した WebAssembly.Memory をインポートで渡したり、Memory のエクスポートから (Instance.prototype.exports を介して) 受け取れることを意味しています。"}},{"type":"prose","value":{"id":"より複雑なメモリーの例","title":"より複雑なメモリーの例","isH3":true,"content":"

      より複雑なメモリーの例を見て、上記のことを明確にしましょう。先に定義したメモリーインスタンスをインポートし、それを整数の配列で埋め込んで、それらを合計する WebAssembly モジュールです。これは memory.wasm で見ることができます。\n

        \n
      1. \n

        memory.wasm のローカルコピーを以前と同じディレクトリーに作成します。\n

      2. \n

        memory.html サンプルファイルに戻って、以前と同じように wasm モジュールを読み取り、コンパイル、インスタンス化します。以下のものをスクリプトの最後に追加してください。\n

        js
        WebAssembly.instantiateStreaming(fetch(\"memory.wasm\"), {\n  js: { mem: memory },\n}).then((results) => {\n  // ここにコードを追加\n});\n\n\n
      3. \n

        このモジュールはモジュール内部のメモリーをエクスポートします。instance という名前でモジュールの Instance が取得され、エクスポートされた関数 accumulate() を使用してモジュールの線形メモリー (mem) に直接入力された配列を合計する事ができます。指定された場所に、次のコードを追加してみましょう。\n

        js
        const i32 = new Uint32Array(memory.buffer);\n\nfor (let i = 0; i < 10; i++) {\n  i32[i] = i;\n}\n\nconst sum = results.instance.exports.accumulate(0, 10);\nconsole.log(sum);\n\n\n\n

        Memory オブジェクト自体でなく、Memory オブジェクトの buffer (Memory.prototype.buffer) から Uint32Array ビューを作成していることに注意してください。\n

        メモリーのインポートは関数のインポートと同じように機能します。JavaScript 関数の代わりに Memory オブジェクトを渡すだけです。メモリーのインポートは 2 つの理由で役に立ちます。\n