見出し画像

ZigをWasmにコンパイルしてNode.jsから呼び出す

はじめに

SHIFT DAAE部 shinagawa です。 最近、JSのランタイムである「Bun」が話題になっているのを目にしました。 ちょっと調べてみると、スピードやパフォーマンスについて注目されているようでしたが、 特に気になった点は、実装にZigという言語が利用されていることでした。

※ Zigについて

Zigは、アンドリュー・ケリーによって設計された命令型の汎用の静的型付けのコンパイル型システムプログラミング言語である。 この言語は「堅牢性、最適性及び保守性」向けに設計されており、コンパイル時のジェネリクス、リフレクション、クロスコンパイル及び手動メモリ管理をサポートしている。

Zig (プログラミング言語) - Wikipedia

他の言語と比べて情報量がそれほど多くは無かったのですが、実行バイナリを小さくできる点と、 ビルドの標準のオプションでWasmが吐ける点が素晴らしいと思ったので触ってみることにしました。

環境

この検証では以下の環境を利用しています。

  • Windows10 / WSL2

  • Zig 0.9.1

  • Wasmer 2.3.0

  • Node.js 18.2.0

環境構築

まずはWasmを生成するのに必要なコンパイラ基盤であるLLVMとZig本体をhomebrewを使ってインストールします。

$ brew install llvm zig

インストールが完了するとコマンドが利用できる状態になります。

$ zig version
0.9.1

Zigプロジェクト作成

ワークスペースを作成します。

$ mkdir hello-world
$ cd hello-world
$ zig init-exe

init-exeでプロジェクトを初期化すると、次のようにセットアップされます。

$ tree .
.
├── build.zig
└── src
    └── main.zig

src/main.zigを書き換えていきます。まずは簡単にHello Worldできるようにします。

// src/main.zig
const std = @import("std");

pub fn main() anyerror!void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Hello, World!\n", .{});
}

zig runで実行します。Hello World!と出てくれば成功です。

$ zig run src/main.zig 
Hello, World!

Wasmerで実行する

生成したWasmの動作確認を行うために、Wasmerを以下のコマンドでインストールします。

※環境によっては、依存関係の調整が必要な可能性があります。

$ curl https://get.wasmer.io -sSfL | sh

実行権限を付与し、パスを通しておきます。

$ chmod +x ~/.wasmer/bin/wasmer
$ export PATH="~/.wasmer/bin:$PATH"

これでWasmerが利用できる状態となりました。

$ wasmer --version
wasmer 2.3.0

次にWasmとしてコンパイルを行います。

$ zig build-exe -O ReleaseSmall -target wasm32-wasi src/main.zig

実行すると以下の様にmain.wasmが吐き出されます。

$ tree
.
├── build.zig
├── main.wasm
└── src
    └── main.zig

wasmerで実行します。Hello World!と出てくれば成功です。

$ wasmer main.wasm
Hello, World!

Node.jsから呼び出す

最後に、タイトルにある通りNode.jsから呼び出してみます。

まずは JSで呼び出すための関数をsrc/main.zigに定義します。

// src/main.zig
const std = @import("std");

pub fn main() anyerror!void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Hello, World!\n", .{});
}

export fn test1() usize {
    return 1;
}

export fn test2(num1: usize, num2: usize) usize {
    return num1 +| num2;
}

呼び出す側を call_wasm.jsとして作成します。

// call_wasm.js
const fs = require("fs");
const content = fs.readFileSync("./main.wasm");

WebAssembly.compile(content)
  .then((module) => {
    const lib = new WebAssembly.Instance(module, {
      env: {
        memoryBase: 0,
        tableBase: 0,
        memory: new WebAssembly.Memory({ initial: 256 }),
        table: new WebAssembly.Table({ initial: 0, element: "anyfunc" }),
      },
    }).exports;
    // test1
    console.log(lib.test1()); // 1
    // test2
    console.log(lib.test2(128, 128)); // 256
  })
  .catch((e) => {
    console.error(e);
  });

関数 test1、test2 を外から呼び出せるように--exportオプションで関数を指定してコンパイルします。

$ zig build-lib \
  -O ReleaseSmall \
  -target wasm32-wasi \
  -dynamic \
  --export=test1 \
  --export=test2 \
  src/main.zig

2つの関数が呼び出されていることが確認できれば成功です。

$ node call_wasm.js
1
256

所感

Zigについてソースを見ていく中で、Cに近い様な印象を受けました。

エディタの補完機能や構文チェックを行うプラグインは見当たらず、パッケージマネージャーもなさそうなので、 開発言語として利用していくには、これからに期待という感じでした。

補足1:「wasmer: error while loading shared libraries: libtinfo.so.5 ...」が出現する場合

Wasmerの導入・実行にて

wasmer: error while loading shared libraries: libtinfo.so.5: cannot open shared object file: No such file or directory

が出力される場合、libtinfo5をインストールすることで解消することが出来ました。

Ubunutの場合次のコマンドでインストールします。

$ apt install libtinfo5

補足2: Zig の build オプションについて

文章中に出てきたZigのbuildの際のオプションについて触れておきます。

実行可能な出力形式

zig build-exe 、zig build-lib は、それぞれ実行可能ファイル、ライブラリを出力するために使用できます。

クロスコンパイルオプション

-targetでプラットフォームに合わせたコンパイルを行うことができます。

具体的には次のようなパラメータを利用します。

  • wasm32

  • x86_64

  • i386

参考にしたサイト

\もっと身近にもっとリアルに!DAAE公式Twitter/


執筆者: shinagawa
プロダクト開発を行っています。 クラウドとかバックエンド開発関連のことを書いています。

MacBookほしい。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら 
https://recruit.shiftinc.jp/career/


みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!