DenoからZigで作ったWasmを使う
こちらの記事の続きです。
実際にZigでWebAssembly(Wasm)を作って、そのWasmをDenoから呼び出してみます。
DenoとZigのインストール
DenoとZigは公式サイトに書かれている方法でインストールしてください。
Zig公式サイトに書かれていませんがWindowsではscoopでもインストールできました。
scoop install deno zig
最初のZigファイル
WasmのHello Worldといえば、2つの引数を足し算するだけの関数です。関数にexportを付けるとJavaScriptから呼び出すことができるようになります。
export fn add(x: f64, y: f64) f64 {
return x + y;
}
JavaScriptから呼び出す関数の全ての引数と戻り値は、JavaScriptのNumberで表せる型でなければなりません。JavaScriptのNumberは倍精度64ビット浮動小数点型です。Zigの型でいうと主に以下を利用することになります。
i8, u8, i16, u16, i32, u32, f64, isize, usize, bool, ポインタ
f64がNumberと同じ型です。とはいえすべてf64にしてしまうと可読性が落ちるので、プログラムに適した型を利用したほうが良いと思います。Wasmの場合どうなのか分かりませんが、一般的には浮動小数点型より整数型のほうが演算速度が速いです。
このうちisize型はポインタと同じサイズの符号付き整数型です。Wasmをブラウザ(およびDeno)で実行する場合のポインタサイズは64ビットOSであろうと32ビットにしますので、isizeは i32と同じです。
ZigをWasmにコンパイル
コンパイルするには次のコマンドを実行します。
(Zig v0.12.0の場合です。バージョンが変わるたびにコマンドがころころ変わっています)
zig build-exe add.zig -target wasm32-freestanding -fno-entry -rdynamic --import-memory -O ReleaseSmall
長いですね。実行すると、add.wasmとadd.wasm.oが生成されます。add.wasm.oは不要なので削除しても問題ありません。
build-exeはサブコマンドです。Wasmにするときはこれを使います。
add.zigはコンパイルするZigファイルです。
-target wasm32-freestandingは生成するファイルの種類になります。wasm32-wasiというターゲットもありますが、ブラウザやDenoで動かすときにはwasm32-freestandingにします。
-fno-entryはエントリポイント、すなわちmain関数が無いということです。
-rdynamicはexportを付けた関数をすべてwasmファイルに出力するフラグです。
--import-memoryはメモリをWasmではなく外部で確保するということです。メモリの確保はJavaScript側で行ったほうが便利ですのでこのオプションを付けています。
-O ReleaseSmallは最適化オプションです。ファイルサイズが小さくなります。-O Debugにするとデバッグモードになります。
その他のオプションはzig build-exe --helpで確認できます。
DenoからWasmを呼び出す
JavaScriptでメモリ空間を確保してWasmのプログラムを実行します。
// メモリの確保
const memory = new WebAssembly.Memory({initial:20, maximum:100})
// Wasmモジュールのインポート
const module = new WebAssembly.Module(Deno.readFileSync('add.wasm'))
// Wasmにメモリを割り当てる
const instance = new WebAssembly.Instance(module, { env: { memory } })
// 関数のシンボルを取り出して型を付ける
const add = instance.exports.add as (x: number, y: number) => number
// 関数を実行する
const result = add(-1, 3)
console.log(result)
1行ずつ説明していきます。
const memory = new WebAssembly.Memory({ initial: 20, maximum: 100 })
この行でメモリオブジェクトを生成しています。メモリサイズは必要量が自動的に確保されますので、初期サイズと最大サイズだけ指定します。
const module = new WebAssembly.Module(Deno.readFileSync('./add.wasm'))
この行でWasmのモジュールオブジェクトを生成しています。インスタンス引数にはWasmファイルのバイナリをUint8Arrayのような型付き配列かArrayBufferで指定します。
この仕組みにより、Wasmファイルをファイルから読み取ってモジュールにすることもできますし、Base64エンコードしてJavaScriptコードに埋め込んだ文字列からデコードしてモジュールにすることもできます。
ここではDeno.readFileSyncでWasmファイルを読み込んでいます。
const instance = new WebAssembly.Instance(module, { env: { memory } })
先ほど作ったWasmのモジュールにメモリを割り当ててインスタンスオブジェクトを生成しています。
この行を変更することはほとんど無いと思います。
なお、第二引数の{ env: { memory } }はenvプロパティのmemoryプロパティにメモリを渡していますが、Zigの場合こうなるというものです。他の言語の場合は変わる可能性があります。また、Zigのバージョンアップで将来変わる可能性があります。どうやって調べればいいのかについては以下に折りたたんで説明します。
importObjectの調べ方
Wasmの開発をするときは、The WebAssembly Binary Toolkit(WABT)を使うのが便利です。
WABTをインストールすると、ツール群の中に、wasm2watというwasmファイルから逆アセンブルして、
人間が辛うじて読めるアセンブリ言語のWatファイルを作ることができます。
Wasmが外部からインポートするものは、Watファイルを見ると書かれています。
それでは、wasm2watを使って、add.wasmを逆アセンブルしてみましょう。
(module
(type (;0;) (func (param f64 f64) (result f64)))
(import "env" "memory" (memory (;0;) 16))
(func (;0;) (type 0) (param f64 f64) (result f64)
local.get 0
local.get 1
f64.add)
(global (;0;) (mut i32) (i32.const 1048576))
(export "add" (func 0)))
この中で、2行目の(import ~となっているのが外部からインポートするものです。ここでは(import "env" "memory" (memoryとなっているので、メモリはenvプロパティのmemoryプロパティに渡してあげれば良いことが分かります。
const add = wasmInstance.exports.add as (x: number, y: number) => number
この行でWasm内に定義されている関数を取り出して実行できるようにします。Wasm内でexportしている関数の数だけ必要です。
引数と戻り値の型はすべてnumberになります。
const result = add(-1, 3.2)
Wasmから取り出した関数は普通の関数と同じように実行できます。渡すデータはJavaScript側ではnumberですが、Zig側ではそれより小さいサイズの型の可能性がありますので、渡すデータの値には注意が必要です。
Denoを実行する
作ったDenoファイルを実行します。Deno.readFileSyncを用いていますので--allow-readオプションが必要です。
deno run --allow-read add.ts
2.2と出力されます。
以上となります。
使ったファイルはたったこれだけです。
add.zig
add.wasm
add.wasm.o
app.ts
Wasmを使ってみた系の記事はここまでが多いのですが、数値計算だけできてもあまり役に立ちません。次の記事で、数値以外のデータを受け渡す方法を説明します。
また、今回とは逆にZigからJavaScriptの関数を呼び出す方法はこちらです。
Discussion