Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .dev/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -601,3 +601,47 @@ or `own` (runtime closes on teardown).
to keep the header cross-language friendly.

**Affected files**: wasi.zig, types.zig, c_api.zig, include/zwasm.h, test_ffi.c.

## D134: Async Execution Cancellation

**Context**: Issue #27 / PR #28. Embedders need to abort a running Wasm invocation
from another thread — for instance to enforce user-initiated cancel buttons on top
of Wasm plugins, or to unwind infinite loops that escape fuel/deadline budgets.
Pre-set fuel limits and deadline timeouts were the only existing escape hatches;
both are decided before execution starts.

**Decision**: Expose a thread-safe `cancel()` request on `Vm` / `WasmModule` /
`zwasm_module_t`. The interpreter and JIT both poll the flag at their existing
periodic budget checkpoints, so the new path reuses proven machinery rather than
adding a second interrupt mechanism.

**Mechanism**:
- `Vm.cancelled: std.atomic.Value(bool)` — `release` store on cancel, `acquire`
load on check.
- Interpreter path: `consumeInstructionBudget()` already fired every
`DEADLINE_CHECK_INTERVAL` (1024) instructions for deadline polling; cancel
checks piggyback on the same checkpoint with no extra branch on the hot path.
- JIT path: `armJitFuel` caps `jit_fuel` to `DEADLINE_JIT_INTERVAL` when
cancellation is armed, so `jitFuelCheckHelper` fires periodically even when
no fuel/deadline is set. The helper returns error code `11` (`Canceled`) back
into the trampoline.
- `reset()` clears the flag — each `invoke()` starts from a clean state, and
cancel requests issued against an idle module are dropped. This is documented
on both the Zig and C APIs; the FFI test races cancels across invoke start
to cover the corresponding window.

**Opt-out**: `Vm.cancellable: bool = true` (Zig default) / `cancellable: ?bool`
in `WasmModule.Config` / `zwasm_config_set_cancellable(config, false)`. Disabling
restores `jit_fuel = maxInt(i64)` for fuel/deadline-free runs, recovering pre-PR
throughput for hosts that never need cancel.

**API surface**:
- Zig: `Vm.cancel()`, `WasmModule.cancel()`, `error.Canceled`,
`WasmModule.Config.cancellable: ?bool`.
- C: `void zwasm_module_cancel(zwasm_module_t *)`,
`void zwasm_config_set_cancellable(zwasm_config_t *, bool)`.
- CLI: `error.Canceled` prints "execution canceled".

**Affected files**: vm.zig, types.zig, cli.zig, c_api.zig, include/zwasm.h,
test/c_api/test_ffi.c, docs/{embedding,errors,usage,api-boundary}.md,
book/{en,ja}/src/{c-api,embedding-guide}.md.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).

## [Unreleased]

