Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

zwasm

Zig で書かれたスタンドアロンの WebAssembly ランタイムです。CLI ツールとして Wasm モジュールを実行したり、Zig ライブラリとして組み込むことができます。

特徴

  • Wasm 3.0 完全対応: コア仕様 + 9 つのプロポーザル (GC、例外処理、末尾呼び出し、SIMD、スレッドなど)
  • 62,158 件のスペックテストに合格: macOS ARM64 および Linux x86_64 で 100%
  • 4 段階の実行方式: レジスタ IR を備えたインタプリタと ARM64/x86_64 JIT コンパイル
  • WASI Preview 1: デフォルト拒否のケーパビリティモデルによる 46 のシステムコール
  • 小さなフットプリント: バイナリ約 1.4 MB、ランタイムメモリ約 3.5 MB
  • ライブラリと CLI: zig build の依存関係として使用するか、コマンドラインからモジュールを実行
  • WAT サポート: .wat テキスト形式のファイルを直接実行可能

クイックスタート

# WebAssembly モジュールを実行
zwasm hello.wasm

# 特定の関数を呼び出す
zwasm math.wasm --invoke add 2 3

# WAT テキストファイルを実行
zwasm program.wat

インストール方法についてははじめにを参照してください。

はじめに

このガイドでは、ゼロから WebAssembly モジュールを実行するまでを 5 分以内で解説します。

前提条件

インストール

ソースからビルド

git clone https://github.com/clojurewasm/zwasm.git
cd zwasm
zig build -Doptimize=ReleaseSafe

バイナリは zig-out/bin/zwasm に生成されます。PATH の通ったディレクトリにコピーしてください:

cp zig-out/bin/zwasm ~/.local/bin/

インストールスクリプト

curl -fsSL https://raw.githubusercontent.com/clojurewasm/zwasm/main/install.sh | bash

Homebrew (macOS/Linux) — 近日公開予定

brew install clojurewasm/tap/zwasm  # not yet available

インストールの確認

zwasm version

最初のモジュールを実行する

1. WAT ファイルから実行

hello.wat を作成します:

