WebAssembly テキストフォーマットを理解する

人間が WebAssembly を読んだり編集するための wasm バイナリ形式のテキスト表現が存在します。これはテキストエディタ、ブラウザの開発者ツールなどで見せるために設計された中間表現です。この記事では、テキスト形式のしくみ、生の構文、および元のバイトコードの表現との関係(と JavaScript で wasm を表現したラッパーオブジェクト)について説明します。

: この記事は、あなたがウェブ開発者で wasm モジュールをページにロードしてコード内で使用するだけなら過剰なものかもしれません (WebAssembly JavaScript API を使用する を参照)。しかし、例えば、パフォーマンスを最適化するために wasm モジュールを書きたいときや、あなた自身で WebAssembly コンパイラを作るときに役に立ちます。

S式

バイナリ、テキスト形式の両方で、WebAssembly の基本的なコードの単位はモジュールです。テキスト形式ではモジュールは1つの大きなS式として表現されます。S式はツリー構造を表現するための非常に古くてシンプルなテキスト形式で、モジュールはモジュールの構造とそのコードを記述するノードツリーとして考えることができます。しかし、プログラミング言語のAST (中小構文木) とは異なり、WebAssembly のツリーはかなり平坦で、ほとんどは命令の列で構成されています。

はじめに、S式がどういうものか見てみましょう。ツリー内の各ノードは1組の括弧内に入れられます — ( ... )。  括弧内の最初のラベルは、それがどのノードタイプかを示し、スペースで区切られた属性、または子ノードのリストが続きます。次のコードは WebAssembly のS式を意味します:

(module (memory 1) (func))

ルートノード "module" と2つの子ノード、"1" を属性に持つ "memory" ノード、"func" ノードを表します。これらのノードが実際にどういう意味なのかを見ていきましょう。

最もシンプルなモジュール

最もシンプルで短い実行可能な wasm モジュールから始めてみましょう。

(module)

このモジュールは完全に空ですが、モジュールとしては有効です。

いま、このモジュールをバイナリに変換すると (WebAssembly テキストフォーマットから wasm に変換する を参照) 、 バイナリ形式 で記述された8バイトのモジュールヘッダだけになります:

0000000: 0061 736d              ; WASM_BINARY_MAGIC
0000004: 0d00 0000              ; WASM_BINARY_VERSION

モジュールに機能を追加する

Ok、これは全然面白くないですね。モジュールに実行可能なコードを追加していきましょう。

全ての WebAssembly モジュール内のコードは次の擬似コード構造を持つ関数にグループ化されます:

( func <signature> <locals> <body> )
  • signature は関数が何を受け取る (引数) かと何を返す (戻り値) かを宣言します。
  • locals は JavaScript でいうと変数のようなものですが、明示的な型が宣言されます。
  • body は線形な低レベルな命令列です。

S式であるために違って見えますが、これは、他の言語の関数に似ています。

シグネチャとパラメータ

シグネチャは戻り値の型宣言のリストが後に続く、パラメータの型宣言のシーケンスです。ここで注目すべきは:

  • 結果がない場合、関数は何も返しません。
  • 現在は、最大で1つの戻り値を返すことができますが、任意の数に緩和される予定 です。

各パラメータは明示的に宣言された型を持ちます。wasm では現在4つの型が有効です:

  • i32: 32ビット整数
  • i64: 64ビット整数
  • f32: 32ビット浮動小数点数
  • f64: 64ビット浮動小数点数

単体のパラメータは (param i32) 、戻り値は (result i32) のように書きます。したがって、2つの32ビット整数を引数にとり、64ビット浮動小数点数を返すバイナリ関数は次のように記述します:

(func (param i32) (param i32) (result f64) ... )

シグネチャのあとに型付けされたローカル変数のリストが続きます (例: (local i32)) 。 パラメータは基本的には呼び出し元から渡された、対応する引数の値で初期化された、ただのローカル変数です。

ローカル変数とパラメータを取得/設定する

