diff --git a/docs/api-boundary.md b/docs/api-boundary.md index cada02b7..02d70f8c 100644 --- a/docs/api-boundary.md +++ b/docs/api-boundary.md @@ -12,6 +12,7 @@ Types and functions listed here are covered by SemVer guarantees. | `WasmValType` | Enum of Wasm value types (i32, i64, f32, f64, v128, funcref, externref) | v0.1.0 | | `ExportInfo` | Metadata for an exported function (name, param/result types) | v0.1.0 | | `ImportEntry` | Maps an import module name to a source | v0.2.0 | +| `WasmModule.Config`| Unified loading configuration | vNEXT | | `ImportSource` | Union: wasm_module or host_fns | v0.2.0 | | `HostFnEntry` | A single host function (name, callback, context) | v0.2.0 | | `HostFn` | Function type: `fn (*anyopaque, usize) anyerror!void` | v0.2.0 | @@ -26,12 +27,13 @@ Types and functions listed here are covered by SemVer guarantees. | Method | Signature | Since | |--------|-----------|-------| +| `loadWithOptions` | `(Allocator, []const u8, Config) !*WasmModule` | vNEXT | | `load` | `(Allocator, []const u8) !*WasmModule` | v0.1.0 | | `loadFromWat` | `(Allocator, []const u8) !*WasmModule` | v0.2.0 | | `loadWasi` | `(Allocator, []const u8) !*WasmModule` | v0.2.0 | | `loadWasiWithOptions` | `(Allocator, []const u8, WasiOptions) !*WasmModule` | v0.2.0 | -| `loadWithImports` | `(Allocator, []const u8, []const ImportEntry) !*WasmModule` | v0.2.0 | -| `loadWasiWithImports` | `(Allocator, []const u8, []const ImportEntry, WasiOptions) !*WasmModule` | v0.2.0 | +| `loadWithImports` | `(Allocator, []const u8, ?[]const ImportEntry) !*WasmModule` | v0.2.0 | +| `loadWasiWithImports` | `(Allocator, []const u8, ?[]const ImportEntry, WasiOptions) !*WasmModule` | v0.2.0 | | `loadWithFuel` | `(Allocator, []const u8, u64) !*WasmModule` | v0.3.0 | | `deinit` | `(*WasmModule) void` | v0.1.0 | | `invoke` | `(*WasmModule, []const u8, []u64, []u64) !void` | v0.1.0 | diff --git a/docs/embedding.md b/docs/embedding.md index 8a60f0d0..df69eb69 100644 --- a/docs/embedding.md +++ b/docs/embedding.md @@ -208,7 +208,7 @@ Key function groups: | Group | Functions | |-------|-----------| -| Config | `zwasm_config_new`, `zwasm_config_delete`, `zwasm_config_set_allocator` | +| Config | `zwasm_config_new`, `zwasm_config_delete`, `zwasm_config_set_allocator`, `zwasm_config_set_fuel`, `..._set_timeout`, `..._set_max_memory`, `..._set_force_interpreter` | | Module | `zwasm_module_new`, `zwasm_module_new_configured`, `zwasm_module_delete` | | WASI | `zwasm_module_new_wasi`, `zwasm_module_new_wasi_configured2` | | Invoke | `zwasm_module_invoke`, `zwasm_module_invoke_start` | diff --git a/include/zwasm.h b/include/zwasm.h index c1ea5a32..c531307e 100644 --- a/include/zwasm.h +++ b/include/zwasm.h @@ -93,6 +93,26 @@ void zwasm_config_set_allocator(zwasm_config_t *config, zwasm_alloc_fn_t alloc_fn, zwasm_free_fn_t free_fn, void *ctx); +/** + * Set the instruction fuel limit. Traps when exhausted. + */ +void zwasm_config_set_fuel(zwasm_config_t *config, uint64_t fuel); + +/** + * Set the execution timeout in milliseconds. + */ +void zwasm_config_set_timeout(zwasm_config_t *config, uint64_t timeout_ms); + +/** + * Set the memory ceiling in bytes (limits memory.grow). + */ +void zwasm_config_set_max_memory(zwasm_config_t *config, uint64_t max_memory_bytes); + +/** + * Force the use of the interpreter only (bypass RegIR and JIT). + */ +void zwasm_config_set_force_interpreter(zwasm_config_t *config, bool force_interpreter); + /* ================================================================ * Module lifecycle * ================================================================ */ diff --git a/src/c_api.zig b/src/c_api.zig index 4beb261e..104ad4a3 100644 --- a/src/c_api.zig +++ b/src/c_api.zig @@ -93,6 +93,11 @@ const CAllocatorWrapper = struct { const CApiConfig = struct { c_alloc: ?*CAllocatorWrapper = null, + fuel: ?u64 = null, + timeout_ms: ?u64 = null, + max_memory_bytes: ?u64 = null, + force_interpreter: bool = false, + fn deinit(self: *CApiConfig) void { if (self.c_alloc) |ca| page_alloc.destroy(ca); } @@ -102,6 +107,16 @@ const CApiConfig = struct { if (self.c_alloc) |ca| return ca.allocator(); return null; } + + /// Build a WasmModule.Config from this C API config. + fn toModuleConfig(self: *CApiConfig) types.WasmModule.Config { + return .{ + .fuel = self.fuel, + .timeout_ms = self.timeout_ms, + .max_memory_bytes = self.max_memory_bytes, + .force_interpreter = self.force_interpreter, + }; + } }; pub const zwasm_config_t = CApiConfig; @@ -126,40 +141,42 @@ const CApiModule = struct { module: *WasmModule, fn create(wasm_bytes: []const u8, wasi: bool) !*CApiModule { - return createWithAllocator(wasm_bytes, wasi, null); + return createConfigured(wasm_bytes, wasi, null); } - fn createWithAllocator(wasm_bytes: []const u8, wasi: bool, custom_alloc: ?std.mem.Allocator) !*CApiModule { + fn createConfigured(wasm_bytes: []const u8, wasi: bool, config: ?*CApiConfig) !*CApiModule { const self = try std.heap.page_allocator.create(CApiModule); errdefer std.heap.page_allocator.destroy(self); - const allocator = custom_alloc orelse default_allocator; + const allocator = if (config) |c| c.getAllocator() orelse default_allocator else default_allocator; + var mod_cfg = if (config) |c| c.toModuleConfig() else types.WasmModule.Config{}; + mod_cfg.wasi = wasi; - self.module = if (wasi) - try WasmModule.loadWasi(allocator, wasm_bytes) - else - try WasmModule.load(allocator, wasm_bytes); + self.module = try WasmModule.loadWithOptions(allocator, wasm_bytes, mod_cfg); return self; } fn createWasiConfigured(wasm_bytes: []const u8, opts: WasiOptions) !*CApiModule { - return createWasiConfiguredWithAllocator(wasm_bytes, opts, null); + return createWasiConfiguredEx(wasm_bytes, opts, null); } - fn createWasiConfiguredWithAllocator(wasm_bytes: []const u8, opts: WasiOptions, custom_alloc: ?std.mem.Allocator) !*CApiModule { + fn createWasiConfiguredEx(wasm_bytes: []const u8, opts: WasiOptions, config: ?*CApiConfig) !*CApiModule { const self = try std.heap.page_allocator.create(CApiModule); errdefer std.heap.page_allocator.destroy(self); - const allocator = custom_alloc orelse default_allocator; + const allocator = if (config) |c| c.getAllocator() orelse default_allocator else default_allocator; + var mod_cfg = if (config) |c| c.toModuleConfig() else types.WasmModule.Config{}; + mod_cfg.wasi = true; + mod_cfg.wasi_options = opts; - self.module = try WasmModule.loadWasiWithOptions(allocator, wasm_bytes, opts); + self.module = try WasmModule.loadWithOptions(allocator, wasm_bytes, mod_cfg); return self; } fn createWithImports(wasm_bytes: []const u8, imports: []const types.ImportEntry) !*CApiModule { const self = try std.heap.page_allocator.create(CApiModule); errdefer std.heap.page_allocator.destroy(self); - self.module = try WasmModule.loadWithImports(default_allocator, wasm_bytes, imports); + self.module = try WasmModule.loadWithOptions(default_allocator, wasm_bytes, .{ .imports = imports }); return self; } @@ -281,6 +298,22 @@ export fn zwasm_config_set_allocator( config.c_alloc = wrapper; } +export fn zwasm_config_set_fuel(config: *zwasm_config_t, fuel: u64) void { + config.fuel = fuel; +} + +export fn zwasm_config_set_timeout(config: *zwasm_config_t, timeout_ms: u64) void { + config.timeout_ms = timeout_ms; +} + +export fn zwasm_config_set_max_memory(config: *zwasm_config_t, max_memory_bytes: u64) void { + config.max_memory_bytes = max_memory_bytes; +} + +export fn zwasm_config_set_force_interpreter(config: *zwasm_config_t, force_interpreter: bool) void { + config.force_interpreter = force_interpreter; +} + // ============================================================ // Module lifecycle // ============================================================ @@ -309,8 +342,7 @@ export fn zwasm_module_new_wasi(wasm_ptr: [*]const u8, len: usize) ?*zwasm_modul /// Pass null for config to use default allocator (same as zwasm_module_new). export fn zwasm_module_new_configured(wasm_ptr: [*]const u8, len: usize, config: ?*zwasm_config_t) ?*zwasm_module_t { clearError(); - const custom_alloc = if (config) |c| c.getAllocator() else null; - return CApiModule.createWithAllocator(wasm_ptr[0..len], false, custom_alloc) catch |err| { + return CApiModule.createConfigured(wasm_ptr[0..len], false, config) catch |err| { setError(err); return null; }; @@ -395,8 +427,7 @@ export fn zwasm_module_new_wasi_configured2( .stdio_ownership = stdio_ownership2, }; - const custom_alloc = if (config) |c| c.getAllocator() else null; - return CApiModule.createWasiConfiguredWithAllocator(wasm_ptr[0..len], opts, custom_alloc) catch |err| { + return CApiModule.createWasiConfiguredEx(wasm_ptr[0..len], opts, config) catch |err| { setError(err); return null; }; @@ -1172,3 +1203,24 @@ test "c_api: module_new_wasi_configured2 with null config" { try testing.expect(module != null); zwasm_module_delete(module.?); } + +test "c_api: config set vm limits" { + const config = zwasm_config_new().?; + defer zwasm_config_delete(config); + + zwasm_config_set_fuel(config, 9999); + zwasm_config_set_timeout(config, 1000); + zwasm_config_set_max_memory(config, 65536); + zwasm_config_set_force_interpreter(config, true); + + const module = zwasm_module_new_configured(MINIMAL_WASM.ptr, MINIMAL_WASM.len, config); + try testing.expect(module != null); + defer zwasm_module_delete(module.?); + + const mod = &module.?.module.*; + try testing.expectEqual(@as(?u64, 9999), mod.vm.fuel); + try testing.expectEqual(@as(?u64, 65536), mod.vm.max_memory_bytes); + try testing.expectEqual(true, mod.vm.force_interpreter); + try testing.expect(mod.vm.deadline_ns != null); +} + diff --git a/src/cli.zig b/src/cli.zig index a58804c3..9e111abb 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -309,6 +309,13 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer func_args_start = args.len; } + const base_cfg = types.WasmModule.Config{ + .fuel = fuel, + .timeout_ms = timeout_ms, + .max_memory_bytes = max_memory_bytes, + .force_interpreter = force_interpreter, + }; + const path = wasm_path orelse { try stderr.print("error: no wasm file specified\n", .{}); try stderr.flush(); @@ -349,22 +356,25 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer return false; }; // Load with already-loaded linked modules as imports (transitive chains) - const lm = if (import_entries.items.len > 0) - types.WasmModule.loadWithImports(allocator, link_bytes, import_entries.items) catch - // Retry without imports if the linked module doesn't need them - types.WasmModule.load(allocator, link_bytes) catch |err| { - allocator.free(link_bytes); - try stderr.print("error: failed to load linked module '{s}': {s}\n", .{ lpath, formatWasmError(err) }); - try stderr.flush(); - return false; + var cfg = base_cfg; + cfg.imports = import_entries.items; + const lm = types.WasmModule.loadWithOptions(allocator, link_bytes, cfg) catch |err| blk: { + // Retry without imports if the linked module doesn't need them + if (err == error.ImportNotFound and cfg.imports.len > 0) { + var retry_cfg = base_cfg; + retry_cfg.imports = &.{}; + break :blk types.WasmModule.loadWithOptions(allocator, link_bytes, retry_cfg) catch |retry_err| { + allocator.free(link_bytes); + try stderr.print("error: failed to load linked module '{s}': {s}\n", .{ lpath, formatWasmError(retry_err) }); + try stderr.flush(); + return false; + }; } - else - types.WasmModule.load(allocator, link_bytes) catch |err| { - allocator.free(link_bytes); - try stderr.print("error: failed to load linked module '{s}': {s}\n", .{ lpath, formatWasmError(err) }); - try stderr.flush(); - return false; - }; + allocator.free(link_bytes); + try stderr.print("error: failed to load linked module '{s}': {s}\n", .{ lpath, formatWasmError(err) }); + try stderr.flush(); + return false; + }; try linked_bytes.append(allocator, link_bytes); try linked_modules.append(allocator, lm); try import_entries.append(allocator, .{ @@ -374,7 +384,7 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer } if (batch_mode) { - return cmdBatch(allocator, wasm_bytes, import_entries.items, link_names.items, linked_modules.items, stdout, stderr, trace_categories, dump_regir_func, dump_jit_func); + return cmdBatch(allocator, wasm_bytes, import_entries.items, link_names.items, linked_modules.items, stdout, stderr, trace_categories, dump_regir_func, dump_jit_func, base_cfg); } const imports_slice: ?[]const types.ImportEntry = if (import_entries.items.len > 0) @@ -395,11 +405,22 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer }; const module = load_blk: { - if (imports_slice != null) { + const wasi_cfg = blk: { + var c = base_cfg; + c.wasi = true; + c.wasi_options = wasi_opts; + break :blk c; + }; + + if (import_entries.items.len > 0) { // With --link: try imports only, then imports + WASI - break :load_blk types.WasmModule.loadWithImports(allocator, wasm_bytes, imports_slice.?) catch |err| { + var imports_only_cfg = base_cfg; + imports_only_cfg.imports = imports_slice.?; + break :load_blk types.WasmModule.loadWithOptions(allocator, wasm_bytes, imports_only_cfg) catch |err| { if (err == error.ImportNotFound) { - break :load_blk types.WasmModule.loadWasiWithImports(allocator, wasm_bytes, imports_slice, wasi_opts) catch |err2| { + var imports_wasi_cfg = wasi_cfg; + imports_wasi_cfg.imports = imports_slice.?; + break :load_blk types.WasmModule.loadWithOptions(allocator, wasm_bytes, imports_wasi_cfg) catch |err2| { try stderr.print("error: failed to load module: {s}\n", .{formatWasmError(err2)}); try stderr.flush(); return false; @@ -411,9 +432,10 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer }; } // No --link: try plain, then WASI - break :load_blk types.WasmModule.load(allocator, wasm_bytes) catch |err| { + const plain_cfg = base_cfg; + break :load_blk types.WasmModule.loadWithOptions(allocator, wasm_bytes, plain_cfg) catch |err| { if (err == error.ImportNotFound) { - break :load_blk types.WasmModule.loadWasiWithOptions(allocator, wasm_bytes, wasi_opts) catch |err2| { + break :load_blk types.WasmModule.loadWithOptions(allocator, wasm_bytes, wasi_cfg) catch |err2| { try stderr.print("error: failed to load module: {s}\n", .{formatWasmError(err2)}); try stderr.flush(); return false; @@ -455,13 +477,6 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer if (trace_categories != 0 or dump_regir_func != null or dump_jit_func != null) { module.vm.trace = &trace_config; } - - // Apply resource limits - module.vm.max_memory_bytes = max_memory_bytes; - module.vm.fuel = fuel; - module.vm.force_interpreter = force_interpreter; - if (timeout_ms) |ms| module.vm.setDeadlineTimeoutMs(ms); - // Lookup export info for type-aware parsing and validation const export_info = module.getExportInfo(func_name); const func_args_slice = args[func_args_start..]; @@ -564,15 +579,20 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer try wasi_args_list.append(allocator, @ptrCast(a)); } - // Run as WASI module (_start), with --link imports if provided - const wasi_opts2: types.WasiOptions = .{ + const wasi_opts: types.WasiOptions = .{ .args = wasi_args_list.items, .env_keys = env_keys.items, .env_vals = env_vals.items, .preopen_paths = preopen_paths.items, .caps = caps, }; - var module = types.WasmModule.loadWasiWithImports(allocator, wasm_bytes, imports_slice, wasi_opts2) catch |err| { + + // Run as WASI module (_start), with --link imports if provided + var cfg = base_cfg; + cfg.wasi = true; + cfg.wasi_options = wasi_opts; + cfg.imports = imports_slice orelse &.{}; + var module = types.WasmModule.loadWithOptions(allocator, wasm_bytes, cfg) catch |err| { try stderr.print("error: failed to load WASI module: {s}\n", .{formatWasmError(err)}); try stderr.flush(); return false; @@ -604,13 +624,6 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer if (trace_categories != 0 or dump_regir_func != null or dump_jit_func != null) { module.vm.trace = &wasi_trace_config; } - - // Apply resource limits - module.vm.max_memory_bytes = max_memory_bytes; - module.vm.fuel = fuel; - module.vm.force_interpreter = force_interpreter; - if (timeout_ms) |ms| module.vm.setDeadlineTimeoutMs(ms); - var no_args = [_]u64{}; var no_results = [_]u64{}; module.invoke("_start", &no_args, &no_results) catch |err| { @@ -671,7 +684,7 @@ fn cmdCompile(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Wr defer allocator.free(wasm_bytes); // Load module (with WASI to handle any imports) - var module = types.WasmModule.loadWasi(allocator, wasm_bytes) catch |err| { + var module = types.WasmModule.loadWithOptions(allocator, wasm_bytes, .{ .wasi = true }) catch |err| { try stderr.print("error: failed to load module: {s}\n", .{formatWasmError(err)}); try stderr.flush(); return false; @@ -1234,20 +1247,15 @@ fn threadRunner(ctx: *ThreadCtx) void { /// Batch mode: read invocations from stdin, one per line. /// Protocol: "invoke [arg1 arg2 ...]" /// Output: "ok [val1 val2 ...]" or "error " -fn cmdBatch(allocator: Allocator, wasm_bytes: []const u8, imports: []const types.ImportEntry, link_names: []const []const u8, linked_modules: []const *types.WasmModule, stdout: *std.Io.Writer, stderr: *std.Io.Writer, trace_categories: u8, dump_regir_func: ?u32, dump_jit_func: ?u32) !bool { +fn cmdBatch(allocator: Allocator, wasm_bytes: []const u8, imports: []const types.ImportEntry, link_names: []const []const u8, linked_modules: []const *types.WasmModule, stdout: *std.Io.Writer, stderr: *std.Io.Writer, trace_categories: u8, dump_regir_func: ?u32, dump_jit_func: ?u32, base_cfg: types.WasmModule.Config) !bool { _ = stderr; - var module = if (imports.len > 0) - types.WasmModule.loadWithImports(allocator, wasm_bytes, imports) catch |err| { - try stdout.print("error load {s}\n", .{@errorName(err)}); - try stdout.flush(); - return false; - } - else - types.WasmModule.load(allocator, wasm_bytes) catch |err| { - try stdout.print("error load {s}\n", .{@errorName(err)}); - try stdout.flush(); - return false; - }; + var cfg = base_cfg; + cfg.imports = imports; + var module = types.WasmModule.loadWithOptions(allocator, wasm_bytes, cfg) catch |err| { + try stdout.print("error load {s}\n", .{@errorName(err)}); + try stdout.flush(); + return false; + }; defer module.deinit(); // Enable tracing if requested diff --git a/src/component.zig b/src/component.zig index 78c95c89..a132185e 100644 --- a/src/component.zig +++ b/src/component.zig @@ -1144,7 +1144,7 @@ pub const ComponentInstance = struct { pub fn instantiate(self: *ComponentInstance) !void { // Phase 1: Load embedded core modules for (self.comp.core_modules.items) |mod_bytes| { - const m = try types.WasmModule.load(self.alloc, mod_bytes); + const m = try types.WasmModule.loadWithOptions(self.alloc, mod_bytes, .{}); self.core_modules.append(self.alloc, m) catch return error.OutOfMemory; } @@ -1164,9 +1164,9 @@ pub const ComponentInstance = struct { // Phase 1: Load embedded core modules with imports for (self.comp.core_modules.items) |mod_bytes| { const m = if (imports.len > 0) - try types.WasmModule.loadWithImports(self.alloc, mod_bytes, imports) + try types.WasmModule.loadWithOptions(self.alloc, mod_bytes, .{ .imports = imports }) else - try types.WasmModule.load(self.alloc, mod_bytes); + try types.WasmModule.loadWithOptions(self.alloc, mod_bytes, .{}); self.core_modules.append(self.alloc, m) catch return error.OutOfMemory; } diff --git a/src/fuzz_gen.zig b/src/fuzz_gen.zig index e3ada46b..17905420 100644 --- a/src/fuzz_gen.zig +++ b/src/fuzz_gen.zig @@ -1721,8 +1721,8 @@ fn loadAndExercise(alloc: Allocator, wasm: []const u8) void { // Call multiple times to trigger JIT compilation for (0..FUZZ_JIT_CALLS) |_| { + module.fuel = FUZZ_FUEL; module.invoke(ei.name, arg_slice, result_slice) catch break; - module.vm.fuel = FUZZ_FUEL; } } } diff --git a/src/fuzz_loader.zig b/src/fuzz_loader.zig index c06be0e6..f8f237df 100644 --- a/src/fuzz_loader.zig +++ b/src/fuzz_loader.zig @@ -48,7 +48,7 @@ fn loadModule(allocator: std.mem.Allocator, input: []const u8) ?*zwasm.WasmModul const m = zwasm.WasmModule.loadWasiWithOptions(allocator, input, .{ .caps = zwasm.Capabilities.sandbox, }) catch return null; - m.vm.fuel = FUEL_LIMIT; + m.fuel = FUEL_LIMIT; return m; } @@ -79,8 +79,8 @@ fn fuzzOne(allocator: std.mem.Allocator, input: []const u8) void { // Call multiple times to trigger JIT compilation for (0..JIT_CALLS) |_| { + module.fuel = FUEL_LIMIT; module.invoke(ei.name, arg_slice, result_slice) catch break; - module.vm.fuel = FUEL_LIMIT; } } } diff --git a/src/fuzz_wat_loader.zig b/src/fuzz_wat_loader.zig index f7f03c2a..bb9fe778 100644 --- a/src/fuzz_wat_loader.zig +++ b/src/fuzz_wat_loader.zig @@ -64,8 +64,8 @@ fn fuzzOne(allocator: std.mem.Allocator, wat_source: []const u8) void { // Call multiple times to trigger JIT compilation for (0..JIT_CALLS) |_| { + module.fuel = FUEL_LIMIT; module.invoke(ei.name, arg_slice, result_slice) catch break; - module.vm.fuel = FUEL_LIMIT; } } } diff --git a/src/module.zig b/src/module.zig index fdfc9c08..2f5d21a2 100644 --- a/src/module.zig +++ b/src/module.zig @@ -2059,7 +2059,7 @@ test "fuzz — full pipeline (load+instantiate) does not panic" { var results: [1]u64 = .{0}; const result_slice = results[0..ei.result_types.len]; module.invoke(ei.name, &.{}, result_slice) catch continue; - module.vm.fuel = 100_000; + module.fuel = 100_000; } } } diff --git a/src/types.zig b/src/types.zig index 604936cd..7bb536c8 100644 --- a/src/types.zig +++ b/src/types.zig @@ -232,15 +232,39 @@ pub const WasmModule = struct { vm: *rt.vm_mod.Vm = undefined, /// Owned wasm bytes (from WAT conversion). Freed on deinit. owned_wasm_bytes: ?[]const u8 = null, + /// Persistent fuel budget from Config. Decremented across all invocations. + fuel: ?u64 = null, + /// Persistent timeout setting from Config. Applied at start of every invocation. + timeout_ms: ?u64 = null, + /// Persistent memory limit from Config. Applied at start of every invocation. + max_memory_bytes: ?u64 = null, + /// Persistent interpreter-only setting from Config. + force_interpreter: bool = false, + + /// Configuration for module loading. + pub const Config = struct { + wasi: bool = false, + wasi_options: ?WasiOptions = null, + imports: []const ImportEntry = &.{}, + fuel: ?u64 = null, + timeout_ms: ?u64 = null, + max_memory_bytes: ?u64 = null, + force_interpreter: bool = false, + }; + + /// Load a Wasm module from binary bytes with explicit configuration. + pub fn loadWithOptions(allocator: Allocator, wasm_bytes: []const u8, config: Config) !*WasmModule { + return loadCore(allocator, wasm_bytes, config); + } /// Load a Wasm module from binary bytes, decode, and instantiate. pub fn load(allocator: Allocator, wasm_bytes: []const u8) !*WasmModule { - return loadCore(allocator, wasm_bytes, false, null, null); + return loadWithOptions(allocator, wasm_bytes, .{}); } /// Load with a fuel limit (traps start function if it exceeds the limit). pub fn loadWithFuel(allocator: Allocator, wasm_bytes: []const u8, fuel: u64) !*WasmModule { - return loadCore(allocator, wasm_bytes, false, null, fuel); + return loadWithOptions(allocator, wasm_bytes, .{ .fuel = fuel }); } /// Load a module from WAT (WebAssembly Text Format) source. @@ -248,7 +272,7 @@ pub const WasmModule = struct { pub fn loadFromWat(allocator: Allocator, wat_source: []const u8) !*WasmModule { const wasm_bytes = try rt.wat.watToWasm(allocator, wat_source); errdefer allocator.free(wasm_bytes); - const self = try loadCore(allocator, wasm_bytes, false, null, null); + const self = try loadWithOptions(allocator, wasm_bytes, .{}); self.owned_wasm_bytes = wasm_bytes; return self; } @@ -257,14 +281,14 @@ pub const WasmModule = struct { pub fn loadFromWatWithFuel(allocator: Allocator, wat_source: []const u8, fuel: u64) !*WasmModule { const wasm_bytes = try rt.wat.watToWasm(allocator, wat_source); errdefer allocator.free(wasm_bytes); - const self = try loadCore(allocator, wasm_bytes, false, null, fuel); + const self = try loadWithOptions(allocator, wasm_bytes, .{ .fuel = fuel }); self.owned_wasm_bytes = wasm_bytes; return self; } /// Load a WASI module — registers wasi_snapshot_preview1 imports. pub fn loadWasi(allocator: Allocator, wasm_bytes: []const u8) !*WasmModule { - return loadCore(allocator, wasm_bytes, true, null, null); + return loadWithOptions(allocator, wasm_bytes, .{ .wasi = true }); } /// Apply WasiOptions to a WasiContext (shared logic for all WASI loaders). @@ -301,31 +325,17 @@ pub const WasmModule = struct { /// Load a WASI module with custom args, env, and preopened directories. pub fn loadWasiWithOptions(allocator: Allocator, wasm_bytes: []const u8, opts: WasiOptions) !*WasmModule { - const self = try loadCore(allocator, wasm_bytes, true, null, null); - errdefer self.deinit(); - - if (self.wasi_ctx) |*wc| { - try applyWasiOptions(wc, opts); - } - - return self; + return loadWithOptions(allocator, wasm_bytes, .{ .wasi = true, .wasi_options = opts }); } /// Load with imports from other modules or host functions. - pub fn loadWithImports(allocator: Allocator, wasm_bytes: []const u8, imports: []const ImportEntry) !*WasmModule { - return loadCore(allocator, wasm_bytes, false, imports, null); + pub fn loadWithImports(allocator: Allocator, wasm_bytes: []const u8, imports: ?[]const ImportEntry) !*WasmModule { + return loadWithOptions(allocator, wasm_bytes, .{ .imports = imports orelse &.{} }); } /// Load with combined WASI + import support. Used by CLI for --link + WASI fallback. pub fn loadWasiWithImports(allocator: Allocator, wasm_bytes: []const u8, imports: ?[]const ImportEntry, opts: WasiOptions) !*WasmModule { - const self = try loadCore(allocator, wasm_bytes, true, imports, null); - errdefer self.deinit(); - - if (self.wasi_ctx) |*wc| { - try applyWasiOptions(wc, opts); - } - - return self; + return loadWithOptions(allocator, wasm_bytes, .{ .wasi = true, .wasi_options = opts, .imports = imports orelse &.{} }); } /// Register this module's exports in its store under the given module name. @@ -365,10 +375,15 @@ pub const WasmModule = struct { self.owned_wasm_bytes = null; self.store = rt.store_mod.Store.init(allocator); self.wasi_ctx = null; + self.timeout_ms = null; + self.fuel = null; + self.max_memory_bytes = null; + self.force_interpreter = false; self.module = rt.module_mod.Module.init(allocator, wasm_bytes); self.module.decode() catch |err| { self.module.deinit(); + self.store.deinit(); allocator.destroy(self); return err; }; @@ -379,6 +394,7 @@ pub const WasmModule = struct { self.instance.instantiateBase() catch |err| { self.instance.deinit(); self.module.deinit(); + self.store.deinit(); allocator.destroy(self); return err; }; @@ -404,7 +420,7 @@ pub const WasmModule = struct { return .{ .module = self, .apply_error = apply_error }; } - fn loadCore(allocator: Allocator, wasm_bytes: []const u8, wasi: bool, imports: ?[]const ImportEntry, fuel: ?u64) !*WasmModule { + fn loadCore(allocator: Allocator, wasm_bytes: []const u8, config: Config) !*WasmModule { const self = try allocator.create(WasmModule); errdefer allocator.destroy(self); @@ -417,7 +433,7 @@ pub const WasmModule = struct { errdefer self.module.deinit(); try self.module.decode(); - if (wasi) { + if (config.wasi) { try rt.wasi.registerAll(&self.store, &self.module); self.wasi_ctx = rt.wasi.WasiContext.init(allocator); self.wasi_ctx.?.caps = rt.wasi.Capabilities.cli_default; @@ -426,8 +442,14 @@ pub const WasmModule = struct { } errdefer if (self.wasi_ctx) |*wc| wc.deinit(); - if (imports) |import_entries| { - try registerImports(&self.store, &self.module, import_entries, allocator); + if (self.wasi_ctx) |*wc| { + if (config.wasi_options) |opts| { + try applyWasiOptions(wc, opts); + } + } + + if (config.imports.len > 0) { + try registerImports(&self.store, &self.module, config.imports, allocator); } self.instance = rt.instance_mod.Instance.init(allocator, &self.store, &self.module); @@ -440,14 +462,26 @@ pub const WasmModule = struct { self.wit_funcs = &[_]wit_parser.WitFunc{}; self.vm = try allocator.create(rt.vm_mod.Vm); + errdefer allocator.destroy(self.vm); self.vm.* = rt.vm_mod.Vm.init(allocator); - self.vm.fuel = fuel; + self.max_memory_bytes = config.max_memory_bytes; + self.force_interpreter = config.force_interpreter; + self.timeout_ms = config.timeout_ms; + self.fuel = config.fuel; + self.vm.max_memory_bytes = self.max_memory_bytes; + self.vm.force_interpreter = self.force_interpreter; + self.vm.fuel = self.fuel; + self.vm.setDeadlineTimeoutMs(self.timeout_ms); // Execute start function if present if (self.module.start) |start_idx| { self.vm.reset(); - self.vm.fuel = fuel; + self.vm.fuel = self.fuel; + self.vm.max_memory_bytes = self.max_memory_bytes; + self.vm.force_interpreter = self.force_interpreter; + self.vm.setDeadlineTimeoutMs(self.timeout_ms); try self.vm.invokeByIndex(&self.instance, start_idx, &.{}, &.{}); + self.fuel = self.vm.fuel; } return self; @@ -476,6 +510,11 @@ pub const WasmModule = struct { /// Args and results are passed as u64 arrays. pub fn invoke(self: *WasmModule, name: []const u8, args: []const u64, results: []u64) !void { self.vm.reset(); + self.vm.fuel = self.fuel; + self.vm.max_memory_bytes = self.max_memory_bytes; + self.vm.force_interpreter = self.force_interpreter; + self.vm.setDeadlineTimeoutMs(self.timeout_ms); + defer self.fuel = self.vm.fuel; try self.vm.invoke(&self.instance, name, args, results); } @@ -483,8 +522,12 @@ pub const WasmModule = struct { /// Used by differential testing to get a reference result. pub fn invokeInterpreterOnly(self: *WasmModule, name: []const u8, args: []const u64, results: []u64) !void { self.vm.reset(); + self.vm.fuel = self.fuel; + self.vm.max_memory_bytes = self.max_memory_bytes; self.vm.force_interpreter = true; - defer self.vm.force_interpreter = false; + self.vm.setDeadlineTimeoutMs(self.timeout_ms); + defer self.vm.force_interpreter = self.force_interpreter; + defer self.fuel = self.vm.fuel; try self.vm.invoke(&self.instance, name, args, results); } @@ -1385,3 +1428,57 @@ test "loadWasiWithOptions explicit all grants full access" { try testing.expect(caps.allow_env); try testing.expect(caps.allow_path); } + +test "force_interpreter — persistence across invokeInterpreterOnly calls" { + if (!@import("build_options").enable_wat) return error.SkipZigTest; + var wasm_mod = try WasmModule.loadFromWat(testing.allocator, + \\(module + \\ (func (export "f") (result i32) + \\ i32.const 42 + \\ ) + \\) + ); + defer wasm_mod.deinit(); + + // Set force_interpreter to true on load + wasm_mod.force_interpreter = true; + wasm_mod.vm.force_interpreter = true; + + var results = [_]u64{0}; + + // 1. Regular invoke should follow persistent setting + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == true); + try testing.expectEqual(@as(u64, 42), results[0]); + + // 2. invokeInterpreterOnly should restore the persistent setting afterward + try wasm_mod.invokeInterpreterOnly("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == true); // must NOT be reset to false + try testing.expectEqual(@as(u64, 42), results[0]); + + // 3. Subsequent regular invoke still sees interpreter mode + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == true); + try testing.expectEqual(@as(u64, 42), results[0]); + + // 4. Clearing the flag persists to the next invoke + wasm_mod.force_interpreter = false; + try wasm_mod.invoke("f", &.{}, &results); + try testing.expect(wasm_mod.vm.force_interpreter == false); +} + +test "WasmModule.Config applies VM limits" { + const wasm_bytes = &[_]u8{ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00 }; + var wasm_mod = try WasmModule.loadWithOptions(testing.allocator, wasm_bytes, .{ + .fuel = 12345, + .timeout_ms = 5000, + .max_memory_bytes = 1048576, + .force_interpreter = true, + }); + defer wasm_mod.deinit(); + + try testing.expectEqual(@as(?u64, 12345), wasm_mod.vm.fuel); + try testing.expectEqual(@as(?u64, 1048576), wasm_mod.vm.max_memory_bytes); + try testing.expectEqual(true, wasm_mod.vm.force_interpreter); + try testing.expect(wasm_mod.vm.deadline_ns != null); +} diff --git a/test/c_api/test_ffi.c b/test/c_api/test_ffi.c index 1e3c6e07..51be221c 100644 --- a/test/c_api/test_ffi.c +++ b/test/c_api/test_ffi.c @@ -89,6 +89,10 @@ typedef const char *(*fn_last_error)(void); /* Config */ typedef zwasm_config_t (*fn_config_new)(void); typedef void (*fn_config_delete)(zwasm_config_t); +typedef void (*fn_config_set_fuel)(zwasm_config_t, uint64_t); +typedef void (*fn_config_set_timeout)(zwasm_config_t, uint64_t); +typedef void (*fn_config_set_max_memory)(zwasm_config_t, uint64_t); +typedef void (*fn_config_set_force_interpreter)(zwasm_config_t, bool); /* Imports */ typedef zwasm_imports_t (*fn_import_new)(void); @@ -128,6 +132,10 @@ static struct { fn_last_error last_error; fn_config_new config_new; fn_config_delete config_delete; + fn_config_set_fuel config_set_fuel; + fn_config_set_timeout config_set_timeout; + fn_config_set_max_memory config_set_max_memory; + fn_config_set_force_interpreter config_set_force_interpreter; fn_import_new import_new; fn_import_delete import_delete; fn_import_add_fn import_add_fn; @@ -173,6 +181,10 @@ static bool load_api(const char *path) { LOAD_SYM(last_error, "zwasm_last_error_message"); LOAD_SYM(config_new, "zwasm_config_new"); LOAD_SYM(config_delete, "zwasm_config_delete"); + LOAD_SYM(config_set_fuel, "zwasm_config_set_fuel"); + LOAD_SYM(config_set_timeout, "zwasm_config_set_timeout"); + LOAD_SYM(config_set_max_memory, "zwasm_config_set_max_memory"); + LOAD_SYM(config_set_force_interpreter, "zwasm_config_set_force_interpreter"); LOAD_SYM(import_new, "zwasm_import_new"); LOAD_SYM(import_delete, "zwasm_import_delete"); LOAD_SYM(import_add_fn, "zwasm_import_add_fn"); @@ -450,6 +462,15 @@ static void test_config_lifecycle(void) { zwasm_config_t cfg = api.config_new(); ASSERT(cfg != NULL, "config_new"); + api.config_set_fuel(cfg, 10000); + api.config_set_timeout(cfg, 5000); + api.config_set_max_memory(cfg, 65536); + api.config_set_force_interpreter(cfg, true); + + zwasm_module_t mod2 = api.module_new_configured(RETURN42_WASM, sizeof(RETURN42_WASM), cfg); + ASSERT(mod2 != NULL, "module_new_configured(cfg)"); + if (mod2) api.module_delete(mod2); + if (cfg) api.config_delete(cfg); /* Configured module (NULL config = default) */