この文書は、nodejs-8で有効化されているWebAssemblyがどういう仕組なのかを調査したものです。主題はWebAssemblyそのものの仕組みについてであり、普通のWebAssembly記事でやってるような既存のC/C++資産利用やemscripten使用法や速度比較といったブラックボックス利用を主題にした話ではありません。
WebAssemblyとは
WebAssemblyは、ブラウザ上で実行可能なプログラム実行環境の標準の一つです。
ただし現時点では直接実行させるのではなく、JavaScript環境から実行させるものとなっています。WebAssembly
オブジェクトを介して、WebAssemblyプログラムのバイナリをWebAssembly.Module
としてロードし、そこからJavaScript側で関数として取り出して呼び出すことでコード実行させることができるもの、となっています。
そして現在、v8等のブラウザに搭載されるJavaScriptエンジン自体が、WebAssemblyプログラムの実行環境を備え、ブラウザ上で利用可能となっています。node.jsではバージョン8からデフォルトでブラウザと同じWebAssembly
オブジェクトが利用可能となりました。
WebAssemblyのMinimum Viable Product(MVP)
WebAssemblyの最初の仕様は、実用性を損なわない範囲で最低限必要な機能のみを備えたものを早期に出荷することを目指して作られ、MVPと呼ばれています。スレッドやGCなどの複雑な機能の追加は、MVP以後にされるべきものとされています。
たとえば、以下は、WebAssemblyの設計指針上の制約ではなく、MVPな要素だとしています:
- module形式だけ実行できる
- 関数の戻り値が一つ
- 扱えるのは32ビットと64ビットの値だけ
将来的には、Post-MVP部分として追加されていくことでしょう。現状の制限がWebAssemblyの設計指針からくるものなのか、単にMVP以後へ後回しされたものでしかないのか注意してみていく必要があります。(この文の全体にわたって、MVP的な内容への言及には"現在"、"現時点"、"現状"と言った形容をつけてあります)
WebAssemblyプログラムの2つのフォーマット
WebAssemblyのプログラムファイルには、2つの表現形式があります。
テキストフォーマット(=.wastファイル)では、(
と)
で木構造データを表す、S式スタイルを採用しています。
そして、JavaScriptから実行させるには、バイナリフォーマット(=.wasmファイル)を使用します。
WebAssemblyのmodule
現在WebAssemblyプログラムは"module"の単位で提供します。すなわちwastファイルのS式のルート要素は(module)
になります。
moduleは以下のトップレベル要素をバンドルしたものとなっています。
-
(global)
: 名前付きグローバル変数値(要初期値) -
(memory)
: インデックス値でアクセスするリニアな可変ヒープ領域 -
(table)
: インデックス値でアクセスする固定型要素のリスト -
(func)
: 関数のコード -
(import)
: ロード時に渡す必要のある名前付きの値 -
(export)
: 外からモジュールへアクセスできる名前付きの値 -
(type)
: funcの型シグネチャ定義 -
(data)
: memoryの初期化値 -
(elem)
: tableの初期化値 -
(start)
: ロード時に呼び出される関数の指定
global
/memory
/table
/func
は、それぞれ固有に存在するアクセス命令セットの対象となるオブジェクトであり、外部のJavaScriptと連携するためのimport
/export
の対象にもなります。
import
/export
式は、トップレベルで、名前付きglobal
/memory
/table
/func
に対して割り当てます。type
はfunc
の型(引数と戻り値の型)の定義で、トップレベルに存在します。ただし、テキストフォーマットでは、import
/export
/type
はfunc
/global
/memory
/table
式の内部に埋め込む表現も可能です。
data
/elem
/start
はロード時の初期化用の仕組みです。存在しなくても問題ありません。
ちなみにtable
は、現状ではmoduleにひとつだけ置くことができ、要素は"anyfunc"
(関数)のみ可能となっています。命令の中にtable
の要素の関数を呼び出すcall_indirect
があります。静的に型チェックする通常のfunc
へのcall
命令と違って、call_indirect
では実行時に型チェックをするという違いがあります。
ツールbinaryen
WebAssembyフォーマットを扱うためのツールは数種類あるけれど、"binaryen"が、homebrewですでにパッケージとして用意されており、導入しやすいです。
# on macOS
$ brew install binaryen
binaryenには以下のコマンドが含まれています:
-
wasm-as
: wastからwasmへ変換する -
wasm-dis
: wasmかwastへ変換 -
wasm-shell
: wasmファイルを実行する(unittest用?) -
asm2wasm
: asm.jsモジュール形式のJavaScriptファイルをwasm/wastに変換する -
s2wasm
: アセンブリスタイルのWebAssembly(LLVMでのWebAssemblyバックエンド出力)である.sファイルを
wasm/wastに変換する
今回は、wasm-as
/wasm-dis
/asm2wasm
だけを使います。
WebAssembly JavaScript API
WebAssemblyでは、JavaScript上でのWebAssembly
オブジェクトの仕様も決められています。
WebAssembly.validate(buf)
WebAssembly.compile(buf)
WebAssembly.instantiate(buf, imports)
new WebAssembly.Module(buf)
new WebAssembly.Instance(module, imports)
new WebAssembly.Memory(opts)
new WebAssembly.Table(opts)
new WebAssembly.CompileError
WebAssembly.LinkError
WebAssembly.RuntimeError
compile
とinstantiate
はPromise
です。
また、このAPI仕様で、WebAssemblyの値とJavaScriptの値のマッピングも規定されています。
例1: 手書きwastファイルでwasmファイルを作り、nodejsから呼び出すまで
(他の言語は使わずに)簡単なWebAssemblyプログラムをテキストフォーマットで記述して、それをnodejsで実行させることから始めます。
adder.wast
を記述
テキストフォーマット仕様とオペレータ仕様を元に、整数加算をする関数addだけを入れたモジュールのwastファイル"adder.wast"は以下のようになります:
(module
(func (export "add") (param $a i32) (param $b i32) (result i32)
(return (i32.add (get_local $a) (get_local $b))))
)
WebAssemblyの関数は(固定サイズの)ローカル変数(param
及びlocal
式)のあるスタックマシンであり、その範疇ではglobalやmemoryは不要です。
現状、変数や値の型は、i32
、i64
、f32
、f64
の4種類のみです。add
等演算命令には必ず型指定がつき、引数も戻り値も全て同じ型を使います。このため、4型で[相互変換](http://webassembly.org/docs/semantics/#datatype-conversions-truncations-reinterpretati
ons-promotions-and-demotions)をする命令が多数存在しています。
注意点として、JavaScript APIの仕様により、現状i64
型なシグネチャの関数は、JavaScript側から直接アクセスできません。
adder.wasm
へ変換
binaryenのwasm-as
コマンドを使って、wastファイルをバイナリのwasmファイルに変換します。
$ wasm-as adder.wast > adder.wasm
非同期版use-adder-async.js
ブラウザ等での外部リソースアクセスは通常非同期処理のため、基本は非同期にロードして使うのがよいでしょう。
//use-adder-async.js
"use strict";
const {promisify} = require("util");
const fs = require("fs");
const readFile = promisify(fs.readFile);
readFile("./adder.wasm").
then(buf => WebAssembly.instantiate(buf, {})).
then(source => console.log(source.instance.exports.add(10, 20)));
instantiate(buf, imports)
の結果は{module: WebAssembly.Module, instance: WebAssembly.Instance}
であり、WebAssembly.Instance
のexports
プロパティにwasmからexportした関数があります。
このコードは、以下のように、特別なオプション等の必要なく実行可能です。
$ ls
adder.wasm adder.wast use-adder-async.js
$ node use-adder-async.js
30
同期版use-adder-sync.js
一方、nodejs APIのfs
にある同期ファイルアクセスを使えば、同期的にもwasmモジュールをロードできます。
//use-adder-sync.js
"use strict";
const fs = require("fs");
const buf = fs.readFileSync("./adder.wasm");
const mod = new WebAssembly.Module(buf);
const inst = new WebAssembly.Instance(mod, {});
const {add} = inst.exports;
console.log(add(10, 20));
ちなみに、WebAssembly JavaScript APIには、非同期的なcompile
やinstantiate
と、同期的なModule
とInstance
が用意されていますが、compile
で作ったModule
をつかってnew Instance
するなど、ミックスさせて使うことも可能なようです。
例2: JavaScriptにはないWebAssembly演算を使ってみる
現在、64ビット整数演算や、ビット処理rotr/rotl/popcnt/ctz等はJavaScriptには存在していません。
そこでWebAssemblyに現在実装済みのこれらを呼ぶだけの関数をexportするmodule "bitops.wast"を書いてみます:
(module
(func (export "popcnt") (param $a i32) (result i32)
(return (i32.popcnt (get_local $a))))
(func (export "pack") (param $a f64) (result f64)
(return (f64.reinterpret/i64 (i64.trunc_s/f64 (get_local $a)))))
(func (export "unpack") (param $a f64) (result f64)
(return (f64.convert_s/i64 (i64.reinterpret/f64 (get_local $a)))))
(func (export "clz64") (param $a f64) (result i32)
(return (i32.wrap/i64 (i64.clz (i64.reinterpret/f64 (get_local $a))))))
)
現在、64ビット整数なシグネチャを持つ関数はJavaScriptから直接アクセスする手段がないので、なんらかの工夫をする必要がでてきます。そこで、以下のような手段が考えられます。
- memory(ArrayBuffer)を使う
- 32ビット整数2つに割る
- ビット列として埋め込んだ64ビット浮動小数を使う
今回は最後のを採用するために、64ビット整数なビット列を作る機能が必要で、(JavaScript側で処理した)浮動小数の数値を64ビット整数ビット列にするpack
と64ビット整数ビット列から(JavaScript側で処理する)浮動小数値にするunpack
の2つの関数をWebAssembly内で定義しました。
reinterpret
は同じビット列として型情報だけ差し替える命令であり、一方convert
やtruncate
は数値として型変換する命令です。53ビットを超える64ビット整数値は、64ビット浮動小数で表せられない値があるので、truncate
では切り詰められることになります。
以降、"wasm-as"コマンドでwastファイルをバイナリwasmにして、JavaScriptでロードして利用する手段は前と一緒です。
そこで今回は、nodejsモジュールっぽい"bitops.js"を作成し、それを使ったコードにしてみました。
//bitops.js
"use strict";
const fs = require("fs");
const buf = fs.readFileSync("./bitops.wasm");
const mod = new WebAssembly.Module(buf);
const inst = new WebAssembly.Instance(mod, {});
module.exports = inst.exports;
//use-bitops.js
"use strict";
const {popcnt, pack, unpack, clz64} = require("./bitops.js");
console.log(popcnt(0b101101000)); // => 4
console.log(popcnt(-1)); // => 32
console.log(popcnt(5.5)); // => 2 (as 5.5|0 => 5)
console.log(pack(1)); // => 5e-324: pack (f64) number as i64 bitfield
console.log(unpack(pack(1))); // => 1: unpack i64 bitfield to (f64) number
console.log(clz64(pack(1))); // => 63
$ node use-bitops.js
4
32
2
5e-324
1
63
例3: asm.jsなモジュールをwasm化して使う
まず、asm.jsなモジュール"adder.asm.js"を手書きします。
// adder.asm.js
function () {
"use asm";
function add(a, b) {
a = a | 0;
b = b | 0;
return a + b | 0;
}
return {add: add};
}
binaryenの"asm2wasm"コマンドでwasmファイルへ変換します。また、asm2wasmは"-o file.wasm"オプションが存在しなければ(色付きの)wast形式で標準出力します。
$ asm2wasm adder.asm.js -o adder.wasm
$ asm2wasm adder.asm.js
(module
(import "env" "memory" (memory $0 256 256))
(import "env" "table" (table 0 0 anyfunc))
(import "env" "memoryBase" (global $memoryBase i32))
(import "env" "tableBase" (global $tableBase i32))
(export "add" (func $add))
(func $add (param $a i32) (param $b i32) (result i32)
(return
(i32.add
(get_local $a)
(get_local $b)
)
)
)
)
今度のwastには、import
式が入っている点に注目です。WebAssembly.Instance
の第二引数として、ここで指定された名前の位置に適切なデータを与える必要があります(func内で使ってなくても)。
- 初期ブロック256、最大ブロック256の
memory
- 初期長0、最大長0の
table
この"adder.wasm"を呼び出すJavaScriptコードは、以下のようになります。
//use-asmjs-adder.js
"use strict";
const fs = require("fs");
const buf = fs.readFileSync("./bitops.wasm");
const mod = new WebAssembly.Module(buf);
const imports = {
env: {
memory: new WebAssembly.Memory({initial: 256, maximum: 256}),
table: new WebAssembly.Table({
initial: 0, maximum: 0, element: "anyfunc"}),
memoryBase: 0,
tableBase: 0,
},
};
const inst = new WebAssembly.Instance(mod, imports);
const {add} = inst.exports;
console.log(add(10, 20));
wastでの(import "env" "memory" (memory ...))
は、実行時JavaScript側でimports.env.memory
にWebAssembly.Memory
をセットしてある、という意味になります。table
も同様です。実質使っていないmemoryBase
やtableBase
は0
にしています。
この一連のimport
は、"asm.js"言語を成立させるための環境を構成するのに必要となる要素です。他の言語からwasmを生成する場合は、その言語環境のために埋め込まれたimport
に、何を与えてやらなければいけないのかを把握し、それをJavaScript側で埋め合わせてやる必要があります。
例4: emscriptenからwasmを生成して使う
emscriptenは、llvmを使ってcやc++からJavaScriptコードに変換するシステムですが、その主要コード部分をasm.jsスタイルなコードで生成ができるようになり、さらにその部分をbinaryenによってWebAssemblyに置き換える事ができるように発展していった経緯があります。
emscriptenの設定
homebrewでインストールしたバイナリがそのままで使えますが、設定の調整が必須です。
$ brew install emscripten binaryen
$ emcc
最初にemscriptenのコマンド(Cコンパイラemccなど)を空実行すると"~/.emscripten"ファイル等が生成されます。
しかし、この"~/.emscripten"のうち、EMSCRIPTEN_ROOT
、LLVM_ROOT
、BINARYEN_ROOT
が正しくないので、以下のように修正します(インストールしたパッケージのバージョンに注意)。
EMSCRIPTEN_ROOT = "/usr/local/Cellar/emscripten/1.37.10/libexec"
LLVM_ROOT = "/usr/local/Cellar/emscripten/1.37.10/libexec/llvm/bin"
BINARYEN_ROOT = "/usr/local/Cellar/binaryen/33"
ほかは実行可能コマンドのあるディレクトリだけど、BINARYEN_ROOT
だけはbin/asm2wasm
のあるベースの位置になるのにも注意です。
設定ファイル変更後の最初のemcc等を実行したときにはキャッシュ再生成するため、終了までにすこし時間がかかり、キャッシュ関係の余計な出力がでてきます。設定が間違っている場合はここでエラーや警告がでてきます。
adder.c
wasm化するためのCコードのadder.cです。
/* adder.c */
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
EMSCRIPTEN_KEEPALIVE
マクロは、emccのコマンドラインオプション-s EXPORTED_FUNCTIONS="['_add']"
でも代用できますが、ここではオプションの単純化のためソースファイル内に埋め込みました。
(C言語での名前add
は、ABIとしての外部名_add
になり、外部JavaScriptからはこの_add
を使います。)
emscriptenモジュールとしてWebAssembly利用
emscriptenは、C言語の機能を実現するための(malloc
等の)システム環境を用意した上で、JavaScript上で実行させます。このため、WebAssemblyが使えると言っても、WebAssembly
オブジェクトを直接呼び出すのではなく、emscriptenで生成するemscriptenのモジュール(.jsファイル)として呼び出し、その内部の生成コード内で使われるという関係になります。
また、emscriptenのモジュールには非同期性があるので、JavaScript側から呼び出すには、モジュールへセットした初期化終了のコールバックonRuntimeInitialized
のなかで関数を呼び出すことになります。
// use-emscripten.js
"use strict";
const adder = require("./adder.js");
adder.onRuntimeInitialized = () => {
console.log(adder._add(10, 20)); // "add()"'s binary name is "_add"
};
このemscriptenモジュール"adder.js"を生成するコマンドは、以下のとおりです。
$ emcc adder.c -o adder.js -s WASM=1
$ ls
adder.c adder.js adder.wasm
-s WASM=1
オプションを付けたemccコマンドによって"adder.js"と同時に"adder.wasm"も生成されます。
この"adder.wasm"を"wasm-dis"コマンドで変換してみるとわかりますが、emscriptenが生成したwasmファイルに含まれるimport式はかなり多く、自前でimportsオブジェクトを用意するのは難しいものになっています。importが多くなるのは、C標準ライブラリ実装のための部分が大きいからです。
別手法: ONLY_MY_CODE
でwasm生成
しかし、この"addeer.c"では、標準ライブラリの機能を一切使用していません。そこで、-s ONLY_MY_CODE=1
オプションをつかって、(ライブラリ部分を省いた)ソースにあるコードだけでwasmを生成するようにしてみます。
$ emcc adder.c -o adder.js -s WASM=1 -s ONLY_MY_CODE=1 -s SIDE_MODULE=1
$ ls
adder.c adder.wasm
ちなみに、-s SIDE_MODULE=1
は、外側のemscriptenモジュールjsファイルを生成しないオプションです。この"adder.wasm"の中身を"wasm-dis"でみると、
$ wasm-dis adder.wasm
(module
(type $0 (func (param i32)))
(type $1 (func (param i32 i32) (result i32)))
(import "env" "STACKTOP" (global $import$0 i32))
(import "env" "STACK_MAX" (global $import$1 i32))
(import "env" "abortStackOverflow" (func $import$2 (param i32)))
(import "env" "memory" (memory $0 256 256))
(import "env" "table" (table 0 0 anyfunc))
(import "env" "memoryBase" (global $import$5 i32))
(import "env" "tableBase" (global $import$6 i32))
(global $global$0 (mut i32) (get_global $import$0))
(global $global$1 (mut i32) (get_global $import$1))
(global $global$2 (mut f32) (f32.const 0))
(export "_add" (func $0))
...
と、importは7つだけになりました。そして、この7つを自前で用意するコードは以下のようになりました。
//use-onlymycode.js
"use strict";
const fs = require("fs");
const buf = fs.readFileSync("./adder.wasm");
const mod = new WebAssembly.Module(buf);
const imports = {
env: {
STACKTOP: 4064,
STACK_MAX: 5246944,
abortStackOverflow: function (i32) {throw Error("stack oveflow");},
memory: new WebAssembly.Memory({initial: 256, maximum: 256}),
table: new WebAssembly.Table({
initial: 0, maximum: 0, element: "anyfunc"}),
memoryBase: 1024,
tableBase: 0,
},
};
const inst = new WebAssembly.Instance(mod, imports);
const {_add} = inst.exports;
console.log(_add(10, 20));
STACKTOP
、STACK_MAX
、memoryBase
、tableBase
はemscriptenモジュールの"adder.js"にあった値です。STACKTOP
やSTACK_MAX
は生成コード内で使われているので、その関係を満たす必要があります。
もしTOPとMAXをともに0にすると_add
からabortStackOverflow
が呼ばれます。
例5: メモリで数値配列を扱うWASTプログラムを書く
ここまでは、WebAssembly環境調査のために単純な単数値のみを扱うadd
関数のみで扱っていました。ここでは、WebAssembly言語自体の調査として、より高度な処理を書く導入のために、memoryをつかいループを行うプログラムを書いてみます。
"vector.wast"
メモリを用意し、メモリ上の2つの配列を足し合わせるaddTo
を行うモジュール"vector.wast"は、以下のようになります。
(module
(memory $0 (export "heap") 1 256)
(func (export "addTo") (param $a i32) (param $b i32) (param $len i32) (result i32)
(loop $loop
(if (i32.eqz (get_local $len))
(return (i32.const 0)))
(i32.store
(get_local $a)
(i32.add (i32.load (get_local $a))
(i32.load (get_local $b))))
(set_local $a (i32.add (get_local $a) (i32.const 4)))
(set_local $b (i32.add (get_local $b) (i32.const 4)))
(set_local $len (i32.sub (get_local $len) (i32.const 1)))
(br $loop)
)
)
)
32ビット整数配列を指定した長さ部分だけ、ループで足し合わせるコードです。引数の$a
と$b
にメモリ上のアドレス、$len
には数値配列の長さを想定します。
- 現状、
(memory)
は一つだけ指定できます(ただし、binaryen/33では、ローカル名`$0'でないと失敗する) - 現状、シグネチャとして
(result )
は必ず数値一つ返す必要があります。 -
(loop label ...)
は、内部の(br label)
によって繰り返し処理します。(br)
しなければ繰り返しません。 -
(i32.store address value)
や(i32.load address)
のアドレスはメモリのバイトオフセットです。 - メモリデータのバイトオーダーはJavaScriptの
TypedArray
と同じ(リトルエンディアン)です。
これまで見てきたツール生成コードのようにimport
するのではなく、メモリはexport
して使うこともできます。他とメモリを共有しない単独のwastモジュールとしてはexportのほうが使いやすいでしょう。
ちなみに、WebAssemblyの各命令の呼びだしかたは、WebAssemblyのtestsuiteの例を参考に類推しました。
同様に、"wasm-as"コマンドでwasmファイルに変換します。
$ wasm-as vector.wast > vector.wasm
"use-vector.js"
このvector.wasmを呼び出すコードは以下のようになります。
// use-vector.js
"use strict";
const fs = require("fs");
const buf = fs.readFileSync("./vector.wasm");
const mod = new WebAssembly.Module(buf);
const imports = {};
const inst = new WebAssembly.Instance(mod, imports);
const {heap, addTo} = inst.exports;
const len = 8;
const a = 0 * Int32Array.BYTES_PER_ELEMENT,
b = 8 * Int32Array.BYTES_PER_ELEMENT;
const memA = new Int32Array(heap.buffer, a, len);
const memB = new Int32Array(heap.buffer, b, len);
for (let i = 0; i < len; i++) memA[i] = i + 1;
for (let i = 0; i < len; i++) memB[i] = (i + 1) * 10;
addTo(a, b, len);
console.log(memA); //=> [11, 22, 33, 44, 55, 66, 77, 88]
console.log(memB); //=> [10, 20, 30, 40, 50, 60, 70, 80]
importもしくはexportしたWebAssembly.Memory
オブジェクトのbuffer
プロパティがArrayBuffer
であるので、JavaScript側でそこから処理させる型にあったTypedArray
に切り出して使えます。
追記: WebAssembyは速い?のか
「なぜWebAssemblyはasm.jsより速いのか」という記事がありますが、注意して読めば、その「速い」は**条件付きでの「速い」**であるいう話なのです:
- コードのファイルサイズが小さい => 起動時だけ速い
- 64ビット整数演算ができる => 64ビット整数値を使わないなら同じ
- メモリオフセットが使える => (構造体無しで)数値配列を使うだけなら同じ
- popcntやcopysignが使える => ビット演算しないなら同じ
- emscriptenが賢くなった => 最初からコンパクトなコードなら同じ
という意味です。
ということで、実際に比較可能な数値配列のみのコードを書いてみました。
ES6コードはmap/reduceを多用する関数型スタイルのコードで、これがベースとなっています。wasmやasm.jsのコードは、この畳込み部分をメモリを使うように差し替えたものです。
ベンチマークとして、セルオートマトンの次ステップ生成の時間を図っています。初期化時間は計測していません。結果は、以下の通りでした。
firefox-54 | chrome-59 | |
---|---|---|
ES6 | 70ms | 240ms |
wasm | 80ms | 130ms |
asm.js | 80ms | 210ms |
記事にあるようにfirefoxではasm.jsコードとwasmコードでは速度にほぼ差がないようです。ただし、Arrayのmap
やreduce
メソッドを使ったコードのほうが若干速いです。最初のTypedArrayのset
をしないコードでnext全体を移植すれば早くなるように見えますが、実行する計算の数から考えれば結果はほぼ変わりません。実際にnext()
呼び出し全体を囲って図ったとしてもほぼ同じ結果になりました(chromeも含めて)。
確かにchromeではwasmコードが最速でしたが、ただこれはv8でArrayのインデックスアクセスがかなり高速化されたのに、map
やreduce
等のメソッドがいまだ高速化されていない、という現状によるものです。ここ2年のchromeとfirefoxの速度の関係はこのようなものです。
ただ、wasmでは実行速度面でのブラウザ間の違いが小さい、とは言えるでしょう。
まとめ
- WebAssemblyは、JavaScriptから呼び出す実行環境で、JavaScriptエンジンが備える機能である
- WebAssemblyプログラムは、バイナリフォーマットwasmとテキストフォーマットwastがある
- WebAssemblyプログラムはmodule構造である
- wasm moduleにはimportsとexportsがあり、JavaScript側ではimportsを用意し、exportsを利用する
- wasm moduleは、JavaScriptからは非同期でロードするものであるが、nodejsでは同期ロードも可能である
- WebAssemblyには、JavaScriptにはないビット演算や64ビット整数演算が存在する
- 64ビット整数のシグネチャを持つ関数は、JavaScript側から直接呼び出せない
- 他プログラミング言語から変換したwasmには、その言語環境固有のimportsを用意する必要がある
- emscriptenでのWebAsemblyは、基本emscripten moduleの内部で利用し、直接触らないものである
- 追記: WebAssemblyだからといって劇的に早くなるわけじゃないが、ブラウザ間の速度差は小さい