(module
  (func (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))

実行します:

zwasm hello.wat --invoke add 2 3
# Output: 5

2. WASI モジュール

WASI(ファイルシステム、標準出力など)を使用するモジュールの場合:

zwasm hello_wasi.wasm --allow-all

必要な権限のみを付与することもできます:

zwasm hello_wasi.wasm --allow-read --dir ./data

3. モジュールの検査

モジュールのエクスポートとインポートを確認します:

zwasm inspect hello.wasm

4. モジュールの検証

モジュールを実行せずに有効性を検証します:

zwasm validate hello.wasm

Zig ライブラリとして使う

build.zig.zon に zwasm を依存関係として追加します:

.dependencies = .{
    .zwasm = .{
        .url = "https://github.com/clojurewasm/zwasm/archive/refs/tags/v1.1.0.tar.gz",
        .hash = "...",  // zig build will tell you the correct hash
    },
},

次に build.zig で以下を記述します:

const zwasm_dep = b.dependency("zwasm", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("zwasm", zwasm_dep.module("zwasm"));

API の使い方については埋め込みガイドを参照してください。

その他のサンプル

リポジトリには、初級から上級まで順に並んだ 33 個の WAT サンプルが examples/wat/ にあります:

zwasm examples/wat/01_hello_add.wat --invoke add 2 3      # basics
zwasm examples/wat/02_if_else.wat --invoke abs -7          # if/else
zwasm examples/wat/03_loop.wat --invoke sum 100            # loops → 5050
zwasm examples/wat/05_fibonacci.wat --invoke fib 10        # recursion → 55
zwasm examples/wat/24_call_indirect.wat --invoke apply 0 10 3  # tables → 13
zwasm examples/wat/25_return_call.wat --invoke sum 1000000 # tail calls
zwasm examples/wat/30_wasi_hello.wat --allow-all           # WASI → Hi!
zwasm examples/wat/32_wasi_args.wat --allow-all -- hi      # WASI args

各ファイルのヘッダーコメントに実行方法が記載されています。Zig での埋め込みサンプルは examples/zig/ にあります。

次のステップ

CLI リファレンス

コマンド

zwasm run / zwasm <file>

WebAssembly モジュールを実行します。run サブコマンドは省略可能です。zwasm file.wasmzwasm run file.wasm と同等です。

zwasm <file.wasm|.wat> [options] [args...]
zwasm run <file.wasm|.wat> [options] [args...]

デフォルトでは _start(WASI エントリーポイント)を呼び出します。特定のエクスポート関数を呼び出すには --invoke を使用してください。

使用例:

# WASI モジュールを実行(_start を呼び出す)
zwasm hello.wasm --allow-all

# WAT テキスト形式のファイルを実行(コンパイル不要)
zwasm program.wat

# 特定のエクスポート関数を呼び出す
zwasm math.wasm --invoke add 2 3

引数の型

関数の引数は型を認識します。zwasm は関数の型シグネチャを使用して、整数、浮動小数点数、負の数を正しくパースします。

# 整数
zwasm math.wat --invoke add 2 3          # → 5

# 負の数(-- は不要)
zwasm math.wat --invoke negate -5        # → -5
zwasm math.wat --invoke abs -42          # → 42

# 浮動小数点数
zwasm math.wat --invoke double 3.14      # → 6.28
zwasm math.wat --invoke half -6.28       # → -3.14

# 64ビット整数
zwasm math.wat --invoke fib 50           # → 12586269025

結果は自然な形式で表示されます:

  • i32/i64: 符号付き10進数(例: -14294967295 ではなく)
  • f32/f64: 10進数(例: 3.14、生のビット表現ではなく)

引数の数は関数シグネチャに対して検証されます:

zwasm math.wat --invoke add 2             # error: 'add' expects 2 arguments, got 1

WASI モジュール

WASI モジュールは _start を使用し、args_get 経由で文字列引数を受け取ります。WASI の引数と zwasm のオプションを区別するには -- を使用してください:

# WASI モジュールに文字列引数を渡す
zwasm app.wasm --allow-all -- hello world
zwasm app.wasm --allow-read --dir ./data -- input.txt

# 環境変数(注入された変数は --allow-env なしでもアクセス可能)
zwasm app.wasm --env HOME=/tmp --env USER=alice

# サンドボックスモード: 全権限を拒否 + fuel 10億 + メモリ 256MB
zwasm untrusted.wasm --sandbox
zwasm untrusted.wasm --sandbox --allow-read --dir ./data

マルチモジュールリンク

# インポートモジュールをリンクして関数を呼び出す
zwasm app.wasm --link math=math.wasm --invoke compute 42

リソース制限

# 命令数(fuel メータリング)とメモリを制限
zwasm untrusted.wasm --fuel 1000000 --max-memory 16777216

zwasm inspect

モジュールのインポートとエクスポートを表示します。

zwasm inspect [--json] <file.wasm|.wat>
# 人間が読める形式
zwasm inspect examples/wat/01_hello_add.wat

# JSON 出力(スクリプト用)
zwasm inspect --json math.wasm

オプション:

  • --json — JSON 形式で出力

zwasm validate

モジュールを実行せずに妥当性を検証します。

zwasm validate <file.wasm|.wat>

zwasm features

サポートしている WebAssembly プロポーザルの一覧を表示します。

zwasm features [--json]

zwasm version

バージョン文字列を表示します。

zwasm help

使い方の情報を表示します。

run オプション

実行

フラグ説明
--invoke <func>_start の代わりに <func> を呼び出す
--batchバッチモード: stdin から呼び出しコマンドを読み取る
--link name=fileモジュールをインポートソースとしてリンク(繰り返し指定可)

WASI ケーパビリティ

フラグ説明
--sandbox全ケーパビリティを拒否 + fuel 10億 + メモリ 256MB
--allow-allすべての WASI ケーパビリティを付与
--allow-readファイルシステムの読み取りを許可
--allow-writeファイルシステムの書き込みを許可
--allow-env環境変数へのアクセスを許可
--allow-pathパス操作(open, mkdir, unlink)を許可
--dir <path>ホストディレクトリをプリオープン(繰り返し指定可)
--env KEY=VALUEWASI 環境変数を設定(常にアクセス可能)

リソース制限

フラグ説明
--max-memory <N>メモリ上限(バイト単位、memory.grow を制限)
--fuel <N>命令 fuel の上限(使い切るとトラップ)

デバッグ

フラグ説明
--profile実行プロファイルを表示(オペコード頻度、呼び出し回数)
--trace=CATSトレースカテゴリ: jit,regir,exec,mem,call(カンマ区切り)
--dump-regir=N関数インデックス N のレジスタ IR をダンプ
--dump-jit=N関数インデックス N の JIT ディスアセンブリをダンプ

バッチモード

--batch を使用すると、zwasm は stdin から呼び出しコマンドを1行ずつ読み取ります:

add 2 3
mul 4 5
fib 10
echo -e "add 2 3\nmul 4 5" | zwasm math.wasm --batch --invoke add

終了コード

コード意味
0成功
1ランタイムエラー(トラップ、スタックオーバーフロー等)
2不正なモジュールまたはバリデーションエラー
126ファイルが見つからない

組み込みガイド

zwasm を Zig ライブラリとして使用し、アプリケーション内で WebAssembly モジュールをロード・実行できます。

セットアップ

build.zig.zon に zwasm を追加します:

.dependencies = .{
    .zwasm = .{
        .url = "https://github.com/clojurewasm/zwasm/archive/refs/tags/v1.1.0.tar.gz",
        .hash = "...",  // zig build will provide the correct hash
    },
},

build.zig に以下を記述します:

const zwasm_dep = b.dependency("zwasm", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("zwasm", zwasm_dep.module("zwasm"));

基本的な使い方

const zwasm = @import("zwasm");
const WasmModule = zwasm.WasmModule;

// Load a module from bytes
const mod = try WasmModule.load(allocator, wasm_bytes);
defer mod.deinit();

// Call an exported function
var args = [_]u64{ 10, 20 };
var results = [_]u64{0};
try mod.invoke("add", &args, &results);

const sum: i32 = @bitCast(@as(u32, @truncate(results[0])));

ロードのバリエーション

メソッド用途
load(alloc, bytes)基本的なモジュール、WASI なし
loadFromWat(alloc, wat_src)WAT テキスト形式からロード
loadWasi(alloc, bytes)WASI 付きモジュール (cli_default ケーパビリティ)
loadWasiWithOptions(alloc, bytes, opts)カスタム設定の WASI
loadWithImports(alloc, bytes, imports)ホスト関数付きモジュール
loadWasiWithImports(alloc, bytes, imports, opts)WASI とホスト関数の両方
loadWithFuel(alloc, bytes, fuel)命令フューエル制限付き

ホスト関数

ネイティブの Zig 関数を Wasm インポートとして提供できます:

const zwasm = @import("zwasm");

fn hostLog(ctx_ptr: *anyopaque, context: usize) anyerror!void {
    _ = context;
    const vm: *zwasm.Vm = @ptrCast(@alignCast(ctx_ptr));

    // Pop argument from operand stack
    const value = vm.popOperandI32();
    std.debug.print("log: {}\n", .{value});

    // Push return value (if function returns one)
    // try vm.pushOperand(@bitCast(@as(i32, result)));
}

const imports = [_]zwasm.ImportEntry{
    .{
        .module = "env",
        .source = .{ .host_fns = &.{
            .{ .name = "log", .callback = hostLog, .context = 0 },
        }},
    },
};

const mod = try WasmModule.loadWithImports(allocator, wasm_bytes, &imports);

WASI 設定

// loadWasi() defaults to cli_default caps (stdio, clock, random, proc_exit).
// Use loadWasiWithOptions for full access or custom capabilities:
const opts = zwasm.WasiOptions{
    .args = &.{ "my-app", "--verbose" },
    .env_keys = &.{"HOME"},
    .env_vals = &.{"/tmp"},
    .preopen_paths = &.{"./data"},
    .caps = zwasm.Capabilities.all,
};

const mod = try WasmModule.loadWasiWithOptions(allocator, wasm_bytes, opts);

メモリアクセス

モジュールのリニアメモリに対して読み書きできます:

// Read 100 bytes starting at offset 0
const data = try mod.memoryRead(allocator, 0, 100);
defer allocator.free(data);

// Write data at offset 256
try mod.memoryWrite(256, &.{ 0x48, 0x65, 0x6C, 0x6C, 0x6F });

モジュールリンク

複数のモジュールをリンクできます:

// Load the "math" module and register its exports
const math_mod = try WasmModule.load(allocator, math_bytes);
defer math_mod.deinit();
try math_mod.registerExports("math");

// Load another module that imports from "math"
const imports = [_]zwasm.ImportEntry{
    .{ .module = "math", .source = .{ .wasm_module = math_mod } },
};
const app_mod = try WasmModule.loadWithImports(allocator, app_bytes, &imports);
defer app_mod.deinit();

インポートの検査

インスタンス化の前に、モジュールが必要とするインポートを確認できます:

const import_infos = try zwasm.inspectImportFunctions(allocator, wasm_bytes);
defer allocator.free(import_infos);

for (import_infos) |info| {
    std.debug.print("{s}.{s}: {d} params, {d} results\n", .{
        info.module, info.name, info.param_count, info.result_count,
    });
}

リソース制限

リソース使用量を制御できます:

// Fuel limit: traps after N instructions
const mod = try WasmModule.loadWithFuel(allocator, wasm_bytes, 1_000_000);

// Memory limit: via WASI options or direct Vm access

エラーハンドリング

すべてのロード・実行メソッドはエラーユニオンを返します。主要なエラー型は以下のとおりです:

  • error.InvalidWasm — バイナリ形式が不正
  • error.ImportNotFound — 必要なインポートが提供されていない
  • error.Trap — unreachable 命令が実行された
  • error.StackOverflow — 呼び出し深度が 1024 を超過
  • error.OutOfBoundsMemoryAccess — メモリアクセスが範囲外
  • error.OutOfMemory — アロケータが失敗
  • error.FuelExhausted — 命令フューエル制限に到達

完全なリストは エラーリファレンス を参照してください。

アロケータの制御

zwasm はロード時に std.mem.Allocator を受け取り、すべての内部メモリ確保に使用します。アロケータはユーザーが制御できます:

// Use the general purpose allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const mod = try WasmModule.load(gpa.allocator(), wasm_bytes);

// Or use an arena for batch cleanup
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const mod = try WasmModule.load(arena.allocator(), wasm_bytes);

API の安定性

型と関数は 3 つの安定性レベルに分類されます:

  • Stable: SemVer に準拠します。マイナー/パッチリリースで破壊的変更はありません。対象: WasmModuleWasmFnWasmValTypeExportInfoImportEntryHostFnEntryWasiOptions、およびそれらのすべてのパブリックメソッド。

  • Experimental: マイナーリリースで変更される可能性があります。対象: runtime.Storeruntime.Moduleruntime.InstanceloadLinked、WIT 関連関数。

  • Internal: ライブラリ利用者からはアクセスできません。types.zig 以外のソースファイル内のすべての型が対象です。

完全なリストは docs/api-boundary.md を参照してください。

FAQ & トラブルシューティング

一般

zwasm はどの Wasm プロポーザルに対応していますか?

Wasm 3.0 の全 9 プロポーザルに加え、threads、wide arithmetic、custom page sizes に対応しています。詳細は Spec Coverage をご覧ください。

zwasm は Windows に対応していますか?

現時点では対応していません。zwasm は macOS (ARM64) および Linux (x86_64, aarch64) で動作します。JIT とメモリガードページは POSIX API (mmap, mprotect, シグナルハンドラ) を使用しています。

JIT なしで zwasm を使えますか?

はい。デフォルトではインタープリタがすべての関数を処理します。JIT はホットな関数に対してのみ起動されます。特定の関数に対して JIT を無効にする方法はありませんが、呼び出し回数が約 8 回未満の関数は常にインタープリタで実行されます。

WAT パーサとは何ですか?

zwasm は .wat テキスト形式のファイルを直接実行できます: zwasm run program.wat。WAT パーサはコンパイル時に -Dwat=false を指定することで無効化でき、バイナリサイズを削減できます。

トラブルシューティング

“trap: out-of-bounds memory access”

Wasm モジュールがリニアメモリの範囲外のメモリを読み書きしようとしました。これは zwasm ではなく Wasm モジュール側のバグです。モジュールのメモリがデータに対して十分な大きさがあるか確認してください。

“trap: call stack overflow (depth > 1024)”

再帰的な関数呼び出しが深さ 1024 の制限を超えました。これは通常、Wasm モジュール内の無限再帰が原因です。

“required import not found”

モジュールが必要とするインポートが提供されていません。zwasm inspect を使用してモジュールに必要なインポートを確認し、--link またはホスト関数で提供してください。

“invalid wasm binary”

ファイルが有効な WebAssembly バイナリではありません。マジックバイト \0asm とバージョン \01\00\00\00 で始まっているか確認してください。WAT ファイルには .wat 拡張子を使用してください。

パフォーマンスが遅い

  • zig build -Doptimize=ReleaseSafe でビルドしていることを確認してください。デバッグビルドは 5〜10 倍遅くなります。
  • ホットな関数 (多数回呼び出される関数) は自動的に JIT コンパイルされます。実行時間の短いプログラムでは JIT の恩恵を受けられない場合があります。
  • --profile を使用してオペコードの頻度と呼び出し回数を確認できます。

メモリ使用量が多い

  • リニアメモリを持つすべての Wasm モジュールはガードページ (仮想メモリ約 4 GiB、物理メモリではない) を確保します。これは正常な動作で、VSIZE は大きく表示されますが RSS は小さいままです。
  • --max-memory を使用してモジュールが確保できる実際のメモリ量を制限できます。

アーキテクチャ

zwasm は WebAssembly モジュールを複数段のパイプラインで処理します。デコード、バリデーション、レジスタ IR へのプリデコード、そしてインタプリタまたは JIT による実行です。

パイプライン

.wasm binary
      |
      v
+-----------+
|  Decode   |  module.zig -- parse binary format, sections, types
+-----+-----+
      |
      v
+-----------+
| Validate  |  validate.zig -- type checking, operand stack simulation
+-----+-----+
      |
      v
+-----------+
| Predecode |  predecode.zig -- stack machine -> register IR
+-----+-----+
      |
      v
+-----------+
| Regalloc  |  regalloc.zig -- virtual -> physical register assignment
+-----+-----+
      |
      v
+----------------------+
|      Execution       |
|  +----------------+  |
|  |  Interpreter   |  |  vm.zig -- register IR dispatch loop
|  +-------+--------+  |
|          | hot path  |
|  +-------v--------+  |
|  |  JIT Compiler  |  |  jit.zig (ARM64), x86.zig (x86_64)
|  +----------------+  |
+----------------------+

実行ティア

zwasm は自動昇格を備えた階層型実行を採用しています。

  1. インタプリタ — レジスタ IR 命令を直接実行します。すべての関数はここからスタートします。
  2. JIT (ARM64/x86_64) — 関数の呼び出し回数またはバックエッジ回数が閾値を超えると、レジスタ IR がネイティブマシンコードにコンパイルされます。以降の呼び出しではネイティブコードが直接実行されます。

JIT の閾値はアダプティブです。ホットループはバックエッジカウントにより、より速くコンパイルがトリガーされます。

ソースマップ

ファイル役割LOC
module.zigバイナリデコーダ、セクションパース、LEB128~2K
validate.zig型チェッカー、オペランドスタックシミュレーション~1.7K
predecode.zigスタック IR → レジスタ IR 変換~0.7K
regalloc.zig仮想レジスタ → 物理レジスタ割り当て~2K
vm.zigインタプリタ、実行エンジン、ストア~8K
jit.zigARM64 JIT バックエンド~5.9K
x86.zigx86_64 JIT バックエンド~4.7K
types.zigコア型定義、値型~1.3K
opcode.zigオペコード定義 (全581+個)~1.3K
wasi.zigWASI Preview 1 (46 システムコール)~2.6K
gc.zigGC プロポーザル: ヒープ、struct/array 型~1.4K
wat.zigWAT テキストフォーマットパーサー~5.9K
cli.zigCLI フロントエンド~2.1K
instance.zigモジュールインスタンス化、リンク~0.9K
component.zigComponent Model デコーダー~1.9K
wit.zigWIT パーサー~2.1K
canon_abi.zigCanonical ABI~1.2K

レジスタ IR

zwasm は WebAssembly のスタックマシンを直接インタプリトする代わりに、プリデコード時に各関数本体をレジスタベースの中間表現 (IR) に変換します。これにより、実行時のオペランドスタック管理が不要になります。

  • スタック IR: local.get 0 / local.get 1 / i32.add (3つのスタック操作)
  • レジスタ IR: add r2, r0, r1 (1命令)

レジスタ IR は仮想レジスタを使用し、レジスタアロケータによって物理レジスタにマッピングされます。ローカル変数が少ない関数は直接マッピングされ、多い関数はメモリにスピルされます。

JIT コンパイル

JIT コンパイラはレジスタ IR をネイティブマシンコードに変換します。

  • ARM64: フルサポート — 算術演算、制御フロー、浮動小数点、メモリ、call_indirect、SIMD
  • x86_64: フルサポート — ARM64 と同等のカバレッジ

主な JIT 最適化:

  • インライン自己呼び出し (再帰関数がトランポリンのオーバーヘッドなしに自身を呼び出し)
  • スマートスピル/リロード (コールをまたいで生存しているレジスタのみスピル)
  • ダイレクト関数呼び出し (既知のターゲットに対して関数テーブルルックアップをバイパス)
  • デプスガードキャッシング (呼び出し深度チェックをメモリではなくレジスタで実行)

JIT は W^X メモリ保護を使用します。コードは RW ページに書き込まれ、実行前に RX に切り替えられます。シグナルハンドラが JIT コード内のメモリフォルトを Wasm トラップに変換します。

モジュールのインスタンス化

WasmModule.load(bytes)       -> decode + validate + predecode
    |
    v
Instance.instantiate(store)  -> link imports, init memory/tables/globals
    |
    v
Vm.invoke(func_name, args)   -> execute via interpreter or JIT

Store はすべてのランタイム状態を保持します。メモリ、テーブル、グローバル変数、関数インスタンスです。複数のモジュールインスタンスがストアを共有することで、クロスモジュールリンクが可能です。

仕様カバレッジ

zwasm は WebAssembly 3.0 への完全準拠を目指しています。すべての仕様テストが macOS ARM64 および Linux x86_64 で合格しています。

テスト結果: 62,158 / 62,158 (100.0%)

コア仕様

機能オペコード数ステータス
MVP (core)172完了
Sign extension7完了
Non-trapping float-to-int8完了
Bulk memory9完了
Reference types5完了
Multi-value-完了
コア合計201+100%

SIMD

機能オペコード数ステータス
SIMD (v128)236完了
Relaxed SIMD20完了
SIMD 合計256100%

Wasm 3.0 プロポーザル

9 つの Wasm 3.0 プロポーザルすべてが完全に実装されています:

プロポーザルオペコード数仕様テストステータス
Memory64既存を拡張Pass完了
Tail calls2Pass完了
Extended const既存を拡張Pass完了
Branch hintingメタデータセクションPass完了
Multi-memory既存を拡張Pass完了
Relaxed SIMD2085/85完了
Exception handling3Pass完了
Function references5104/106完了
GC31Pass完了

追加プロポーザル

プロポーザルオペコード数ステータス
Threads79 (0xFE prefix)完了 (310/310 spec)
Wide arithmetic4完了 (99/99 e2e)
Custom page sizes-完了 (18/18 e2e)

WASI Preview 1

46 / 46 システムコール実装済み (100%):

カテゴリ関数
args2args_get, args_sizes_get
environ2environ_get, environ_sizes_get
clock2clock_time_get, clock_res_get
fd14read, write, close, seek, stat, prestat, readdir, …
path8open, create_directory, remove, rename, symlink, …
proc2exit, raise
random1random_get
poll1poll_oneoff
sock4NOSYS スタブ

Component Model

機能ステータス
WIT パーサー完了
バイナリデコーダー完了
Canonical ABI完了
WASI P2 アダプター完了
CLI サポート完了

121 件の Component Model テストが合格しています。

WAT パーサー

テキストフォーマットパーサーは以下をサポートしています:

  • v128 を含むすべての値型
  • 名前付きローカル、グローバル、関数、型
  • インラインエクスポートとインポート
  • S 式構文とフラット構文
  • データセクションと要素セクション
  • すべてのプレフィックスオペコード: 0xFC (bulk memory, trunc_sat), 0xFD (SIMD + lane ops), 0xFE (atomics)
  • Wasm 3.0 オペコード: try_table, call_ref, br_on_null, throw_ref など
  • GC プレフィックス (0xFB): GC 型アノテーションと struct/array エンコーディング
  • 100% WAT ラウンドトリップ: 62,156/62,156 の仕様テストモジュールが正しくパース・再エンコード

オペコード総数

カテゴリ
Core201+
SIMD256
GC31
Threads79
その他14+
合計581+

セキュリティモデル

zwasm は、ゲスト(WebAssembly モジュール)とホスト(組み込みアプリケーションまたは CLI)の間に明確な境界を設けています。

信頼境界

+-------------------+    WASI capabilities    +------------------+
|   Guest (Wasm)    | <-- deny-by-default --> |  Host (Zig/CLI)  |
|                   |                         |                  |
| Linear memory     |    Imports/exports      | Native memory    |
| Table entries     | <-- validated types --> | Filesystem, env  |
| Global variables  |                         | Network, OS APIs |
+-------------------+                         +------------------+

正当な Wasm モジュールは、どれほど悪意のあるものであっても、以下のことはできません:

  • 自身のリニアメモリ外のホストメモリを読み書きする
  • 明示的にインポートされていないホスト関数を呼び出す
  • WASI のケーパビリティ制限を回避する
  • バリデーション済みの命令ストリーム外のコードを実行する
  • トラップを発生させずにコールスタックまたはバリュースタックをオーバーフローさせる

防御レイヤー

モジュールデコード

すべてのバイナリ入力は境界チェックされます。リソース制限により過剰なアロケーションを防止します:

  • セクション数: セクションタイプごとに 100〜100,000
  • 関数あたりのローカル変数: 最大 50,000(オーバーフロー対策として飽和演算を使用)
  • ブロックのネスト深度: 500
  • LEB128 の読み取りはバイナリスライスに対して境界チェック済み

バリデーション

コード実行前に Wasm 3.0 の完全な型チェックを行います。62,158 件の spec テストにより正確性を検証しています。

リニアメモリの分離

  • すべての load/store は u33 演算(address + offset)を使用し、32 ビットオーバーフローを防止
  • ガードページ: 4 GiB + 64 KiB の PROT_NONE 領域により、すべての範囲外アクセスを捕捉
  • シグナルハンドラがメモリフォルトを Wasm トラップに変換

JIT セキュリティ

  • W^X: コードページはコンパイル中は RW で、実行前に RX に切り替え。書き込み可能と実行可能が同時に有効になることはありません。
  • すべての分岐ターゲットはレジスタ IR に対してバリデーション済み
  • シグナルハンドラが JIT コード内のフォルトを Wasm トラップに変換

WASI ケーパビリティ

デフォルト拒否モデルで、8 つのケーパビリティフラグがあります:

フラグ制御対象
allow-readファイルシステムの読み取り
allow-writeファイルシステムの書き込み
allow-env環境変数
allow-pathパス操作(open, mkdir, unlink)
allow-clockクロックアクセス
allow-random乱数生成
allow-procプロセス操作
allow-all上記すべて

46 個の WASI 関数のうち 32 個が実行前にケーパビリティをチェックします。残りの 14 個は安全な操作(args のサイズ照会、fd_close など)です。

ライブラリ API のデフォルトloadWasi()): cli_default — stdio、clock、random、proc_exit のみ。フルアクセスが必要なエンベッダは loadWasiWithOptions(.{ .caps = .all }) を使用します。

--sandbox モード: すべてのケーパビリティを拒否し、fuel を 10 億命令に設定し、メモリ上限を 256MB に制限します。--allow-* フラグと組み合わせて選択的にアクセスを許可できます:

zwasm untrusted.wasm --sandbox --allow-read --dir ./data

--env KEY=VALUE: 注入された環境変数は、--allow-env がなくてもゲストから常にアクセス可能です。--allow-env フラグはホスト環境のパススルーアクセスを制御します。

スタック保護

  • コール深度の制限: 1024(すべての呼び出し時にチェック)
  • オペランドスタック: 固定サイズ配列、境界チェック済み
  • ラベルスタック: 境界チェック済み

zwasm が保護しないもの

  • タイミングサイドチャネル: 定数時間の保証なし
  • リソース枯渇: モジュールが無限ループする可能性あり(--fuel で軽減)
  • ホスト関数のバグ: ホスト関数に脆弱性がある場合、Wasm コードがそれを引き起こす可能性あり
  • Spectre/Meltdown: ハードウェアレベルの軽減策なし
  • タイミングによる情報漏洩: JIT コンパイル時間はコード構造によって異なる可能性あり

推奨事項

  • 本番環境では ReleaseSafe でビルド(Zig の境界チェック + オーバーフロー検出)
  • 信頼できないモジュールには --fuel を使用して無限ループを防止
  • --max-memory でメモリ使用量を制限
  • モジュールに必要な WASI ケーパビリティのみを付与
  • 脆弱性の報告については SECURITY.md を参照

パフォーマンス

実行ティア

zwasm は階層型実行を採用しています:

  1. インタープリタ: すべての関数はレジスタ IR として開始され、ディスパッチループで実行されます。起動が速く、コンパイルのオーバーヘッドがありません。
  2. JIT (ARM64/x86_64): ホットな関数は、呼び出し回数またはバックエッジ回数がしきい値を超えるとネイティブコードにコンパイルされます。

JIT が発動する条件

  • 呼び出しのしきい値: 同じ関数が約8回呼び出された後
  • バックエッジカウント: ホットなループは JIT をより早くトリガーします(ループの反復回数がしきい値にカウントされます)
  • 適応型: しきい値は関数の特性に基づいて調整されます

JIT コンパイルが完了すると、その関数への以降のすべての呼び出しはインタープリタをバイパスし、ネイティブマシンコードを直接実行します。

バイナリサイズとメモリ

指標
バイナリサイズ (ReleaseSafe)約 1.4 MB
ランタイムメモリ (fib ベンチマーク)約 3.5 MB RSS
wasmtime バイナリ(比較用)56.3 MB

zwasm は wasmtime の約1/40のサイズです。

ベンチマーク結果

Apple M4 Pro 上で zwasm を wasmtime 41.0.1、Bun 1.3.8、Node v24.13.0 と比較した代表的なベンチマーク。 29 個中 16 個のベンチマークで wasmtime と同等以上の性能。29 個中 25 個が 1.5 倍以内。

ベンチマークzwasmwasmtimeBunNode
nqueens(8)2 ms5 ms14 ms23 ms
nbody(1M)22 ms22 ms32 ms36 ms
gcd(12K,67K)2 ms5 ms14 ms23 ms
tak(24,16,8)5 ms9 ms17 ms29 ms
sieve(1M)5 ms7 ms17 ms29 ms
fib(35)46 ms51 ms36 ms52 ms
st_fib2900 ms674 ms353 ms389 ms

メモリ使用量は wasmtime の 3〜4 分の 1、Bun/Node の 8〜10 分の 1 です。

全結果(29 ベンチマーク): bench/runtime_comparison.yaml

SIMD パフォーマンス

SIMD 操作は機能的に完全です(256 オペコード、仕様テスト 100%)が、レジスタ IR や JIT ではなくスタックインタプリタで実行されます。その結果、SIMD 実行は wasmtime の約 22 倍遅くなります。改善計画: レジスタ IR の v128 拡張、その後選択的な JIT NEON/SSE エミッション。

ベンチマーク手法

すべての測定は hyperfine を使用し、ReleaseSafe ビルドで行っています:

# クイックチェック(1回実行、ウォームアップなし)
bash bench/run_bench.sh --quick

# 完全な測定(3回実行、1回ウォームアップ)
bash bench/run_bench.sh

# 履歴に記録
bash bench/record.sh --id="X" --reason="description"

ベンチマークレイヤー

レイヤー説明
WAT micro5手書き: fib, tak, sieve, nbody, nqueens
TinyGo11TinyGo コンパイラ出力: 同じアルゴリズム + 文字列操作
Shootout5Sightglass shootout スイート (WASI)
Real-world6Rust, C, C++ を Wasm にコンパイル (行列、数学、文字列、ソート)
GC2GC プロポーザル: 構造体アロケーション、木の走査

CI によるリグレッション検出

PR は自動的にパフォーマンスリグレッションがチェックされます:

  • 6つの代表的なベンチマークがベースブランチと PR ブランチの両方で実行されます
  • いずれかのベンチマークが20%以上リグレッションした場合、失敗となります
  • 同一ランナーにより公平な比較が保証されます

パフォーマンスのヒント

  • ReleaseSafe: 本番環境では必ず使用してください。Debug は5〜10倍遅くなります。
  • ホットな関数: 頻繁に呼び出される関数は自動的に JIT コンパイルされます。
  • Fuel 制限: --fuel は命令ごとにオーバーヘッドが加わります。信頼できないコードにのみ使用してください。
  • メモリ: リニアメモリを持つ Wasm モジュールはガードページを割り当てます。初期 RSS はモジュールサイズに関係なく約 3.5 MB です。

メモリモデル

リニアメモリ

各 Wasm モジュールインスタンスは最大1つのリニアメモリを持ちます(multi-memory プロポーザルにより複数持つことも可能です)。リニアメモリは連続したバイト配列であり、Wasm の load/store 命令によってアクセスされます。

アロケーション

  • 初期サイズはモジュール内で指定されます(例: 1ページ = 64 KiB)
  • memory.grow により、指定したページ数だけメモリを拡張できます
  • 最大サイズはモジュール内で指定するか、--max-memory で制限できます

ガードページ

zwasm はリニアメモリの先に 4 GiB + 64 KiB の PROT_NONE 領域を確保します。範囲外アクセスはこのガード領域に到達し、シグナルハンドラによって捕捉されて Wasm トラップに変換されます。これにより、ほとんどのメモリ操作で命令ごとの境界チェックが不要になります。

アドレッシング

すべてのメモリアドレスは u33 演算(32ビットアドレス + 32ビットオフセット)を使用し、オーバーフローを防止します。これにより、address + offset がラップアラウンドして有効なメモリにアクセスしてしまうことがなくなります。

GC ヒープ

GC プロポーザルはマネージドヒープオブジェクト(structs、arrays、i31ref)を導入します。これらは zwasm が管理する別のアリーナに存在します:

  • アリーナアロケータ: オブジェクトは事前に確保されたアリーナから割り当てられます
  • 適応的しきい値: GC コレクションはアロケーション圧に基づいてトリガーされます
  • リファレンスエンコーディング: オペランドスタック上の GC リファレンスはタグ付き u64 値を使用します

GC オブジェクトはリニアメモリからアクセスできず、その逆も同様です。これらは別のアドレス空間に存在します。

アロケータのパラメータ化

zwasm はロード時に std.mem.Allocator を受け取ります。すべての内部アロケーション(モジュールメタデータ、レジスタ IR、テーブルなど)はこのアロケータを経由します。リニアメモリ自体はガードページのサポートのために mmap を直接使用します。

これにより、以下のような使い方が可能です:

  • 通常の用途には汎用アロケータを使用する
  • バッチ処理(ロード、実行、全解放)にはアリーナアロケータを使用する
  • メモリ使用量の監視にはトラッキングアロケータを使用する
  • 組み込み/制約のある環境には固定バッファアロケータを使用する

メモリ制限

リソースデフォルト制限CLI フラグ
リニアメモリモジュール定義の最大値--max-memory <bytes>
コールスタック深度1024設定不可
オペランドスタック固定サイズ設定不可
GC ヒープ無制限(アリーナ)設定不可

他のランタイムとの比較

zwasm と他の WebAssembly ランタイムの比較です。

概要

特徴zwasmwasmtimewasm3wasmer
言語ZigRustCRust/C
バイナリサイズ約 1.4 MB56 MB~100 KB30+ MB
メモリ (fib)3.5 MB12 MB~1 MB15+ MB
実行方式Interp + JITAOT/JITInterpreterAOT/JIT
Wasm 3.0完全対応完全対応部分対応部分対応
GC プロポーザル対応対応非対応非対応
SIMD完全対応 (256 ops)完全対応部分対応完全対応
WASIP1 (46 syscalls)P1 + P2P1 (部分的)P1 + P2
プラットフォームmacOS, LinuxmacOS, Linux, Windows多数 (JIT なし)macOS, Linux, Windows

zwasm を選ぶべきとき

小さなフットプリント: バイナリサイズとメモリ使用量が重要な場合。zwasm は wasmtime の約 40 分の 1 のサイズです。

Zig エコシステム: Zig アプリケーションに組み込む場合。zwasm は C 依存なしのネイティブな zig build 依存関係として統合できます。

仕様の完全性: GC、SIMD、スレッド、例外処理を含む完全な Wasm 3.0 サポートを小さなランタイムで必要とする場合。

高速な起動: インタプリタが即座に実行を開始します。JIT コンパイルはホットな関数に対してバックグラウンドで行われます。

他のランタイムを選ぶべきとき

最大スループット: wasmtime の Cranelift AOT コンパイラは高度に最適化されたネイティブコードを生成します。長時間の計算負荷が高いワークロードでは、wasmtime のほうが高速な場合があります。特に SIMD 多用のワークロードでは、zwasm は現在約 22 倍遅くなります(スタックインタプリタ、SIMD JIT 未実装)。

Windows サポート: zwasm は現在 macOS と Linux をサポートしています。Windows で使用する場合は wasmtime または wasmer を選択してください。

最小サイズ: wasm3 は約 100 KB でマイクロコントローラ上でも動作します。JIT なしで最も小さなランタイムが必要な場合は、wasm3 のほうが適しているかもしれません。

WASI Preview 2: wasmtime は最も完全な WASI P2 実装を備えています。zwasm の P2 サポートは P1 アダプタレイヤーを介して提供されます。

コントリビューターガイド

ビルドとテスト

git clone https://github.com/clojurewasm/zwasm.git
cd zwasm

# ビルド
zig build

# ユニットテストの実行
zig build test

# 特定のテストのみ実行
zig build test -- "Module — rejects excessive locals"

# スペックテストの実行(wasm-tools が必要)
python3 test/spec/run_spec.py --build --summary

# ベンチマークの実行
bash bench/run_bench.sh --quick

必要なツール

  • Zig 0.15.2
  • Python 3(スペックテストランナー用)
  • wasm-tools(スペックテスト変換用)
  • hyperfine(ベンチマーク用)

コード構成

src/
  types.zig       Public API (WasmModule, WasmFn, etc.)
  module.zig      Binary decoder
  validate.zig    Type checker
  predecode.zig   Stack → register IR
  regalloc.zig    Register allocation
  vm.zig          Interpreter + execution engine
  jit.zig         ARM64 JIT backend
  x86.zig         x86_64 JIT backend
  opcode.zig      Opcode definitions
  wasi.zig        WASI Preview 1
  gc.zig          GC proposal
  wat.zig         WAT text format parser
  cli.zig         CLI frontend
  instance.zig    Module instantiation
test/
  spec/           WebAssembly spec tests
  e2e/            End-to-end tests (wasmtime misc_testsuite, 792 assertions)
  fuzz/           Fuzz testing infrastructure
  realworld/      Real-world compatibility tests (30 programs)
bench/
  run_bench.sh    Benchmark runner
  record.sh       Record results to history.yaml
  wasm/           Benchmark wasm modules

開発ワークフロー

  1. フィーチャーブランチを作成: git checkout -b feature/my-change
  2. まず失敗するテストを書く(TDD)
  3. テストを通すための最小限のコードを実装する
  4. テストを実行: zig build test
  5. インタープリターやオペコードを変更した場合は、スペックテストも実行する
  6. 説明的なメッセージでコミットする
  7. main に対してプルリクエストを作成する

コミットガイドライン

  • 1コミットにつき1つの論理的な変更
  • コミットメッセージ: 命令形で簡潔な件名をつける
  • テストの変更はテスト対象のコードと同じコミットに含める

CI チェック

プルリクエストでは以下が自動的にチェックされます:

  • ユニットテストの通過(macOS + Ubuntu)
  • スペックテストの通過(62,158 テスト)
  • E2E テストの通過(792 アサーション)
  • バイナリサイズ <= 1.5 MB
  • ベンチマークの性能劣化が 20% 以内
  • ReleaseSafe ビルドの成功