🦁

DenoからZigで作ったWasmを使う

2023/07/29に公開

こちらの記事の続きです。

https://zenn.dev/itte/articles/5ec941edf3f044

実際にZigでWebAssembly(Wasm)を作って、そのWasmをDenoから呼び出してみます。

DenoとZigのインストール

DenoZigは公式サイトに書かれている方法でインストールしてください。
Zig公式サイトに書かれていませんがWindowsではscoopでもインストールできました。

scoop install deno zig

最初のZigファイル

WasmのHello Worldといえば、2つの引数を足し算するだけの関数です。関数にexportを付けるとJavaScriptから呼び出すことができるようになります。

add.zig
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, ポインタ

f64Numberと同じ型です。とはいえすべてf64にしてしまうと可読性が落ちるので、プログラムに適した型を利用したほうが良いと思います。Wasmの場合どうなのか分かりませんが、一般的には浮動小数点型より整数型のほうが演算速度が速いです。

このうちisize型はポインタと同じサイズの符号付き整数型です。Wasmをブラウザ(およびDeno)で実行する場合のポインタサイズは64ビットOSであろうと32ビットにしますので、isizei32と同じです。

ZigをWasmにコンパイル

コンパイルするには次のコマンドを実行します。
(Zig v0.12.0の場合です。バージョンが変わるたびにコマンドがころころ変わっています)

zig build-exe add.zig -target wasm32-freestanding -fno-entry -rdynamic --import-memory -O ReleaseSmall

長いですね。実行すると、add.wasmadd.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のプログラムを実行します。

app.ts
// メモリの確保
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のモジュールにメモリを割り当ててインスタンスオブジェクトを生成しています。
この行を変更することはほとんど無いと思います。

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を使ってみた系の記事はここまでが多いのですが、数値計算だけできてもあまり役に立ちません。次の記事で、数値以外のデータを受け渡す方法を説明します。

https://zenn.dev/itte/articles/57021ace128fff

Discussion