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