ローカル変数とパラメータは関数本体から get_local と set_local 命令を使用して読み書きすることができます。

get_local/set_local コマンドは数値のインデックスから取得/設定される項目を参照します。最初にパラメータが宣言順に、その後に、ローカル変数が宣言順に参照されます。次の関数を見てください:

(func (param i32) (param f32) (local f64)
  get_local 0
  get_local 1
  get_local 2)

命令 get_local 0 は i32 のパラメータ, get_local 1 は f32 のパラメータ、そして get_local 2 は f64 のローカル変数を取得します。

ここで別の問題があります。数値のインデックスを使用して項目を参照すると、混乱したり、困ってしまうことがあります。そこで、テキストフォーマットでは、単純に型宣言の直前に ($) をプレフィックスとして付けた名前を、パラメータ、ローカル変数や他の多くの項目につけることができます。

したがって、上記のシグネチャを次のように書き直すことができます:

(func (param $p1 i32) (param $p2 f32) (local $loc i32) …)

そして、get_local 0 の代わりに get_local $p1 と書くことができるようになります (このテキストがバイナリに変換されたとき、バイナリには整数値だけが残されることに注意してください) 。

スタックマシン

関数本体を書く前に、もう1つ、スタックマシンについて話をする必要があります。ブラウザはそれを更に効率的な形にコンパイルしますが、wasm の実行はスタックマシンとして定義されます。スタックマシンの基本的なアイデアは全ての命令がスタックから特定の数の i32/i64/f32/f64 値をプッシュ、ポップするようにすることです。

例えば、get_local はローカル変数の値をスタックにプッシュするように定義されます。そして、i32.add は2つの i32 値 (スタックにプッシュされた前の2つの値を暗黙的に取得します) をポップし、合計を計算して (2^32 の剰余として) 結果の i32 値をプッシュします。

関数が呼び出されたとき、空のスタックから開始され、徐々に積まれてゆき、本体の命令が実行されると空になります。例として、次の関数の実行後について見てみましょう:

(func (param $p i32)
  get_local $p
  get_local $p
  i32.add)

スタックには i32.add よって処理された式 ($p + $p) の結果として、ただ1つの i32 値が積まれています。関数の戻り値はスタックに残った最後の値になります。

WebAssembly のバリデーションルールはスタックが正確に一致することを保証します。もし、(result f32) と宣言した場合、最終的にスタックに1つだけ f32 値が積まれている状態である必要があります。結果の型がない場合は、スタックは空でなければなりません。

はじめての関数本体

前述の通り、関数本体は関数が呼び出された後に続く単純な命令列です。 これまでに学んだことと一緒にして、最終的にはシンプルな関数を含むモジュールを定義することができるようになります:

(module
  (func (param $lhs i32) (param $rhs i32) (result i32)
    get_local $lhs
    get_local $rhs
    i32.add))

この関数は2つのパラメータを受け取って、それらを足して、その結果を返します。

関数本体に置けるものはもっとたくさんありますが、いまはシンプルなもので始めます。進むにつれてもっと多くの例を見ていきます。全ての有効なオペコードのリストについては webassembly.org Semantics reference を調べてみてください。

関数を呼び出す

私達が定義した関数は自身では大したことはしません。いまはそれを呼び出す必要があります。どのようにすればよいでしょうか? ES2015 モジュールのように、wasm 関数はモジュール内の export ステートメントによって明示的にエクスポートしなくてはいけません。

ローカル変数と同じように、関数もデフォルトではインデックスで識別されますが、便宜上の関数名を付けることができます。func キーワードの直後にドル記号で始まる名前を付けてみましょう:

(func $add … )

ここでエクスポート宣言を追加する必要があります。次のようになります:

(export "add" (func $add))

ここで add は JavaScript で認識される関数名であるのに対して、$add はモジュール内の、どの WebAssembly 関数をエクスポートするのかを選択します。

最終的なモジュール (いまのところ) は次のようになります:

(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
    get_local $lhs
    get_local $rhs
    i32.add)
  (export "add" (func $add))
)

