人間が 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: 0100 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)
) 。 引数は基本的には呼び出し元から渡された、対応する引数の値で初期化された、ただのローカル変数です。
ローカル変数と引数を取得/設定する
ローカル変数と引数は関数本体から local.get
と local.set
命令を使用して読み書きすることができます。
local.get
/local.get
コマンドは数値のインデックスから取得/設定される項目を参照します。最初に引数が宣言順に、その後に、ローカル変数が宣言順に参照されます。次の関数を見てください:
(func (param i32) (param f32) (local f64)
local.get 0
local.get 1
local.get 2)
命令 local.get 0
は i32 の引数, local.get 1
は f32 の引数、そして local.get 2
は f64 のローカル変数を取得します。
ここで別の問題があります。数値のインデックスを使用して項目を参照すると、混乱したり、困ってしまうことがあります。そこで、テキストフォーマットでは、単純に型宣言の直前に ($
) をプレフィックスとして付けた名前を、引数、ローカル変数や他の多くの項目につけることができます。
したがって、上記のシグネチャを次のように書き直すことができます:
(func (param $p1 i32) (param $p2 f32) (local $loc f64) …)
そして、local.get 0
の代わりに local.get $p1
と書くことができるようになります (このテキストがバイナリに変換されたとき、バイナリには整数値だけが残されることに注意してください) 。
スタックマシン
関数本体を書く前に、もう1つ、スタックマシンについて話をする必要があります。ブラウザはそれを更に効率的な形にコンパイルしますが、wasm の実行はスタックマシンとして定義されます。スタックマシンの基本的なアイデアは全ての命令がスタックから特定の数の i32
/i64
/f32
/f64
値をプッシュ、ポップするようにすることです。
例えば、 local.get
はローカル変数の値をスタックにプッシュするように定義されます。そして、i32.add
は2つの i32
値 (スタックにプッシュされた前の2つの値を暗黙的に取得します) をポップし、合計を計算して (2^32 の剰余として) 結果の i32 値をプッシュします。
関数が呼び出されたとき、空のスタックから開始され、徐々に積まれてゆき、本体の命令が実行されると空になります。例として、次の関数の実行後について見てみましょう:
(func (param $p i32)
(result i32)
local.get $p
local.get $p
i32.add)
スタックには i32.add
よって処理された式 ($p + $p
) の結果として、ただ1つの i32
値が積まれています。関数の返値はスタックに残った最後の値になります。
WebAssembly のバリデーションルールはスタックが正確に一致することを保証します。もし、(result f32)
と宣言した場合、最終的にスタックに1つだけ f32
値が積まれている状態である必要があります。結果の型がない場合は、スタックは空でなければなりません。
はじめての関数本体
前述の通り、関数本体は関数が呼び出された後に続く単純な命令列です。 これまでに学んだことと一緒にして、最終的にはシンプルな関数を含むモジュールを定義することができるようになります:
(module
(func (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $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)
local.get $lhs
local.get $rhs
i32.add)
(export "add" (func $add))
)
例に従うなら、上のモジュールを add.wat
という名前で保存して、wabt を使用して (詳細は WebAssembly テキストフォーマットから wasm に変換する を参照してください) 、add.wasm
というファイルに変換します。
次に、 addCode
という名前の型付き配列にバイナリをロードし (WebAssembly コードのロードと実行 で説明されています) 、コンパイル、インスタンス化して、JavaScript で add
関数を実行します (add()
はインスタンスの exports
プロパティから見つけることができます):
WebAssembly.instantiateStreaming(fetch('add.wasm'))
.then(obj => {
console.log(obj.instance.exports.add(1, 2)); // "3"
});
注: この例は GitHub のadd.html (動作例) にあります。関数のインスタンス化についての詳細は WebAssembly.instantiateStreaming()
も合わせて参照してください。
基礎を探る
ここでは実際の基本的な例を取り上げてから、いくつかの高度な機能について見てみましょう。
同じモジュールの他の関数から関数を呼び出す
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 コードは次のようになります:
WebAssembly.instantiateStreaming(fetch('call.wasm'))
.then(obj => {
console.log(obj.instance.exports.getAnswerPlus1()); // "43"
});
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);
}
}
};
WebAssembly.instantiateStreaming(fetch('logger.wasm'), importObject)
.then(obj => {
obj.instance.exports.logIt();
});
注: この例は GitHub の logger.html (動作例)を参照してください。
WebAssembly でのグローバルの宣言
WebAssembly には、 JavaScript からアクセス可能なグローバル変数インスタンスを作成する機能と、1つ以上の WebAssembly.Module
インスタンスにまたがってインポート/エクスポート可能なグローバル変数インスタンスを作成する機能があります。これは、複数のモジュールを動的にリンクすることができるので、非常に便利です。
WebAssembly のテキスト形式では、次のようになります (GitHub のリポジトリにある global.wat を参照してください。JavaScript の例は global.html も参照してください)。
(module
(global $g (import "js" "global") (mut i32))
(func (export "getGlobal") (result i32)
(global.get $g))
(func (export "incGlobal")
(global.set $g
(i32.add (global.get $g) (i32.const 1))))
)
これは、キーワード global
を使用してグローバルな値を指定していることと、値のデータ型と一緒にキーワード mut
を指定して変更可能にしたい場合に指定していることを除いて、以前に見たものと似ています。
JavaScript を使用して同等の値を作成するには、 WebAssembly.Global()
コンストラクターを使用してください。
const global = new WebAssembly.Global({value: "i32", mutable: true}, 0);
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
を指定していますが、他の多くのエンコーディングをサポートしています) 。
function 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 importObject = { console: { log: consoleLogString }, js: { mem: memory } };
WebAssembly.instantiateStreaming(fetch('logger2.wasm'), importObject)
.then(obj => {
obj.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 funcref)
(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:"funcref"});
// function sections:
var f1 = ... /* some imported WebAssembly function */
var f2 = ... /* some imported WebAssembly 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)
local.get $i
call_indirect (type $return_i32))
(type $return_i32 (func (result i32)))
ブロックで参照名を持つ型を指定します。この型は後でテーブルの関数参照呼び出しの型チェックを行うときに使用されます。ここでは、参照が1つのi32
を返す関数である必要があると言っています。- 次に、
callByIndex
としてエクスポートされる関数を定義します。引数として1つのi32
をとり、引数名として$i
が指定されています。 - 関数内部でスタックに値を1つ追加します。値は引数
$i
のものが渡されます。 - 最後に、テーブルから関数を呼び出すために
call_indirect
を使用します。これは暗黙的に$i
の値をスタックからポップします。この結果、callByIndex
関数はテーブルの$i
番目の関数を呼び出します。
call_indirect
の引数はコマンド呼び出しの前に置く代わりに、次のように明示的に宣言することもできます:
(call_indirect (type $return_i32) (local.get $i))
より高級な、JavaScript のような表現力の高い言語では、関数を含む配列 (あるいはオブジェクトかもしれません) で同じことができることが想像できますよね。擬似コードだとこれは tbl[i]()
のようになります。
型チェックの話に戻ります。WebAssembly は型チェックされていて、anyfunc
は「任意の関数シグネチャ」を意味するので、呼び出し先の (推定される) シグネチャを指定する必要があります。そのため、プログラムに関数が i32
を返すはずだ、と知らせるために $return_i32
型を指定しています。もし呼び出し先のシグネチャがマッチしない (代わりに f32
が返されるような) 場合は WebAssembly.RuntimeError
例外がスローされます。
さて、呼び出しを行うときにどのようにテーブルに call_indirect
をリンクさせているのでしょうか? 答えは、現在モジュールインスタンスごとに1つのテーブルしか許容されないため、call_indirect
はそれを暗黙的に呼び出します。将来的に複数のテーブルを持てるようになったとき、以下の行のように、何らかのテーブル識別子を指定する必要があるでしょう。
call_indirect $my_spicy_table (type $i32_to_void)
完全なモジュールは次のようになります。例は wasm-table.wat を参照してください:
(module
(table 2 funcref)
(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)
local.get $i
call_indirect (type $return_i32))
)
次の JavaScript を使用してウェブページに読み込んでみましょう:
WebAssembly.instantiateStreaming(fetch('wasm-table.wasm'))
.then(obj => {
console.log(obj.instance.exports.callByIndex(0)); // returns 42
console.log(obj.instance.exports.callByIndex(1)); // returns 13
console.log(obj.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 funcref))
(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 funcref))
(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 (type $void_to_i32))
)
These work as follows:
- 関数
shared0func
はshared0.wat
で定義され、インポートされたテーブルに格納されます。 - この関数は定数値
0
を作成して、次にi32.load
コマンドを使用して指定したメモリのインデックスから値をロードします。そのインデックスは0
になります 。先と同様に、前の値をスタックから暗黙的にポップします。つまり、shared0func
はメモリのインデックス0
の位置に格納された値をロードして返します。 shared1.wat
では、doIt
という関数をエクスポートします。この関数は2つの定数値0
と42
を作成してi32.store
を呼び出して、インポートされたメモリの指定したインデックスに指定した値を格納します。ここでも、これらの値はスタックから暗黙的にポップされます。したがって、結果的にメモリのインデックスが0
の位置に、値として42
が格納されます。- 関数の最後では、定数値
0
を作成し、テーブルのインデックスが 0 の位置にある関数を呼び出します。これはshared0func
で、先にshared0.wat
のelem
ブロックで格納されたものです。 - 呼び出されたとき、
shared0func
はshared1.wat
内でi32.store
コマンドを使用してメモリに格納された 42 をロードします。
注: 上の式はスタックから値を暗黙的にポップしますが、代わりにコマンド呼び出しの中で明示的に宣言することができます:
(i32.store (i32.const 0) (i32.const 42))
(call_indirect (type $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: "funcref" })
}
};
Promise.all([
WebAssembly.instantiateStreaming(fetch('shared0.wasm'), importObj),
WebAssembly.instantiateStreaming(fetch('shared1.wasm'), importObj)
]).then(function(results) {
console.log(results[1].instance.exports.doIt()); // prints 42
});
コンパイルされた各モジュールは同じメモリとテーブルオブジェクトをインポートし、その結果同じ線形メモリとテーブルの「アドレス空間」を共有することができます。
注: 例は GitHub の shared-address-space.html (動作例) を参照してください。
WebAssembly の複数値
もっと最近になって (例えば Firefox 78) 言語に追加されたものが WebAssembly 複数値であり、これは、WebAssembly 関数が複数の値を返すことができるようになり、一連の命令が複数のスタック値を消費して生成することができるようになったことを意味します。
執筆時点 (2020年6月) において、これは初期段階であり、利用可能な多値命令は、それ自体が複数の値を返す関数の呼び出しのみです。例を示します。
(module
(func $get_two_numbers (result i32 i32)
i32.const 1
i32.const 2
)
(func (export "add_to_numbers") (result i32)
call $get_two_numbers
i32.add
)
)
しかし、これはより有用な命令タイプやその他のものへの道を開くことになるでしょう。これまでの進捗状況や、これがどのように動作するかについては、 Nick Fitzgerald の Multi-Value All The Wasm! を参照してください。
まとめ
これで、WebAssembly テキストフォーマットの主要コンポーネントとそれらが WebAssembly JS API にどのように反映されるのかの高レベルなツアーが完了しました。
関連情報
- この記事に含まれなかった主なものは、関数本体で現れる全ての命令の包括的なリストです。各命令の処理は WebAssembly のセマンティックス を参照してください。
- スペックインタプリタによって実装された テキストフォーマットの文法 も参照してください。