WebAssembly JavaScript API を使用する

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

: もし、この記事で説明している基本的なコンセプトがよくわからない場合、WebAssemblyのコンセプト をはじめに読んでからこの記事に戻ってきてください。

シンプルな例

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

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

例を準備する

  1. まずは、wasm モジュールが必要です! simple.wasm をコピーしてローカルマシンの新しいディレクトリの中に保存します。
  2. 次に、使用しているブラウザが WebAssembly に対応しているか確認します。Firefox 52+ と Chrome 57+ では WebAssembly がデフォルトで有効になっています。
  3. 次に、wasm ファイルと同じディレクトリに index.html という名前でシンプルな HTML ファイルを作成しましょう (もしも簡単に利用できるテンプレートを持っていない場合、simple template を使用できます) 。
  4. ここで、何が起こっているのか理解を助けるために、wasm モジュールのテキスト表現を見てみましょう (テキストフォーマットから wasm に変換する も参照してください):
    (module
      (func $i (import "imports" "imported_func") (param i32))
      (func (export "exported_func")
        i32.const 42
        call $i))
  5. 2行目に2階層の名前空間を持つインポートの宣言があります — 内部関数 $i は imports.imported_func からインポートされています。wasm モジュールにインポートするオブジェクトを記述するときに、この2階層の名前空間を JavaScript に反映させる必要があります。<script></script> 要素を HTML 内に作成して、次のコードを追加します:
    var importObject = {
      imports: {
          imported_func: function(arg) {
            console.log(arg);
          }
        }
      };

上で説明したように、 imports.imported_func でインポート機能を利用できます。

ES6のアローファンクション を使用するとより簡潔に書くことができます:

var importObject = { imports: { imported_func: arg => console.log(arg) } };

スタイルはあなたが好きなものを選んでください。

wasm モジュールをロードして使用する

インポートオブジェクトを用意して、wasm ファイルをフェッチして、ArrayBuffer に変換して、その後にエクスポートされた関数を使用します。

スクリプトに次のコードを追加します。以下は最初のブロックです:

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

: WebAssemblyコードのロードと実行 でどのように機能するか詳しく説明しました。自身が無い場合、思い出すために参照してください。

この結果、エクスポートされた WebAssembly 関数 exported_func を呼び出すとインポートされた JavaScript 関数 imported_func が呼び出され、WebAssembly インスタンス内の提供される値 (42) がコンソールに表示されます。コード例を保存して WebAssembly をサポートするブラウザで読み込むと 実際に動作が確認できるでしょう!

: WebAssembly は Firefox 52+ と Chrome 57+/最新のOpera でデフォルトで有効になっています (Firefox 47+ で about:config で javascript.options.wasm フラグを有効化するか、Chrome (51+) と Opera (38+) で chrome://flags の Experimental WebAssembly フラグを有効化することによって動作させることができます)

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

デベロッパーツールで wasm を見る

Firefox 54+ では、デベロッパーツールのデバッガパネルでウェブページに含まれる wasm コードのテキスト表現を表示する機能があります。これを表示するためには、デバッガパネルに移動して、“xxx > wasm” エントリをクリックしてください。

Firefox で、WebAssembly をテキストとして表示することに加えて、開発者は WebAssembly のテキスト表現を使用してすぐにデバッグを開始することができます (ブレークポイント、コールスタックの検査、ステップ実行など) 。WebAssembly debugging with Firefox DevTools の動画を参照してください。

メモリ

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

ただし、ネイティブの C/C++ プログラムでは使用可能なメモリ範囲がプロセス全体に及ぶ一方、個別の WebAssembly Instance がアクセス可能なメモリは、特定の WebAssembly Memory オブジェクトの (潜在的にとても小さい) 範囲だけになります。これにより単一のウェブアプリケーションで複数の独立した (WebAssembly を内部的に使用している) ライブラリが完全に分離された別々のメモリを持つことができます。

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

簡単な例を見て探索を始めましょう。

  1. memory.html という名前の新しいシンプルな HTMLページ を作成します (simple template をコピーしてください) 。<script></script> をページに追加します。

  2. Memory インスタンスを作成するために次の1行をスクリプトに追加します:

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

    initial と maximum は WebAssembly ページを1単位 (64KBに固定されています) とします。上の例では、Memory インスタンスは初期サイズが640KB、最大サイズが6.4MBを意味しています。

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

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

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

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

メモリを拡張する

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

memory.grow(1);

Memory インスタンスの作成時に最大値が指定していて、この最大値を超えて拡張しようとすると WebAssembly.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 モジュールをフェッチ、コンパイル、インスタンス化します (以下をスクリプトに追加してください):

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

    var i32 = new Uint32Array(results.instance.exports.mem.buffer);
    for (var i = 0; i < 10; i++) {
      i32[i] = i;
    }
    
    var 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 (動作例) を参照してください。このバージョンでは fetchAndInstantiate() 関数を使用しています。

テーブル

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 template を table.html という名前で同じディレクトリにコピーします。

  3. 前と同じように、wasm モジュールをフェッチ、コンパイル、インスタンス化します。次のコードを HTML の body の末尾の <script> 要素に追加します:

    fetch('table.wasm').then(response =>
      response.arrayBuffer()
    ).then(bytes =>
      WebAssembly.instantiate(bytes)
    ).then(results => {
      // add your code here
    });
  4. 今度はテーブル内のデータにアクセスしてみましょう。コードの指定された場所に次の行を追加します:

    var tbl = results.instance.exports.tbl;
    console.log(tbl.get(0)());  // 13
    console.log(tbl.get(1)());  // 42

このコードはテーブルに格納されている各関数参照に順番にアクセスし、内包した値をコンソールに書き出すためにインスタンス化します。Table.prototype.get() で各関数参照を取得した後、関数を実行するためには括弧を追加することに注意してください。

: 完全なデモは table.html (動作例) を参照してください。このバージョンでは fetchAndInstantiate() 関数を使用しています。

重複度

ここでは、主な WebAssembly の構成要素のデモを見てきました。これは重複度の概念に言及するのに適しています。これはアーキテクチャ効率の点で多くの進歩がもたらされます:

  • 1つの関数がN個のクロージャを生成するのと同じく、1つのモジュールはN個のインスタンスを持つことができます。
  • 1つのモジュールインスタンスは0から1つのメモリインスタンスを持つことができます。これは"アドレス空間"を提供します。WebAssembly の将来のバージョンでは1モジュールにつき0からN個のメモリインスタンスを許容する可能性があります (Multiple Tables and Memories を参照してください) 。
  • 1つのモジュールインスタンスは0から1つのテーブルインスタンスを持つことができます。これはインスタンスの"関数アドレス空間"で、C言語の関数ポインタを実装するために使用されます。WebAssembly の将来のバージョンでは1モジュールにつき0からN個のメモリインスタンスを許容する可能性があります。
  • 1つのメモリ、テーブルは0からN個のモジュールから使用することができます。複数のインスタンス全てが同じアドレス空間を共有でき、これは 動的リンク を可能にします。

Understanding text format article の記事で重複度の働きについてみることができます。その中の Mutating tables and dynamic linking の章を見てください (TBD)。

まとめ

この記事では WebAssembly JavaScript API の基本的な使い方について説明しました。WebAssembly モジュールを JavaScript のコンテキストに組み込む方法、その関数を使えるようすること、JavaScript でのメモリとテーブルの使い方について。さらに、多重度の概念についても触れました。

関連情報

ドキュメントのタグと貢献者

 このページの貢献者: syu_kato
 最終更新者: syu_kato,