例に従うなら、上のモジュールを add.wat という名前で保存して、wabt を使用して (詳細は WebAssembly テキストフォーマットから wasm に変換する を参照してください) 、add.wasm というファイルに変換します。

次に、addCode という名前の型付き配列にバイナリをロードし (WebAssembly コードのロードと実行 で説明されています) 、コンパイル、インスタンス化して、JavaScript で add 関数を実行します (add() はインスタンスの exports プロパティから見つけることができます):

fetchAndInstantiate('add.wasm').then(function(instance) {
   console.log(instance.exports.add(1, 2));  // "3"
});

// fetchAndInstantiate() found in wasm-utils.js
function fetchAndInstantiate(url, importObject) {
  return fetch(url).then(response =>
    response.arrayBuffer()
  ).then(bytes =>
    WebAssembly.instantiate(bytes, importObject)
  ).then(results =>
    results.instance
  );
}

: この例は Github の add.html (動作例)から参照してください。instantiate 関数についての詳細は (WebAssembly.instantiate()) 、fetchAndInstantiate() のソースコードについて (wasm-utils.js) もご確認ください。

基礎を探る

ここでは実際の基本的な例を取り上げてから、いくつかの高度な機能について見てみましょう。

同じモジュールの他の関数から関数を呼び出す

call 命令はインデックスか名前を指定して単一の関数を呼び出します。例えば、次のモジュールには2つの関数が含まれています。1つ目はただ42を返すだけ、もう1つは1つ目のものに1を足した値を返します:

(module
  (func $getAnswer (result i32)
    i32.const 42)
  (func (export "getAnswerPlus1") (result i32)
    call $getAnswer
    i32.const 1
    i32.add))

: i32.const は32ビット整数を定義してスタックにプッシュするだけです。 i32 以外の有効な型に変えて、const の値を好きなものに変えることができます (ここでは 42 に設定しました)。

この例で、あなたは func の直後に宣言された (export "getAnswerPlus1") セクションに気づくでしょう。これはこの関数をエクスポートするための宣言をして、さらにそれに名前をつけるために使用するショートカットです。

これは、上で行ったように、モジュール内の関数外の別の場所で、関数ステートメントと分けて定義するのと同等の機能です:

(export "getAnswerPlus1" (func $functionName))

上のモジュールを呼び出す JavaScript コードは次のようになります:

fetchAndInstantiate('call.wasm').then(function(instance) {
  console.log(instance.exports.getAnswerPlus1());  // "43"
});

: この例は GitHub の call.html (動作例) から参照してください。 2回目ですが、fetchAndInstantiate() のソースは wasm-utils.js を参照してください。

JavaScript から関数をインポートする

すでに、JavaScript から WebAssembly 関数を呼び出すことについては確認しましたが、WebAssembly から JavaScript 関数を呼び出すことについてはどうでしょうか? WebAssembly は実際に JavaScript のビルトインの情報を持っていませんが、JavaScript か wasm 関数をインポートするための一般的な方法があります。例を見てみましょう:

(module
  (import "console" "log" (func $log (param i32)))
  (func (export "logIt")
    i32.const 13
    call $log))

WebAssembly は2階層の名前空間のインポートステートメントを持ちます。ここでは、console モジュールから log 関数をインポートすることを要求しています。また、エクスポートされた logIt 関数から、上で紹介した call 命令を使用して、インポートされた関数を呼ぶ出すことができます。

インポートされた関数は通常の関数と同じようなものです。WebAssembly のバリデーションによって静的にチェックするシグネチャを持ち、インデックスか名前を付けて呼び出すことができます。

JavaScript 関数にはシグネチャの概念がないため、インポート宣言のシグネチャに関係なく、どの JavaScript 関数も渡すことができます。モジュールがインポート宣言をすると、 WebAssembly.instantiate() を呼び出す側は、対応したプロパティを持ったインポートオブジェクトを渡す必要があります。

上の場合、 importObject.console.log が JavaScript 関数であるようなオブジェクト(importObject と呼びましょう) が必要になります。

