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 分以内で解説します。
前提条件
- Zig 0.15.2 以降
インストール
ソースからビルド
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 を Zig ライブラリとして使う
- 仕様カバレッジ — サポートされている Wasm プロポーザル
CLI リファレンス
コマンド
zwasm run / zwasm <file>
WebAssembly モジュールを実行します。run サブコマンドは省略可能です。zwasm file.wasm は zwasm 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進数(例:
-1、4294967295ではなく) - 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=VALUE | WASI 環境変数を設定(常にアクセス可能) |
リソース制限
| フラグ | 説明 |
|---|---|
--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 に準拠します。マイナー/パッチリリースで破壊的変更はありません。対象:
WasmModule、WasmFn、WasmValType、ExportInfo、ImportEntry、HostFnEntry、WasiOptions、およびそれらのすべてのパブリックメソッド。 -
Experimental: マイナーリリースで変更される可能性があります。対象:
runtime.Store、runtime.Module、runtime.Instance、loadLinked、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 は自動昇格を備えた階層型実行を採用しています。
- インタプリタ — レジスタ IR 命令を直接実行します。すべての関数はここからスタートします。
- 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.zig | ARM64 JIT バックエンド | ~5.9K |
x86.zig | x86_64 JIT バックエンド | ~4.7K |
types.zig | コア型定義、値型 | ~1.3K |
opcode.zig | オペコード定義 (全581+個) | ~1.3K |
wasi.zig | WASI Preview 1 (46 システムコール) | ~2.6K |
gc.zig | GC プロポーザル: ヒープ、struct/array 型 | ~1.4K |
wat.zig | WAT テキストフォーマットパーサー | ~5.9K |
cli.zig | CLI フロントエンド | ~2.1K |
instance.zig | モジュールインスタンス化、リンク | ~0.9K |
component.zig | Component Model デコーダー | ~1.9K |
wit.zig | WIT パーサー | ~2.1K |
canon_abi.zig | Canonical 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 extension | 7 | 完了 |
| Non-trapping float-to-int | 8 | 完了 |
| Bulk memory | 9 | 完了 |
| Reference types | 5 | 完了 |
| Multi-value | - | 完了 |
| コア合計 | 201+ | 100% |
SIMD
| 機能 | オペコード数 | ステータス |
|---|---|---|
| SIMD (v128) | 236 | 完了 |
| Relaxed SIMD | 20 | 完了 |
| SIMD 合計 | 256 | 100% |
Wasm 3.0 プロポーザル
9 つの Wasm 3.0 プロポーザルすべてが完全に実装されています:
| プロポーザル | オペコード数 | 仕様テスト | ステータス |
|---|---|---|---|
| Memory64 | 既存を拡張 | Pass | 完了 |
| Tail calls | 2 | Pass | 完了 |
| Extended const | 既存を拡張 | Pass | 完了 |
| Branch hinting | メタデータセクション | Pass | 完了 |
| Multi-memory | 既存を拡張 | Pass | 完了 |
| Relaxed SIMD | 20 | 85/85 | 完了 |
| Exception handling | 3 | Pass | 完了 |
| Function references | 5 | 104/106 | 完了 |
| GC | 31 | Pass | 完了 |
追加プロポーザル
| プロポーザル | オペコード数 | ステータス |
|---|---|---|
| Threads | 79 (0xFE prefix) | 完了 (310/310 spec) |
| Wide arithmetic | 4 | 完了 (99/99 e2e) |
| Custom page sizes | - | 完了 (18/18 e2e) |
WASI Preview 1
46 / 46 システムコール実装済み (100%):
| カテゴリ | 数 | 関数 |
|---|---|---|
| args | 2 | args_get, args_sizes_get |
| environ | 2 | environ_get, environ_sizes_get |
| clock | 2 | clock_time_get, clock_res_get |
| fd | 14 | read, write, close, seek, stat, prestat, readdir, … |
| path | 8 | open, create_directory, remove, rename, symlink, … |
| proc | 2 | exit, raise |
| random | 1 | random_get |
| poll | 1 | poll_oneoff |
| sock | 4 | NOSYS スタブ |
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 の仕様テストモジュールが正しくパース・再エンコード
オペコード総数
| カテゴリ | 数 |
|---|---|
| Core | 201+ |
| SIMD | 256 |
| GC | 31 |
| Threads | 79 |
| その他 | 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 は階層型実行を採用しています:
- インタープリタ: すべての関数はレジスタ IR として開始され、ディスパッチループで実行されます。起動が速く、コンパイルのオーバーヘッドがありません。
- 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 倍以内。
| ベンチマーク | zwasm | wasmtime | Bun | Node |
|---|---|---|---|---|
| nqueens(8) | 2 ms | 5 ms | 14 ms | 23 ms |
| nbody(1M) | 22 ms | 22 ms | 32 ms | 36 ms |
| gcd(12K,67K) | 2 ms | 5 ms | 14 ms | 23 ms |
| tak(24,16,8) | 5 ms | 9 ms | 17 ms | 29 ms |
| sieve(1M) | 5 ms | 7 ms | 17 ms | 29 ms |
| fib(35) | 46 ms | 51 ms | 36 ms | 52 ms |
| st_fib2 | 900 ms | 674 ms | 353 ms | 389 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 micro | 5 | 手書き: fib, tak, sieve, nbody, nqueens |
| TinyGo | 11 | TinyGo コンパイラ出力: 同じアルゴリズム + 文字列操作 |
| Shootout | 5 | Sightglass shootout スイート (WASI) |
| Real-world | 6 | Rust, C, C++ を Wasm にコンパイル (行列、数学、文字列、ソート) |
| GC | 2 | GC プロポーザル: 構造体アロケーション、木の走査 |
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 ランタイムの比較です。
概要
| 特徴 | zwasm | wasmtime | wasm3 | wasmer |
|---|---|---|---|---|
| 言語 | Zig | Rust | C | Rust/C |
| バイナリサイズ | 約 1.4 MB | 56 MB | ~100 KB | 30+ MB |
| メモリ (fib) | 3.5 MB | 12 MB | ~1 MB | 15+ MB |
| 実行方式 | Interp + JIT | AOT/JIT | Interpreter | AOT/JIT |
| Wasm 3.0 | 完全対応 | 完全対応 | 部分対応 | 部分対応 |
| GC プロポーザル | 対応 | 対応 | 非対応 | 非対応 |
| SIMD | 完全対応 (256 ops) | 完全対応 | 部分対応 | 完全対応 |
| WASI | P1 (46 syscalls) | P1 + P2 | P1 (部分的) | P1 + P2 |
| プラットフォーム | macOS, Linux | macOS, 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
開発ワークフロー
- フィーチャーブランチを作成:
git checkout -b feature/my-change - まず失敗するテストを書く(TDD)
- テストを通すための最小限のコードを実装する
- テストを実行:
zig build test - インタープリターやオペコードを変更した場合は、スペックテストも実行する
- 説明的なメッセージでコミットする
mainに対してプルリクエストを作成する
コミットガイドライン
- 1コミットにつき1つの論理的な変更
- コミットメッセージ: 命令形で簡潔な件名をつける
- テストの変更はテスト対象のコードと同じコミットに含める
CI チェック
プルリクエストでは以下が自動的にチェックされます:
- ユニットテストの通過(macOS + Ubuntu)
- スペックテストの通過(62,158 テスト)
- E2E テストの通過(792 アサーション)
- バイナリサイズ <= 1.5 MB
- ベンチマークの性能劣化が 20% 以内
- ReleaseSafe ビルドの成功