### Added
- Asynchronous execution cancellation (PR #28 by @jtakakura, closes #27). A host thread can now abort a running invocation:
- Zig API: `WasmModule.cancel()` / `Vm.cancel()` — thread-safe, returns `error.Canceled` from `invoke()` at the next ~1024-instruction checkpoint (or JIT fuel interval).
- C API: `void zwasm_module_cancel(zwasm_module_t *)` and `void zwasm_config_set_cancellable(zwasm_config_t *, bool)`.
- CLI: reports `execution canceled` when the runtime returns `error.Canceled`.
- `WasmModule.Config.cancellable: ?bool` — opt-out of periodic cancellation checks for peak JIT throughput when the host never cancels.

### Changed
- By default, JIT-compiled loops now fire the fuel-check helper every `DEADLINE_JIT_INTERVAL` iterations even when no fuel/deadline is set, so cancellation takes effect without host instrumentation. Pass `cancellable = false` to restore the pre-PR unconditional `jit_fuel = maxInt(i64)` behaviour.

## [1.8.0] - 2026-04-21

### Added
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,15 @@ zwasm_module_invoke(mod, "f", NULL, 0, results, 1);
zwasm_module_delete(mod);
```

For configured execution limits and behavior, use `zwasm_config_t`:

- `zwasm_config_set_fuel`, `zwasm_config_set_timeout`, `zwasm_config_set_max_memory`
- `zwasm_config_set_force_interpreter`
- `zwasm_config_set_cancellable` (default: `true`)

When fuel is configured, it applies to module startup (`start`/`_start`) as well as
subsequent invocations. Fuel consumed during startup reduces the remaining budget.

See the [C API chapter](https://clojurewasm.github.io/zwasm/en/c-api.html) in the book for the full API reference.

## Examples
Expand Down
64 changes: 64 additions & 0 deletions bench/history.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3629,3 +3629,67 @@ entries:
rw_cpp_string_cached: {time_ms: 5.1}
rw_cpp_sort: {time_ms: 3.4}
rw_cpp_sort_cached: {time_ms: 4.3}
- id: "PR28"
date: "2026-04-24"
reason: "execution cancellation API (D134)"
commit: "3bad07a"
build: ReleaseSafe
results:
fib: {time_ms: 48.8}
fib_cached: {time_ms: 49.6}
tak: {time_ms: 7.3}
tak_cached: {time_ms: 6.3}
sieve: {time_ms: 4.9}
sieve_cached: {time_ms: 6.2}
nbody: {time_ms: 26.3}
nbody_cached: {time_ms: 22.3}
nqueens: {time_ms: 3.0}
nqueens_cached: {time_ms: 2.1}
tgo_fib: {time_ms: 33.9}
tgo_fib_cached: {time_ms: 36.3}
tgo_tak: {time_ms: 6.3}
tgo_tak_cached: {time_ms: 7.8}
tgo_arith: {time_ms: 2.6}
tgo_arith_cached: {time_ms: 2.3}
tgo_sieve: {time_ms: 3.7}
tgo_sieve_cached: {time_ms: 2.9}
tgo_fib_loop: {time_ms: 1.5}
tgo_fib_loop_cached: {time_ms: 3.4}
tgo_gcd: {time_ms: 2.7}
tgo_gcd_cached: {time_ms: 2.3}
tgo_nqueens: {time_ms: 56.7}
tgo_nqueens_cached: {time_ms: 57.2}
tgo_mfr: {time_ms: 50.0}
tgo_mfr_cached: {time_ms: 44.6}
tgo_list: {time_ms: 60.1}
tgo_list_cached: {time_ms: 60.3}
tgo_rwork: {time_ms: 7.2}
tgo_rwork_cached: {time_ms: 8.0}
tgo_strops: {time_ms: 83.5}
tgo_strops_cached: {time_ms: 85.9}
st_fib2: {time_ms: 889.9}
st_fib2_cached: {time_ms: 885.9}
st_sieve: {time_ms: 181.0}
st_sieve_cached: {time_ms: 181.7}
st_nestedloop: {time_ms: 2.1}
st_nestedloop_cached: {time_ms: 2.0}
st_ackermann: {time_ms: 5.6}
st_ackermann_cached: {time_ms: 4.4}
st_matrix: {time_ms: 289.0}
st_matrix_cached: {time_ms: 289.2}
gc_alloc: {time_ms: 6.0}
gc_alloc_cached: {time_ms: 5.5}
gc_tree: {time_ms: 33.1}
gc_tree_cached: {time_ms: 25.7}
rw_rust_fib: {time_ms: 38.8}
rw_rust_fib_cached: {time_ms: 36.6}
rw_c_matrix: {time_ms: 4.3}
rw_c_matrix_cached: {time_ms: 3.1}
rw_c_math: {time_ms: 22.5}
rw_c_math_cached: {time_ms: 19.1}
rw_c_string: {time_ms: 44.8}
rw_c_string_cached: {time_ms: 45.8}
rw_cpp_string: {time_ms: 10.2}
rw_cpp_string_cached: {time_ms: 7.0}
rw_cpp_sort: {time_ms: 4.1}
rw_cpp_sort_cached: {time_ms: 3.6}
40 changes: 33 additions & 7 deletions book/en/src/c-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,19 @@ Functions are grouped by domain. All signatures live in `include/zwasm.h`.
|----------|-------------|
| `zwasm_last_error_message()` | Last error as a null-terminated string. Returns `""` if no error. Thread-local. |

### Runtime configuration

| Function | Description |
|----------|-------------|
| `zwasm_config_new()` | Create a runtime config handle. |
| `zwasm_config_delete(config)` | Free a runtime config handle. |
| `zwasm_config_set_allocator(config, alloc_fn, free_fn, ctx)` | Set custom allocator callbacks for runtime bookkeeping memory. |
| `zwasm_config_set_fuel(config, fuel)` | Set instruction fuel limit. |
| `zwasm_config_set_timeout(config, timeout_ms)` | Set wall-clock timeout in milliseconds. |
| `zwasm_config_set_max_memory(config, max_memory_bytes)` | Set linear-memory growth ceiling in bytes. |
| `zwasm_config_set_force_interpreter(config, force_interpreter)` | Disable RegIR/JIT and force interpreter-only execution. |
| `zwasm_config_set_cancellable(config, enabled)` | Enable/disable periodic JIT cancellation checks. |

### Module lifecycle

| Function | Description |
Expand All @@ -160,6 +173,8 @@ Functions are grouped by domain. All signatures live in `include/zwasm.h`.
| `zwasm_module_new_wasi(wasm_ptr, len)` | Create WASI module with default capabilities. |
| `zwasm_module_new_wasi_configured(wasm_ptr, len, config)` | Create WASI module with custom config. |
| `zwasm_module_new_with_imports(wasm_ptr, len, imports)` | Create module with host function imports. |
| `zwasm_module_new_configured(wasm_ptr, len, config)` | Create module with optional runtime config. |
| `zwasm_module_new_wasi_configured2(wasm_ptr, len, wasi_config, config)` | Create WASI module with both WASI config and runtime config. |
| `zwasm_module_delete(module)` | Free all module resources. |
| `zwasm_module_validate(wasm_ptr, len)` | Validate binary without instantiation. |

Expand All @@ -169,6 +184,7 @@ Functions are grouped by domain. All signatures live in `include/zwasm.h`.
|----------|-------------|
| `zwasm_module_invoke(module, name, args, nargs, results, nresults)` | Invoke an exported function by name. |
| `zwasm_module_invoke_start(module)` | Invoke `_start` (WASI entry point). |
| `zwasm_module_cancel(module)` | Request cancellation of a currently running invocation (thread-safe). |

### Export introspection

Expand Down Expand Up @@ -273,26 +289,35 @@ The `env` pointer lets you pass arbitrary context (a struct, file handle, etc.)

## WASI programs

Use the config builder pattern to run WASI programs with custom settings:
Use a `zwasm_wasi_config_t` for argv/env/preopens, and optionally combine it with `zwasm_config_t` for fuel/timeout/memory limits:

```c
/* Create and configure WASI */
zwasm_wasi_config_t *config = zwasm_wasi_config_new();
zwasm_wasi_config_t *wasi_config = zwasm_wasi_config_new();

const char *argv[] = {"myapp", "--verbose"};
zwasm_wasi_config_set_argv(config, 2, argv);
zwasm_wasi_config_set_argv(wasi_config, 2, argv);

zwasm_wasi_config_preopen_dir(wasi_config, "/tmp/data", 9, "/data", 5);

zwasm_wasi_config_preopen_dir(config, "/tmp/data", 9, "/data", 5);
/* Optional runtime config */
zwasm_config_t *config = zwasm_config_new();
zwasm_config_set_fuel(config, 1000000);
zwasm_config_set_timeout(config, 1000);
zwasm_config_set_max_memory(config, 256 * 1024 * 1024);

/* Create module with WASI config */
zwasm_module_t *mod = zwasm_module_new_wasi_configured(wasm_bytes, wasm_len, config);
/* Create module with both configs */
zwasm_module_t *mod = zwasm_module_new_wasi_configured2(
wasm_bytes, wasm_len, wasi_config, config
);

/* Run the program */
zwasm_module_invoke_start(mod);

/* Cleanup */
zwasm_module_delete(mod);
zwasm_wasi_config_delete(config);
zwasm_config_delete(config);
zwasm_wasi_config_delete(wasi_config);
```

For simple WASI programs that only need default capabilities (stdio, clock, random):
Expand All @@ -307,6 +332,7 @@ zwasm_module_delete(mod);

- **Error buffer**: `zwasm_last_error_message()` returns a thread-local buffer. Safe to call from multiple threads.
- **Modules**: A `zwasm_module_t` is **not** thread-safe. Do not invoke functions on the same module from multiple threads concurrently. Create separate module instances per thread instead.
- **Cancellation**: `zwasm_module_cancel()` is the only thread-safe operation and may be called from another thread to interrupt a running invocation.

## Next steps

Expand Down
35 changes: 30 additions & 5 deletions book/en/src/embedding-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,40 @@ for (import_infos) |info| {
}
```

## Resource limits

Control resource usage:
## Resource limits and Config options

In Zig, resource and execution options are grouped in `WasmModule.Config` and passed to `loadWithOptions`.
This allows you to control:

- **fuel**: Instruction count limit (prevents infinite loops)
- **timeout_ms**: Wall-clock timeout (milliseconds)
- **max_memory_bytes**: Maximum linear memory size
- **force_interpreter**: Disable JIT, always use interpreter

Example (Zig):

```zig
// Fuel limit: traps after N instructions
const mod = try WasmModule.loadWithFuel(allocator, wasm_bytes, 1_000_000);
const zwasm = @import("zwasm");
const Config = zwasm.WasmModule.Config;

// Memory limit: via WASI options or direct Vm access
var config = Config{
.fuel = 1_000_000, // Trap after 1M instructions
.timeout_ms = 1000, // 1 second wall-clock timeout
.max_memory_bytes = 16 * 1024 * 1024, // 16MB
.force_interpreter = false,
};
const mod = try WasmModule.loadWithOptions(allocator, wasm_bytes, config);
```

**fuel**: If set, the module will trap with `error.FuelExhausted` after the specified number of instructions. Use this for untrusted or potentially infinite-looping code.

**cancellation**: `mod.cancel()` can be called from another thread to interrupt an in-progress invocation.

**timeout_ms**: If set, execution will be interrupted after the given wall-clock time.

All options are optional; defaults are safe for most use cases. See the C API section for equivalent `zwasm_config_t` usage.

## Error handling

All loading and execution methods return error unions. Key error types:
Expand All @@ -170,6 +193,8 @@ All loading and execution methods return error unions. Key error types:
- **`error.OutOfBoundsMemoryAccess`** — Memory access out of bounds
- **`error.OutOfMemory`** — Allocator failed
- **`error.FuelExhausted`** — Instruction fuel limit hit
- **`error.Canceled`** — Execution canceled by host via `cancel()`
- **`error.TimeoutExceeded`** — Execution interrupted by wall-clock timeout

See [Error Reference](../docs/errors.md) for the complete list.

Expand Down
40 changes: 33 additions & 7 deletions book/ja/src/c-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,19 @@ cd examples/rust && cargo run
|------|------|
| `zwasm_last_error_message()` | 最後のエラーを null 終端文字列で返す。エラーなしの場合は `""` を返す。スレッドローカル。 |

### ランタイム設定

| 関数 | 説明 |
|------|------|
| `zwasm_config_new()` | ランタイム設定ハンドルを作成。 |
| `zwasm_config_delete(config)` | ランタイム設定ハンドルを解放。 |
| `zwasm_config_set_allocator(config, alloc_fn, free_fn, ctx)` | ランタイム内部管理メモリ向けのカスタムアロケータを設定。 |
| `zwasm_config_set_fuel(config, fuel)` | 命令fuel上限を設定。 |
| `zwasm_config_set_timeout(config, timeout_ms)` | 実時間タイムアウト(ミリ秒)を設定。 |
| `zwasm_config_set_max_memory(config, max_memory_bytes)` | 線形メモリ `memory.grow` の上限(バイト)を設定。 |
| `zwasm_config_set_force_interpreter(config, force_interpreter)` | RegIR/JITを無効化し、インタプリタ実行を強制。 |
| `zwasm_config_set_cancellable(config, enabled)` | JIT実行中のキャンセルチェック有効/無効を設定。 |

### モジュールのライフサイクル

| 関数 | 説明 |
Expand All @@ -160,6 +173,8 @@ cd examples/rust && cargo run
| `zwasm_module_new_wasi(wasm_ptr, len)` | デフォルトケーパビリティで WASI モジュールを作成。 |
| `zwasm_module_new_wasi_configured(wasm_ptr, len, config)` | カスタム設定で WASI モジュールを作成。 |
| `zwasm_module_new_with_imports(wasm_ptr, len, imports)` | ホスト関数インポート付きでモジュールを作成。 |
| `zwasm_module_new_configured(wasm_ptr, len, config)` | ランタイム設定付きでモジュールを作成。 |
| `zwasm_module_new_wasi_configured2(wasm_ptr, len, wasi_config, config)` | WASI設定とランタイム設定の両方を指定してモジュールを作成。 |
| `zwasm_module_delete(module)` | モジュールの全リソースを解放。 |
| `zwasm_module_validate(wasm_ptr, len)` | インスタンス化せずにバイナリを検証。 |

Expand All @@ -169,6 +184,7 @@ cd examples/rust && cargo run
|------|------|
| `zwasm_module_invoke(module, name, args, nargs, results, nresults)` | エクスポート関数を名前で呼び出す。 |
| `zwasm_module_invoke_start(module)` | `_start`(WASI エントリポイント)を呼び出す。 |
| `zwasm_module_cancel(module)` | 実行中呼び出しのキャンセルを要求(スレッドセーフ)。 |

### エクスポートの検査

Expand Down Expand Up @@ -273,26 +289,35 @@ int main(void) {

## WASI プログラム

設定ビルダーパターンを使用して、カスタム設定で WASI プログラムを実行できます:
`zwasm_wasi_config_t` で argv/env/preopen を設定し、必要に応じて `zwasm_config_t` で fuel/timeout/メモリ上限を併用できます:

```c
/* WASI の設定 */
zwasm_wasi_config_t *config = zwasm_wasi_config_new();
zwasm_wasi_config_t *wasi_config = zwasm_wasi_config_new();

const char *argv[] = {"myapp", "--verbose"};
zwasm_wasi_config_set_argv(config, 2, argv);
zwasm_wasi_config_set_argv(wasi_config, 2, argv);

zwasm_wasi_config_preopen_dir(wasi_config, "/tmp/data", 9, "/data", 5);

zwasm_wasi_config_preopen_dir(config, "/tmp/data", 9, "/data", 5);
/* 任意: ランタイム設定 */
zwasm_config_t *config = zwasm_config_new();
zwasm_config_set_fuel(config, 1000000);
zwasm_config_set_timeout(config, 1000);
zwasm_config_set_max_memory(config, 256 * 1024 * 1024);

/* WASI 設定付きでモジュールを作成 */
zwasm_module_t *mod = zwasm_module_new_wasi_configured(wasm_bytes, wasm_len, config);
/* WASI設定 + ランタイム設定でモジュールを作成 */
zwasm_module_t *mod = zwasm_module_new_wasi_configured2(
wasm_bytes, wasm_len, wasi_config, config
);

/* プログラムを実行 */
zwasm_module_invoke_start(mod);

/* クリーンアップ */
zwasm_module_delete(mod);
zwasm_wasi_config_delete(config);
zwasm_config_delete(config);
zwasm_wasi_config_delete(wasi_config);
```

デフォルトケーパビリティ (stdio, clock, random) のみの単純な WASI プログラムの場合:
Expand All @@ -307,6 +332,7 @@ zwasm_module_delete(mod);

- **エラーバッファ**: `zwasm_last_error_message()` はスレッドローカルバッファを返します。複数スレッドからの呼び出しは安全です。
- **モジュール**: `zwasm_module_t` はスレッドセーフ**ではありません**。同一モジュールに対して複数スレッドから同時に関数を呼び出さないでください。スレッドごとに個別のモジュールインスタンスを作成してください。
- **キャンセル**: `zwasm_module_cancel()` は唯一のスレッドセーフな操作であり、他スレッドから実行中の呼び出しを中断できます。

## 次のステップ

Expand Down
Loading
Loading