これは次のようになります:

var importObject = {
  console: {
    log: function(arg) {
      console.log(arg);
    }
  }
};

fetchAndInstantiate('logger.wasm', importObject).then(function(instance) {
  instance.exports.logIt();
});

: この例は GitHub の logger.html (動作例)を参照してください。

WebAssembly メモリ

上の例はとてもひどいロギング関数です。たった1つの整数値を表示するだけです! 文字列を表示するためにはどうしたらよいでしょうか? 文字列やさらに複雑なデータ型を扱うために WebAssembly は メモリ を提供します。WebAssembly によると、メモリは徐々に拡張することのできるただの大きなバイト列です。WebAssembly には 線形メモリ から読み書きするための i32.load や i32.store のような命令を持っています。

JavaScript から見ると、メモリは全て1つの大きな (リサイズ可能な) ArrayBuffer の内部にあるように見えます。それはまさに、asm.js とともに動かさなければならないもの全てです (ただしリサイズは出来ません。asm.js の プログラミングモデル を参照してください) 。

したがって、文字列は線形メモリ内部のどこかに存在するただのバイト列です。適切なバイト列の文字列をメモリに書き込んだとしましょう。その文字列をどのように JavaScript に渡すのでしょうか?

鍵は WebAssembly.Memory() インターフェースを使用して JavaScript から WebAssembly の線形メモリを作成し、関連するインスタンスメソッドを使用して既存の Memory インスタンス (現在は1モジュールごとに1つだけ持つことができます) にアクセスできることです。Memory インスタンスは buffer ゲッターを持ち、これは線形メモリ全体を指し示す ArrayBuffer を返します。

Memory インスタンスは、例えば JavaScript から Memory.grow() メソッドを使用して拡張することもできます。拡張したとき、ArrayBuffer はサイズを変更することができないため、現在の ArrayBuffer は切り離され、新しく作成された、より大きな ArrayBuffer を指し示すようになります。これは、JavaScript に文字列を渡すために必要なことは、線形メモリ内での文字列のオフセットと長さを指定する方法を渡すことだけであることを意味します。

文字列自身に文字列の長さの情報をエンコードするさまざまな方法 (例えば、C言語の文字列) がありますが、簡単にするためにここではオフセットと長さの両方をパラメータとして渡します:

(import "console" "log" (func $log (param i32) (param i32)))

JavaScript 側では、バイト列を簡単に JavaScript 文字列にデコードするために TextDecoder API を使用することができます (ここでは utf8 を指定していますが、他の多くのエンコーディングをサポートしています) 。

consoleLogString(offset, length) {
  var bytes = new Uint8Array(memory.buffer, offset, length);
  var string = new TextDecoder('utf8').decode(bytes);
  console.log(string);
}

最後のに欠けているのは、 consoleLogString が WebAssembly の memory にアクセスできる場所です。このあたり WebAssembly は柔軟です。JavaScript から Memory オブジェクトを作成して WebAssembly モジュールでメモリをインポートするか、WebAssembly モジュールでメモリを作成して JavaScript で使用するためにエクスポートすることができます。

簡単にするために、JavaScript で作成したメモリを WebAssembly にインポートしてみましょう。import ステートメントは次のようになります:

(import "js" "mem" (memory 1))

1 はインポートされたメモリに少なくとも1ページ分のメモリが必要であることを示します(WebAssembly では1ページを 64KB と定義しています)。

文字列 "Hi" を出力する完全なモジュールを見てみましょう。通常のコンパイルされたCのプログラムでは文字列にメモリを割り当てる関数を呼び出しますが、ここでは独自のアセンブリを書くだけで、全ての線形メモリを所有しているので、data セクションを使用してグローバルメモリに文字列の内容を書きこむことができます。データセクションではインスタンス化時にオフセットを指定してバイト列の文字列を書きこむことができます。これはネイティブの実行可能形式の .data セクションに似ています。

最終的な wasm モジュールは次のようになります:

(module
  (import "console" "log" (func $log (param i32 i32)))
  (import "js" "mem" (memory 1))
  (data (i32.const 0) "Hi")
  (func (export "writeHi")
    i32.const 0  ;; pass offset 0 to log
    i32.const 2  ;; pass length 2 to log
    call $log))

: 上記の2重のセミコロン構文 (;;) は WebAssembly ファイル内でコメントを書くためのものです。

ここで、JavaScript から 1ページ分のサイズを持つ Memory を作成してそれに渡すことができます。結果としてコンソールに "Hi" と出力されます:

var memory = new WebAssembly.Memory({initial:1});

var importObj = { console: { log: consoleLogString }, js: { mem: memory } };

fetchAndInstantiate('logger2.wasm', importObj).then(function(instance) {
  instance.exports.writeHi();
});

: 完全なソースは GitHub の logger2.html (動作例) を参照してください。

WebAssembly テーブル

WebAssembly テキストフォーマットのツアーを終了するために、WebAssemblyで最も複雑でしばしば混乱する部分 (テーブル) を見てみましょう。テーブルは基本的に WebAssembly コードからインデックスでアクセスできるリサイズ可能な参照の配列です。

なぜテーブルが必要なのかを見るために、最初に観察する必要があります。さきほど見た call 命令 (同じモジュールの他の関数から関数を呼び出す を参照) は静的な関数インデックスをとり、結果として1つの関数しか呼び出せません。しかし、呼び出し先がランタイム値の場合はどうなるでしょうか?

  • JavaScript ではこれは常に見えます。関数はファーストクラスの値です。
  • C/C++ では関数ポインタで見ることができます。
  • C++ では仮想関数で見ることができます。

WebAssembly にはこれを実現するための一種の呼び出し命令が必要だったため、動的な関数をオペランドに受け取る call_indirect を与えました。問題は WebAssembly ではオペランドに指定できる型が (現在) i32/i64/f32/f64 だけなことです。

WebAssembly は anyfunc 型 (任意のシグニチャの関数を保持できるため "any") を追加することができましたが、あいにくセキュリティ上の理由から anyfunc 型は線形メモリに格納できませんでした。線形メモリは格納された値の生の内容をバイト列として公開し、これによって wasm コンテンツが生の関数ポインタを自由に観察できて破損させることができてしまいます。これはウェブ上では許可できません。

解決方法は関数参照をテーブルに格納し、代わりにテーブルのインデックスを渡すことでした。これは単なる i32 値です。call_indirect のオペランドは単純に i32 のインデックス値にすることができます。

wasm でテーブルを定義する

どのようにしてテーブルに wasm 関数を配置するのでしょうか? data セクションを使用して線形メモリの領域をバイト列で初期化するのと同じように、elem セクションを使用してテーブルの領域を関数の列で初期化することが出来ます:

(module
  (table 2 anyfunc)
  (elem (i32.const 0) $f1 $f2)
  (func $f1 (result i32)
    i32.const 42)
  (func $f2 (result i32)
    i32.const 13)
  ...
)
  • (table 2 anyfunc) で、2 はテーブルの初期サイズ (2つの参照を格納できることを意味します) で、anyfunc はこれらの参照の要素型が「任意のシグニチャの関数」であることを宣言します。WebAssembly の現在のバージョンではこの型だけが要素型として許されますが、要素型は将来的にさらに追加される予定です。
  • 関数 (func) セクションは他の宣言された wasm 関数と同様です。これらはテーブルで参照する関数です (上の例ではそれぞれは定数を返すだけです) 。セクションが宣言された順序は重要ではないことに注意してください。関数はどこででも宣言できて elem セクションから参照することができます。
  • elem セクションはモジュール内の関数のサブセットをリスト化することができます (任意の順で並べることができ、重複を許容します) 。これは参照された順序でテーブルに参照される関数のリストです。
  • elem セクション内の (i32.const 0) 値はオフセットです。これはセクションの先頭で宣言する必要があります。これはテーブルに関数参照を追加するインデックスの開始位置を指定します。ここでは 0 と テーブルのサイズとして 2 (上記参照) を指定していますので、2つの参照はインデックスが 0 と 1 の部分に書き込まれます。もしオフセットを 1 にして書き込みたければ、 (i32.const 1) と記述してテーブルのサイズを 3 にする必要があります。

: 初期化されていない要素はデフォルトの throw-on-call 値が与えられます。

JavaScript で同じようなテーブルのインスタンスを作成する場合、次のようになります:

function() {
  // table section
  var tbl = new WebAssembly.Table({initial:2, element:"anyfunc"});

  // function sections:
  var f1 = function() { … }
  var f2 = function() { … }

  // elem section
  tbl.set(0, f1);
  tbl.set(1, f2);
};

テーブルを使用する

先に進みましょう。いま、何らかの形で使用するために必要なテーブルを定義しました。このコードのセクションで使ってみましょう:

(type $return_i32 (func (result i32))) ;; if this was f32, type checking would fail
(func (export "callByIndex") (param $i i32) (result i32)
  get_local $i
  call_indirect $return_i32)
  • (type $return_i32 (func (param i32))) ブロックで参照名を持つ型を指定します。この型は後でテーブルの関数参照呼び出しの型チェックを行うときに使用されます。ここでは、参照が1つの i32 を返す関数である必要があると言っています。
  • 次に、callByIndex としてエクスポートされる関数を定義します。パラメータとして1つの i32 をとり、引数名として $i が指定されています。
  • 関数内部でスタックに値を1つ追加します。値はパラメータ $i のものが渡されます。
  • 最後に、テーブルから関数を呼び出すために call_indirect を使用します。これは暗黙的に $i の値をスタックからポップします。この結果、callByIndex 関数はテーブルの $i 番目の関数を呼び出します。

call_indirect のパラメータはコマンド呼び出しの前に置く代わりに、次のように明示的に宣言することもできます:

(call_indirect $return_i32 (get_local $i))

より高級な、JavaScript のような表現力の高い言語では、関数を含む配列 (あるいはオブジェクトかもしれません) で同じことができることが想像できますよね。擬似コードだとこれは tbl[i]() のようになります。

型チェックの話に戻ります。WebAssembly は型チェックされていて、anyfunc は「任意の関数シグネチャ」を意味するので、呼び出し先の (推定される) シグネチャを指定する必要があります。そのため、プログラムに関数が i32 を返すはずだ、と知らせるために $return_i32 型を指定しています。もし呼び出し先のシグネチャがマッチしない (代わりに f32 が返されるような) 場合は WebAssembly.RuntimeError 例外がスローされます。

さて、呼び出しを行うときにどのようにテーブルに call_indirect をリンクさせているのでしょうか? 答えは、現在モジュールインスタンスごとに1つのテーブルしか許容されないため、call_indirect はそれを暗黙的に呼び出します。将来的に複数のテーブルを持てるようになったとき、以下の行のように、何らかのテーブル識別子を指定する必要があるでしょう。

call_indirect $my_spicy_table $i32_to_void

完全なモジュールは次のようになります。例は wasm-table.wat を参照してください:

(module
  (table 2 anyfunc)
  (func $f1 (result i32)
    i32.const 42)
  (func $f2 (result i32)
    i32.const 13)
  (elem (i32.const 0) $f1 $f2)
  (type $return_i32 (func (result i32)))
  (func (export "callByIndex") (param $i i32) (result i32)
    get_local $i
    call_indirect $return_i32)
)

次の JavaScript を使用してウェブページに読み込んでみましょう:

fetchAndInstantiate('wasm-table.wasm').then(function(instance) {
  console.log(instance.exports.callByIndex(0)); // returns 42
  console.log(instance.exports.callByIndex(1)); // returns 13
  console.log(instance.exports.callByIndex(2));
  // returns an error, because there is no index position 2 in the table
});

: 例は GitHub の wasm-table.html (動作例) を参照してください。

: Memory と同じように Table も JavaScript から作成すること (WebAssembly.Table() を参照) 、別の wasm モジュール間でインポートすることができます。

テーブルの変更と動的リンク

JavaScript は関数参照にフルアクセスできるため、Table オブジェクトは JavaScript から grow(), get() や set() メソッドを使用して変更することができます。WebAssembly が 参照型 を得たとき、WebAssembly コードは get_elem/set_elem 命令を使用してテーブル自身を変更することができるようになるでしょう。

テーブルはミュータブルであるため、それらは複雑なロード時、実行時の 動的リンクスキーム の実装で使用することができます。プログラムが動的にリンクされたとき、複数のインスタンスで同じメモリとテーブルを共有することができます。これは複数のコンパイル済み .dll が単一のプロセスのアドレス空間を共有するネイティブアプリケーションと対称的です。

この動作を確認するために、Memory オブジェクトと Table オブジェクトを含む単一のインポートオブジェクトを作成し、同じインポートオブジェクトを複数の instantiate() の呼び出しで渡してみましょう。

.wat ファイルの例は次のようになります:

shared0.wat:

(module
  (import "js" "memory" (memory 1))
  (import "js" "table" (table 1 anyfunc))
  (elem (i32.const 0) $shared0func)
  (func $shared0func (result i32)
   i32.const 0
   i32.load)
)

shared1.wat:

(module
  (import "js" "memory" (memory 1))
  (import "js" "table" (table 1 anyfunc))
  (type $void_to_i32 (func (result i32)))
  (func (export “doIt”) (result i32)
   i32.const 0
   i32.const 42
   i32.store  ;; store 42 at address 0
   i32.const 0
   call_indirect $void_to_i32)
)

これらは次のように動作します:

  1. 関数 shared0func は shared0.wat で定義され、インポートされたテーブルに格納されます。
  2. この関数は定数値 0 を作成して、次に i32.load コマンドを使用して指定したメモリのインデックスから値をロードします。そのインデックスは 0 になります 。先と同様に、前の値をスタックから暗黙的にポップします。つまり、shared0func はメモリのインデックス 0 の位置に格納された値をロードして返します。
  3. shared1.wat では、 doIt という関数をエクスポートします。この関数は2つの定数値 042 を作成して i32.store を呼び出して、インポートされたメモリの指定したインデックスに指定した値を格納します。ここでも、これらの値はスタックから暗黙的にポップされます。したがって、結果的にメモリのインデックスが 0 の位置に、値として 42 が格納されます。
  4. 関数の最後では、定数値 0 を作成し、テーブルのインデックスが 0 の位置にある関数を呼び出します。これは shared0func で、先に shared0.wat の elem ブロックで格納されたものです。
  5. 呼び出されたとき、shared0func は shared1.wat 内で i32.store コマンドを使用してメモリに格納された 42 をロードします。

: 上の式はスタックから値を暗黙的にポップしますが、代わりにコマンド呼び出しの中で明示的に宣言することができます:

(i32.store (i32.const 0) (i32.const 42))
(call_indirect $void_to_i32 (i32.const 0))

アセンブリに変換した後、次のコードで JavaScript 内で shared0.wasm と shared1.wasm を使用します:

var importObj = {
  js: {
    memory : new WebAssembly.Memory({ initial: 1 }),
    table : new WebAssembly.Table({ initial: 1, element: "anyfunc" })
  }
};

Promise.all([
  fetchAndInstantiate('shared0.wasm', importObj),
  fetchAndInstantiate('shared1.wasm', importObj)
]).then(function(results) {
  console.log(results[1].exports.doIt());  // prints 42
});

コンパイルされた各モジュールは同じメモリとテーブルオブジェクトをインポートし、その結果同じ線形メモリとテーブルの「アドレス空間」を共有することができます。

: 例は GitHub の shared-address-space.html (動作例) を参照してください。

まとめ

これで、WebAssembly テキストフォーマットの主要コンポーネントとそれらが WebAssembly JS API にどのように反映されるのかの高レベルなツアーが完了しました。

関連情報

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

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