diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d978ec36..9b94fd14 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -26,6 +26,44 @@ jobs: - uses: mlugg/setup-zig@v2.0.5 - name: Run testsuite run: zig build testsuite + wasi-testsuite-discover: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.discover.outputs.matrix }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Fetch WASI testsuite + run: | + git clone --depth 1 --branch prod/testsuite-base https://github.com/WebAssembly/wasi-testsuite.git /tmp/wasi-testsuite + - name: Discover tests + id: discover + run: | + MATRIX=$(python3 test/wasi-testsuite/discover-wasi-tests.py /tmp/wasi-testsuite --format github-matrix) + echo "matrix=$MATRIX" >> $GITHUB_OUTPUT + + wasi-testsuite: + needs: wasi-testsuite-discover + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.wasi-testsuite-discover.outputs.matrix) }} + runs-on: ubuntu-latest + name: wasi-${{ matrix.suite }}-${{ matrix.test }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v4 + with: + python-version: '3.x' + - uses: mlugg/setup-zig@v2.0.5 + - name: Fetch WASI testsuite + run: | + git clone --depth 1 --branch prod/testsuite-base https://github.com/WebAssembly/wasi-testsuite.git /tmp/wasi-testsuite + pip3 install -r /tmp/wasi-testsuite/test-runner/requirements.txt + - name: Run WASI test ${{ matrix.suite }}/${{ matrix.test }} + run: zig build wasi-${{ matrix.suite }}-${{ matrix.test }} + lint: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 1694882f..ec37cd20 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,10 @@ zig-cache .zig-cache zig-out -test/testrunner/bin \ No newline at end of file +test/testrunner/bin + +# WASI testsuite generated files +test/wasi-testsuite/discovered_tests.json +__pycache__/ +**/__pycache__/ +*.pyc \ No newline at end of file diff --git a/build.zig b/build.zig index 8d58a9f5..69bb5304 100644 --- a/build.zig +++ b/build.zig @@ -1,4 +1,5 @@ -const Build = @import("std").Build; +const std = @import("std"); +const Build = std.Build; pub fn build(b: *Build) !void { const target = b.standardTargetOptions(.{}); @@ -60,9 +61,93 @@ pub fn build(b: *Build) !void { testsuite_step.dependOn(&run_test.step); } + // WASI testsuite integration - individual and aggregated test steps + const wasi_testsuite_dep = b.dependency("wasi-testsuite", .{}); + + // Aggregated step that runs all WASI tests at once + const wasi_testsuite_step = b.step("wasi-testsuite", "Run WASI testsuite tests (all languages)"); + const run_wasi_tests = b.addSystemCommand(&.{ + "python3", + "test-runner/wasi_test_runner.py", + "-t", + "tests/c/testsuite/wasm32-wasip1", + "tests/rust/testsuite/wasm32-wasip1", + "tests/assemblyscript/testsuite/wasm32-wasip1", + "-r", + b.pathFromRoot("test/wasi-testsuite/adapter/zware.py"), + }); + run_wasi_tests.setCwd(wasi_testsuite_dep.path(".")); + run_wasi_tests.setEnvironmentVariable("ZWARE_RUN", b.getInstallPath(.bin, "zware-run")); + run_wasi_tests.step.dependOn(b.getInstallStep()); + wasi_testsuite_step.dependOn(&run_wasi_tests.step); + + // Individual test steps - dynamically discovered from testsuite + // Create a helper function to discover and register tests for a suite + const TestSuite = struct { + key: []const u8, + path: []const u8, + }; + + const test_suites = [_]TestSuite{ + .{ .key = "c", .path = "tests/c/testsuite/wasm32-wasip1" }, + .{ .key = "rust", .path = "tests/rust/testsuite/wasm32-wasip1" }, + .{ .key = "as", .path = "tests/assemblyscript/testsuite/wasm32-wasip1" }, + }; + + for (test_suites) |suite| { + // Get the actual path to the testsuite directory + const suite_dir_path = wasi_testsuite_dep.path(suite.path).getPath(b); + + // Open and scan the directory for .wasm files + var suite_dir = std.fs.cwd().openDir(suite_dir_path, .{ .iterate = true }) catch continue; + defer suite_dir.close(); + + var test_list: std.ArrayList([]const u8) = .empty; + defer test_list.deinit(b.allocator); + + var iter = suite_dir.iterate(); + while (iter.next() catch null) |entry| { + if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".wasm")) { + const test_name = entry.name[0 .. entry.name.len - 5]; // remove .wasm extension + const test_name_owned = b.allocator.dupe(u8, test_name) catch continue; + test_list.append(b.allocator, test_name_owned) catch continue; + } + } + + // Sort test names for deterministic ordering + std.mem.sort([]const u8, test_list.items, {}, struct { + fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool { + return std.mem.lessThan(u8, lhs, rhs); + } + }.lessThan); + + // Create individual build steps for each discovered test + for (test_list.items) |test_name| { + const run_test = b.addSystemCommand(&.{ + "python3", + b.pathFromRoot("test/wasi-testsuite/run-single-wasi-test.py"), + suite.path, + test_name, + "-r", + b.pathFromRoot("test/wasi-testsuite/adapter/zware.py"), + "--test-runner", + "test-runner/wasi_test_runner.py", + }); + run_test.setCwd(wasi_testsuite_dep.path(".")); + run_test.setEnvironmentVariable("ZWARE_RUN", b.getInstallPath(.bin, "zware-run")); + run_test.step.dependOn(b.getInstallStep()); + b.step( + b.fmt("wasi-{s}-{s}", .{ suite.key, test_name }), + b.fmt("Run WASI {s} test: {s}", .{ suite.key, test_name }), + ).dependOn(&run_test.step); + } + } + const test_step = b.step("test", "Run all the tests"); test_step.dependOn(unittest_step); test_step.dependOn(testsuite_step); + // Note: WASI testsuite is not included in default test step due to external dependencies + // Run it explicitly with: zig build wasi-testsuite { const exe = b.addExecutable(.{ diff --git a/build.zig.zon b/build.zig.zon index 248ba622..7181a449 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -12,6 +12,10 @@ .url = "https://github.com/WebAssembly/testsuite/archive/e25ae159357c055b3a6fac99043644e208d26d2a.tar.gz", .hash = "N-V-__8AAKtbtgBFkB_BMIKSUqC-temKvHmuqBSvjBlf4hD6", }, + .@"wasi-testsuite" = .{ + .url = "https://github.com/WebAssembly/wasi-testsuite/archive/refs/heads/prod/testsuite-base.tar.gz", + .hash = "N-V-__8AAHAN1wCGU7h8wcRWoFr0TLZ_5M4KqbBYKfkLqIoY", + }, }, .paths = .{ "build.zig", diff --git a/src/instance.zig b/src/instance.zig index 5f739f8a..b984ded3 100644 --- a/src/instance.zig +++ b/src/instance.zig @@ -463,6 +463,15 @@ pub const Instance = struct { }); } + /// Inherit stdin, stdout, and stderr from the host process. + /// Maps WASI fds 0, 1, 2 to host fds 0, 1, 2 respectively. + /// This matches the behavior of wasmtime's inherit_stdio(). + pub fn inheritStdio(self: *Instance) !void { + try self.addWasiPreopen(0, "stdin", 0); + try self.addWasiPreopen(1, "stdout", 1); + try self.addWasiPreopen(2, "stderr", 2); + } + // FIXME: hide any allocation / deinit inside Instance // Caller must call std.process.argsFree on returned args // diff --git a/src/wasi/wasi.zig b/src/wasi/wasi.zig index de74ce05..817e5773 100644 --- a/src/wasi/wasi.zig +++ b/src/wasi/wasi.zig @@ -126,6 +126,10 @@ pub fn fd_close(vm: *VirtualMachine) WasmError!void { const fd = vm.popOperand(i32); const host_fd = vm.getHostFd(fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } posix.close(host_fd); try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); @@ -138,14 +142,25 @@ pub fn fd_fdstat_get(vm: *VirtualMachine) WasmError!void { const memory = try vm.inst.getMemory(0); const host_fd = vm.getHostFd(fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } const file = fs.File{ .handle = host_fd }; const stat = file.stat() catch |err| { try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); return; }; - try memory.write(u16, stat_ptr, 0x00, @intFromEnum(toWasiFileType(stat.kind))); - try memory.write(u16, stat_ptr, 0x02, 0); + // Write fdstat structure: + // offset 0x00: u8 fs_filetype + // offset 0x02: u16 fs_flags + // offset 0x08: u64 fs_rights_base + // offset 0x10: u64 fs_rights_inheriting + try memory.write(u8, stat_ptr, 0x00, @intFromEnum(toWasiFileType(stat.kind))); + try memory.write(u16, stat_ptr, 0x02, 0); // fs_flags + + // Grant all rights for now (FIXME: should be more restrictive) try memory.write(u64, stat_ptr, 0x08, math.maxInt(u64)); try memory.write(u64, stat_ptr, 0x10, math.maxInt(u64)); @@ -168,6 +183,10 @@ pub fn fd_filestat_get(vm: *VirtualMachine) WasmError!void { const memory = try vm.inst.getMemory(0); const host_fd = vm.getHostFd(fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } const file = std.fs.File{ .handle = host_fd }; const stat = file.stat() catch |err| { @@ -194,10 +213,11 @@ pub fn fd_prestat_get(vm: *VirtualMachine) WasmError!void { const memory = try vm.inst.getMemory(0); if (vm.lookupWasiPreopen(fd)) |preopen| { - const some_other_ptr = try memory.read(u32, prestat_ptr, 0); - const name_len_ptr = try memory.read(u32, prestat_ptr, 4); - try memory.write(u32, some_other_ptr, 0, 0); - try memory.write(u32, name_len_ptr, 0, @as(u32, @intCast(preopen.name.len))); + // Write prestat structure: + // offset 0: u8 tag (0 = PREOPENTYPE_DIR) + // offset 4: u32 pr_name_len + try memory.write(u8, prestat_ptr, 0, 0); // tag = 0 (PREOPENTYPE_DIR) + try memory.write(u32, prestat_ptr, 4, @as(u32, @intCast(preopen.name.len))); try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); } else { @@ -229,6 +249,10 @@ pub fn fd_read(vm: *VirtualMachine) WasmError!void { const data = memory.memory(); const host_fd = vm.getHostFd(fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } var i: u32 = 0; var total_read: usize = 0; @@ -260,28 +284,34 @@ pub fn fd_seek(vm: *VirtualMachine) WasmError!void { const offset = vm.popOperand(i64); const fd = vm.popOperand(i32); + const host_fd = vm.getHostFd(fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } + switch (relative_to) { wasi.whence_t.CUR => { - posix.lseek_CUR(fd, offset) catch |err| { + posix.lseek_CUR(host_fd, offset) catch |err| { try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); return; }; }, wasi.whence_t.END => { - posix.lseek_END(fd, offset) catch |err| { + posix.lseek_END(host_fd, offset) catch |err| { try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); return; }; }, wasi.whence_t.SET => { - posix.lseek_SET(fd, @intCast(offset)) catch |err| { + posix.lseek_SET(host_fd, @intCast(offset)) catch |err| { try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); return; }; }, } - const new_offset = posix.lseek_CUR_get(fd) catch |err| { + const new_offset = posix.lseek_CUR_get(host_fd) catch |err| { try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); return; }; @@ -302,6 +332,10 @@ pub fn fd_write(vm: *VirtualMachine) WasmError!void { const data = memory.memory(); const host_fd = vm.getHostFd(fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } var n: usize = 0; var i: u32 = 0; @@ -350,7 +384,21 @@ pub fn path_filestat_get(vm: *VirtualMachine) WasmError!void { const sub_path = data[path_ptr .. path_ptr + path_len]; + // Validate path for security: reject absolute paths and parent directory references + if (sub_path.len > 0 and sub_path[0] == '/') { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.NOTCAPABLE)); + return; + } + if (mem.indexOf(u8, sub_path, "..") != null) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.NOTCAPABLE)); + return; + } + const host_fd = vm.getHostFd(fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } const dir: fs.Dir = .{ .fd = host_fd }; const stat = dir.statFile(sub_path) catch |err| { try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); @@ -393,6 +441,10 @@ pub fn path_open(vm: *VirtualMachine) WasmError!void { const sub_path = data[path_ptr .. path_ptr + path_len]; const host_fd = vm.getHostFd(dir_fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } const flags = posix.O{ .CREAT = oflags.CREAT, @@ -409,31 +461,184 @@ pub fn path_open(vm: *VirtualMachine) WasmError!void { break :blk .RDWR; } else if (fs_rights_base.FD_WRITE) blk: { break :blk .WRONLY; - } else if (fs_rights_base.FD_READ) blk: { + } else blk: { + // Default to RDONLY if no rights or only FD_READ break :blk .RDONLY; - } else unreachable, + }, }; const mode = 0o644; - const opened_fd = posix.openat(host_fd, sub_path, flags, mode) catch |err| { - try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); - return; + + // Try to open with the requested flags first + const opened_fd = posix.openat(host_fd, sub_path, flags, mode) catch |err| blk: { + // If ISDIR error and we didn't request O_DIRECTORY, try again with O_DIRECTORY and RDONLY + if (err == error.IsDir and !oflags.DIRECTORY) { + var retry_flags = flags; + retry_flags.DIRECTORY = true; + retry_flags.ACCMODE = .RDONLY; + break :blk posix.openat(host_fd, sub_path, retry_flags, mode) catch |retry_err| { + try vm.pushOperand(u64, @intFromEnum(toWasiError(retry_err))); + return; + }; + } else { + try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); + return; + } }; + // Return the host fd directly to WASM + // The opened file is not added to preopens (only preopened directories go there) + // WASM will use this host fd directly via passthrough in getHostFd() try memory.write(i32, fd_ptr, 0, opened_fd); try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); } +/// Read the contents of a symbolic link. +/// path_readlink(fd, path, path_len, buf, buf_len, bufused) -> errno +pub fn path_readlink(vm: *VirtualMachine) WasmError!void { + const bufused_ptr = vm.popOperand(u32); + const buf_len = vm.popOperand(u32); + const buf_ptr = vm.popOperand(u32); + const path_len = vm.popOperand(u32); + const path_ptr = vm.popOperand(u32); + const dir_fd = vm.popOperand(i32); + + const memory = try vm.inst.getMemory(0); + const data = memory.memory(); + + const sub_path = data[path_ptr..][0..path_len]; + const buf = data[buf_ptr..][0..buf_len]; + + // Validate path for security: reject absolute paths and parent directory references + if (sub_path.len > 0 and sub_path[0] == '/') { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.NOTCAPABLE)); + return; + } + if (mem.indexOf(u8, sub_path, "..") != null) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.NOTCAPABLE)); + return; + } + + const host_fd = vm.getHostFd(dir_fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } + + const result = posix.readlinkat(host_fd, sub_path, buf) catch |err| { + try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); + return; + }; + + try memory.write(u32, bufused_ptr, 0, @intCast(result.len)); + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); +} + // FIXME: implement pub fn poll_oneoff(vm: *VirtualMachine) WasmError!void { - const param0 = vm.popOperand(i32); - const param1 = vm.popOperand(i32); - const param2 = vm.popOperand(i32); - const param3 = vm.popOperand(i32); - std.debug.print("Unimplemented: poll_oneoff({}, {}, {}, {})\n", .{ param0, param1, param2, param3 }); - try vm.pushOperand(u64, 0); - @panic("Unimplemented: poll_oneoff"); + const nevents_ptr = vm.popOperand(u32); + const nsubscriptions = vm.popOperand(u32); + const out_ptr = vm.popOperand(u32); + const in_ptr = vm.popOperand(u32); + + const memory = try vm.inst.getMemory(0); + + // For now, implement a simple blocking poll that waits on file descriptors + // This is sufficient for basic socket operations + + if (nsubscriptions == 0) { + try memory.write(u32, nevents_ptr, 0, 0); + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); + return; + } + + // Allocate poll_fds array + var poll_fds_buf: [64]posix.pollfd = undefined; + var poll_fds_count: usize = 0; + + // Read subscriptions and build poll structures + var i: u32 = 0; + while (i < nsubscriptions and poll_fds_count < poll_fds_buf.len) : (i += 1) { + const sub_offset = in_ptr + (i * 48); // Each subscription is 48 bytes + + // Read subscription type (u8 at offset 8) + const event_type = try memory.read(u8, sub_offset, 8); + + // Only handle FD_READ (2) and FD_WRITE (3) events + if (event_type == 0 or event_type == 1) { // CLOCK events + // Skip clock events for now + continue; + } else if (event_type == 2) { // FD_READ + const fd = try memory.read(i32, sub_offset, 16); + const host_fd = vm.getHostFd(fd); + + if (host_fd != @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + poll_fds_buf[poll_fds_count] = .{ + .fd = host_fd, + .events = posix.POLL.IN, + .revents = 0, + }; + poll_fds_count += 1; + } + } else if (event_type == 3) { // FD_WRITE + const fd = try memory.read(i32, sub_offset, 16); + const host_fd = vm.getHostFd(fd); + + if (host_fd != @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + poll_fds_buf[poll_fds_count] = .{ + .fd = host_fd, + .events = posix.POLL.OUT, + .revents = 0, + }; + poll_fds_count += 1; + } + } + } + + // If no valid fds, return immediately + if (poll_fds_count == 0) { + try memory.write(u32, nevents_ptr, 0, 0); + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); + return; + } + + // Poll with a timeout (100ms to prevent blocking forever) + const poll_fds = poll_fds_buf[0..poll_fds_count]; + _ = posix.poll(poll_fds, 100) catch |err| { + try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); + return; + }; + + // Write events back + var events_written: u32 = 0; + i = 0; + while (i < poll_fds_count) : (i += 1) { + const pfd = poll_fds[i]; + if (pfd.revents != 0) { + const event_offset = out_ptr + (events_written * 32); // Each event is 32 bytes + + // Write userdata (copy from subscription) + const sub_offset = in_ptr + (i * 48); + const userdata = try memory.read(u64, sub_offset, 0); + try memory.write(u64, event_offset, 0, userdata); + + // Write error (0 = success) + try memory.write(u16, event_offset, 8, 0); + + // Write event type + if ((pfd.revents & posix.POLL.IN) != 0) { + try memory.write(u8, event_offset, 10, 2); // FD_READ + } else if ((pfd.revents & posix.POLL.OUT) != 0) { + try memory.write(u8, event_offset, 10, 3); // FD_WRITE + } + + events_written += 1; + } + } + + try memory.write(u32, nevents_ptr, 0, events_written); + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); } pub fn proc_exit(vm: *VirtualMachine) WasmError!void { @@ -472,9 +677,11 @@ fn toWasiError(err: anyerror) wasi.errno_t { error.FileNotFound => .NOENT, error.PathAlreadyExists => .EXIST, error.IsDir => .ISDIR, + error.NotLink => .INVAL, // EINVAL: Not a symbolic link (per POSIX/WASI spec for readlink) error.Unseekable => .SPIPE, // ESPIPE: Illegal seek (e.g., on pipes/sockets) error.InvalidArgument => .INVAL, error.PermissionDenied => .PERM, + error.WouldBlock => .AGAIN, // EAGAIN: Resource temporarily unavailable (non-blocking I/O) else => std.debug.panic("WASI: Unhandled zig stdlib error: {s}", .{@errorName(err)}), }; } @@ -510,6 +717,10 @@ pub fn fd_tell(vm: *VirtualMachine) WasmError!void { const fd = vm.popOperand(i32); const host_fd = vm.getHostFd(fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } const current_offset = posix.lseek_CUR_get(host_fd) catch |err| { try vm.pushOperand(u64, @intFromEnum(toWasiError(err))); return; @@ -559,6 +770,10 @@ pub fn fd_readdir(vm: *VirtualMachine) WasmError!void { const memory = try vm.inst.getMemory(0); const mem_data = memory.memory(); const host_fd = vm.getHostFd(fd); + if (host_fd == @as(posix.fd_t, @bitCast(@as(i32, -1)))) { + try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.BADF)); + return; + } const dirent_size: u32 = @sizeOf(wasi.dirent_t); switch (native_os) { @@ -570,6 +785,7 @@ pub fn fd_readdir(vm: *VirtualMachine) WasmError!void { var entry_idx: u64 = 0; var bytes_used: u32 = 0; + var buffer_full = false; while (true) { var kernel_buf: [8192]u8 = undefined; @@ -590,18 +806,28 @@ pub fn fd_readdir(vm: *VirtualMachine) WasmError!void { const entry_size: u32 = dirent_size + @as(u32, @intCast(name.len)); if (bytes_used + entry_size > buf_len) { - try memory.write(u32, bufused_ptr, 0, bytes_used); - try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); - return; + // Buffer is full but there are more entries + buffer_full = true; + break; } try writeWasiDirent(mem_data, memory, buf_ptr + bytes_used, entry_idx + 1, entry.ino, name, toWasiFiletype(entry.type)); bytes_used += entry_size; entry_idx += 1; } + + if (buffer_full) break; } - try memory.write(u32, bufused_ptr, 0, bytes_used); + // WASI spec: bufused < buf_len signals EOF. + // If buffer is full but there are more entries, return buf_len to signal "continue reading". + // Zero-fill remaining buffer space to avoid garbage data. + if (buffer_full and bytes_used < buf_len) { + @memset(mem_data[buf_ptr + bytes_used .. buf_ptr + buf_len], 0); + try memory.write(u32, bufused_ptr, 0, buf_len); + } else { + try memory.write(u32, bufused_ptr, 0, bytes_used); + } try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); }, @@ -614,6 +840,7 @@ pub fn fd_readdir(vm: *VirtualMachine) WasmError!void { var entry_idx: u64 = 0; var bytes_used: u32 = 0; var seek: i64 = 0; + var buffer_full = false; while (true) { var kernel_buf: [8192]u8 align(@alignOf(std.c.dirent)) = undefined; @@ -633,18 +860,28 @@ pub fn fd_readdir(vm: *VirtualMachine) WasmError!void { const entry_size: u32 = dirent_size + @as(u32, @intCast(name.len)); if (bytes_used + entry_size > buf_len) { - try memory.write(u32, bufused_ptr, 0, bytes_used); - try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); - return; + // Buffer is full but there are more entries + buffer_full = true; + break; } try writeWasiDirent(mem_data, memory, buf_ptr + bytes_used, entry_idx + 1, entry.ino, name, toWasiFiletype(entry.type)); bytes_used += entry_size; entry_idx += 1; } + + if (buffer_full) break; } - try memory.write(u32, bufused_ptr, 0, bytes_used); + // WASI spec: bufused < buf_len signals EOF. + // If buffer is full but there are more entries, return buf_len to signal "continue reading". + // Zero-fill remaining buffer space to avoid garbage data. + if (buffer_full and bytes_used < buf_len) { + @memset(mem_data[buf_ptr + bytes_used .. buf_ptr + buf_len], 0); + try memory.write(u32, bufused_ptr, 0, buf_len); + } else { + try memory.write(u32, bufused_ptr, 0, bytes_used); + } try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); }, @@ -656,6 +893,7 @@ pub fn fd_readdir(vm: *VirtualMachine) WasmError!void { var entry_idx: u64 = 0; var bytes_used: u32 = 0; + var buffer_full = false; while (true) { var kernel_buf: [8192]u8 align(@alignOf(std.c.dirent)) = undefined; @@ -675,18 +913,28 @@ pub fn fd_readdir(vm: *VirtualMachine) WasmError!void { const entry_size: u32 = dirent_size + @as(u32, @intCast(name.len)); if (bytes_used + entry_size > buf_len) { - try memory.write(u32, bufused_ptr, 0, bytes_used); - try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); - return; + // Buffer is full but there are more entries + buffer_full = true; + break; } try writeWasiDirent(mem_data, memory, buf_ptr + bytes_used, entry_idx + 1, entry.fileno, name, toWasiFiletype(entry.type)); bytes_used += entry_size; entry_idx += 1; } + + if (buffer_full) break; } - try memory.write(u32, bufused_ptr, 0, bytes_used); + // WASI spec: bufused < buf_len signals EOF. + // If buffer is full but there are more entries, return buf_len to signal "continue reading". + // Zero-fill remaining buffer space to avoid garbage data. + if (buffer_full and bytes_used < buf_len) { + @memset(mem_data[buf_ptr + bytes_used .. buf_ptr + buf_len], 0); + try memory.write(u32, bufused_ptr, 0, buf_len); + } else { + try memory.write(u32, bufused_ptr, 0, bytes_used); + } try vm.pushOperand(u64, @intFromEnum(wasi.errno_t.SUCCESS)); }, diff --git a/test/wasi-testsuite/adapter/zware.py b/test/wasi-testsuite/adapter/zware.py new file mode 100644 index 00000000..42ef3985 --- /dev/null +++ b/test/wasi-testsuite/adapter/zware.py @@ -0,0 +1,89 @@ +""" +WASI Testsuite adapter for zware + +This adapter enables the WASI testsuite to test the zware WebAssembly runtime. +The adapter translates WASI test requirements into zware-run command-line arguments. +""" + +import os +import subprocess +import shlex +from pathlib import Path +from typing import Dict, List, Tuple + +# Path to the zware-run executable, can be overridden via environment variable +ZWARE_RUN = shlex.split(os.environ.get("ZWARE_RUN", "zig-out/bin/zware-run")) + + +def get_name() -> str: + """Return the name of this adapter""" + return "zware" + + +def get_version() -> str: + """Return the version of zware""" + try: + result = subprocess.run( + ZWARE_RUN[0:1] + ["--version"], + encoding="UTF-8", + capture_output=True, + check=True + ) + # Parse version from output (format: "zware-run X.Y.Z") + # Note: --version prints to stderr + output = result.stderr if result.stderr else result.stdout + if output: + parts = output.strip().split() + if len(parts) >= 2: + return parts[-1] # Return last word as version + return "unknown" + except (subprocess.SubprocessError, FileNotFoundError): + return "unknown" + + +def get_wasi_versions() -> List[str]: + """Return list of supported WASI versions""" + # zware currently supports WASI preview 1 (wasi_snapshot_preview1) + return ["wasm32-wasip1"] + + +def compute_argv(test_path: str, + args: List[str], + env: Dict[str, str], + dirs: List[Tuple[Path, str]], + wasi_version: str) -> List[str]: + """ + Compute the command-line arguments for running a WASI test + + Args: + test_path: Path to the .wasm file to execute + args: Command-line arguments to pass to the WASM module + env: Environment variables to set + dirs: Directory mappings for preopens (list of (host_path, guest_path) tuples) + wasi_version: WASI version to use (currently only "wasm32-wasip1" is supported) + + Returns: + List of command-line arguments for executing the test with zware-run + """ + argv = [] + ZWARE_RUN + + # Isolate stdio for tests to prevent blocking on stdin + # Tests should not interact with the host's terminal + argv.append("--no-inherit-stdio") + + # Add environment variables using --env KEY=VALUE + for key, value in env.items(): + argv.extend(["--env", f"{key}={value}"]) + + # Add directory mappings (preopens) using --dir GUEST::HOST + for host_path, guest_path in dirs: + argv.extend(["--dir", f"{guest_path}::{host_path}"]) + + # Add the WASM module path + # The testsuite expects to call _start (WASI entry point), which is zware-run's default + argv.append(test_path) + + # Add arguments to pass to the WASM module + argv.extend(args) + + return argv diff --git a/test/wasi-testsuite/discover-wasi-tests.py b/test/wasi-testsuite/discover-wasi-tests.py new file mode 100644 index 00000000..a246bb53 --- /dev/null +++ b/test/wasi-testsuite/discover-wasi-tests.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Discover all WASI tests from the testsuite and output as JSON. +This is used by both build.zig and GitHub Actions to programmatically +create individual test steps. +""" + +import json +import sys +from pathlib import Path +from typing import List, Dict + + +def discover_tests(testsuite_root: Path) -> Dict[str, List[str]]: + """ + Discover all WASI tests in the testsuite. + + Returns: + Dict mapping suite name to list of test names + """ + suites = { + "c": "tests/c/testsuite/wasm32-wasip1", + "rust": "tests/rust/testsuite/wasm32-wasip1", + "as": "tests/assemblyscript/testsuite/wasm32-wasip1", + } + + results = {} + + for suite_name, suite_path in suites.items(): + full_path = testsuite_root / suite_path + if not full_path.exists(): + continue + + test_names = [] + for wasm_file in sorted(full_path.glob("*.wasm")): + test_name = wasm_file.stem # filename without extension + test_names.append(test_name) + + results[suite_name] = test_names + + return results + + +def main(): + if len(sys.argv) < 2: + print("Usage: discover-wasi-tests.py TESTSUITE_ROOT [--format FORMAT]", file=sys.stderr) + print("Formats: json (default), zig-array, github-matrix", file=sys.stderr) + return 1 + + testsuite_root = Path(sys.argv[1]) + output_format = "json" + + if len(sys.argv) >= 4 and sys.argv[2] == "--format": + output_format = sys.argv[3] + + tests = discover_tests(testsuite_root) + + if output_format == "json": + print(json.dumps(tests, indent=2)) + + elif output_format == "zig-array": + # Output format suitable for embedding in Zig code + for suite_name, test_names in tests.items(): + print(f'// {suite_name} tests ({len(test_names)})') + print(f'const wasi_{suite_name}_tests = [_][]const u8{{') + for test_name in test_names: + print(f' "{test_name}",') + print('};') + print() + + elif output_format == "github-matrix": + # Output format for GitHub Actions matrix + include = [] + for suite_name, test_names in tests.items(): + for test_name in test_names: + include.append({ + "suite": suite_name, + "test": test_name + }) + print(json.dumps({"include": include})) + + else: + print(f"Unknown format: {output_format}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/wasi-testsuite/run-single-wasi-test.py b/test/wasi-testsuite/run-single-wasi-test.py new file mode 100644 index 00000000..ef968aed --- /dev/null +++ b/test/wasi-testsuite/run-single-wasi-test.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +Wrapper script to run a single WASI test from the testsuite. +The official test runner only accepts directories, so this script creates +a temporary directory with the single test and runs it. +""" + +import argparse +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser(description="Run a single WASI test") + parser.add_argument("test_suite_dir", help="Path to test suite directory") + parser.add_argument("test_name", help="Name of the test (without .wasm extension)") + parser.add_argument("--runtime-adapter", "-r", required=True, help="Path to runtime adapter") + parser.add_argument("--test-runner", required=True, help="Path to wasi_test_runner.py") + + args = parser.parse_args() + + test_suite_dir = Path(args.test_suite_dir) + test_name = args.test_name + + # Check if test files exist + wasm_file = test_suite_dir / f"{test_name}.wasm" + if not wasm_file.exists(): + print(f"Error: Test file {wasm_file} not found", file=sys.stderr) + return 1 + + # Create temporary directory with just this test + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Copy manifest.json if it exists + manifest_file = test_suite_dir / "manifest.json" + if manifest_file.exists(): + shutil.copy(manifest_file, temp_path / "manifest.json") + + # Copy test files + shutil.copy(wasm_file, temp_path / f"{test_name}.wasm") + + json_file = test_suite_dir / f"{test_name}.json" + if json_file.exists(): + shutil.copy(json_file, temp_path / f"{test_name}.json") + + # Copy any directories from the test suite (like fs-tests.dir) + # These are needed for tests that access the filesystem + for item in test_suite_dir.iterdir(): + if item.is_dir(): + shutil.copytree(item, temp_path / item.name) + + # Run the test runner + cmd = [ + "python3", + args.test_runner, + "-t", str(temp_path), + "-r", args.runtime_adapter, + ] + + result = subprocess.run(cmd) + return result.returncode + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/zware-run.zig b/tools/zware-run.zig index 4fae1afb..641aeb9c 100644 --- a/tools/zware-run.zig +++ b/tools/zware-run.zig @@ -1,5 +1,6 @@ const std = @import("std"); const zware = @import("zware"); +const wasi = @import("wasi"); fn oom(e: error{OutOfMemory}) noreturn { @panic(@errorName(e)); @@ -21,6 +22,20 @@ const global = struct { var import_stubs: std.ArrayListUnmanaged(ImportStub) = .{}; }; +const Config = struct { + wasm_path: []const u8, + function_name: ?[]const u8 = null, // null means auto-detect + wasm_args: []const []const u8, + env_vars: std.StringHashMap([]const u8), + dir_mappings: std.StringHashMap([]const u8), + inherit_stdio: bool = true, // default to true for CLI UX + + fn deinit(self: *Config) void { + self.env_vars.deinit(); + self.dir_mappings.deinit(); + } +}; + pub fn main() !void { try main2(); if (enable_leak_detection) { @@ -30,33 +45,283 @@ pub fn main() !void { } } } +fn parseArgs(args: []const []const u8) !Config { + var env_vars = std.StringHashMap([]const u8).init(global.alloc); + errdefer env_vars.deinit(); + + var dir_mappings = std.StringHashMap([]const u8).init(global.alloc); + errdefer dir_mappings.deinit(); + + var wasm_args: std.ArrayList([]const u8) = .empty; + defer wasm_args.deinit(global.alloc); + + var wasm_path: ?[]const u8 = null; + var function_name: ?[]const u8 = null; + var inherit_stdio: bool = true; // default to true + var i: usize = 0; + + while (i < args.len) : (i += 1) { + const arg = args[i]; + + if (std.mem.eql(u8, arg, "--env")) { + i += 1; + if (i >= args.len) { + std.log.err("--env requires KEY=VALUE argument", .{}); + std.process.exit(0xff); + } + const env_spec = args[i]; + const eq_pos = std.mem.indexOf(u8, env_spec, "=") orelse { + std.log.err("invalid --env format, expected KEY=VALUE", .{}); + std.process.exit(0xff); + }; + try env_vars.put(env_spec[0..eq_pos], env_spec[eq_pos + 1 ..]); + } else if (std.mem.eql(u8, arg, "--dir")) { + i += 1; + if (i >= args.len) { + std.log.err("--dir requires GUEST::HOST argument", .{}); + std.process.exit(0xff); + } + const dir_spec = args[i]; + const sep_pos = std.mem.indexOf(u8, dir_spec, "::") orelse { + std.log.err("invalid --dir format, expected GUEST::HOST", .{}); + std.process.exit(0xff); + }; + try dir_mappings.put(dir_spec[0..sep_pos], dir_spec[sep_pos + 2 ..]); + } else if (std.mem.eql(u8, arg, "-f") or std.mem.eql(u8, arg, "--function")) { + i += 1; + if (i >= args.len) { + std.log.err("-f/--function requires function name argument", .{}); + std.process.exit(0xff); + } + function_name = args[i]; + } else if (std.mem.eql(u8, arg, "--inherit-stdio")) { + inherit_stdio = true; + } else if (std.mem.eql(u8, arg, "--no-inherit-stdio")) { + inherit_stdio = false; + } else if (std.mem.eql(u8, arg, "--version")) { + std.debug.print("zware-run 0.0.1\n", .{}); + std.process.exit(0); + } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { + printUsage(); + std.process.exit(0); + } else if (wasm_path == null) { + wasm_path = arg; + } else { + // All arguments after wasm_path are program arguments + try wasm_args.append(global.alloc, arg); + } + } + + if (wasm_path == null) { + printUsage(); + std.process.exit(0xff); + } + + return Config{ + .wasm_path = wasm_path.?, + .function_name = function_name, + .wasm_args = try wasm_args.toOwnedSlice(global.alloc), + .env_vars = env_vars, + .dir_mappings = dir_mappings, + .inherit_stdio = inherit_stdio, + }; +} + +fn printUsage() void { + const stderr_fd = std.fs.File.stderr(); + var stderr_buf: [4096]u8 = undefined; + var stderr_writer = stderr_fd.writer(&stderr_buf); + const stderr = &stderr_writer.interface; + stderr.writeAll( + \\Usage: zware-run [OPTIONS] FILE.wasm [ARGS...] + \\ + \\Options: + \\ -f, --function NAME Specify function to call (default: _start) + \\ --env KEY=VALUE Set environment variable for WASI + \\ --dir GUEST::HOST Map directory for WASI preopens + \\ --inherit-stdio Inherit stdin/stdout/stderr from host (default) + \\ --no-inherit-stdio Isolate stdio (useful for testing) + \\ --version Show version + \\ --help, -h Show this help + \\ + \\If no function is specified with -f, attempts to call _start (WASI entry point). + \\All arguments after FILE.wasm are passed to the WASM program. + \\WASI imports are always available regardless of which function is called. + \\By default, stdio is inherited from the host for normal CLI usage. + \\ + \\Examples: + \\ zware-run -f fib fib.wasm # Call fib() function + \\ zware-run program.wasm arg1 arg2 # Call _start with arguments + \\ zware-run --env FOO=bar program.wasm # Call _start with env var + \\ zware-run --dir /::. program.wasm arg1 arg2 # Call _start with dir mapping and args + \\ zware-run --no-inherit-stdio test.wasm # Run with isolated stdio + \\ + ) catch {}; + stderr.flush() catch {}; +} + +// WASI wrapper functions - these adapt WASI functions to the host function signature +fn wasi_args_get(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.args_get(vm); +} +fn wasi_args_sizes_get(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.args_sizes_get(vm); +} +fn wasi_environ_get(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.environ_get(vm); +} +fn wasi_environ_sizes_get(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.environ_sizes_get(vm); +} +fn wasi_clock_time_get(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.clock_time_get(vm); +} +fn wasi_fd_close(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_close(vm); +} +fn wasi_fd_fdstat_get(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_fdstat_get(vm); +} +fn wasi_fd_fdstat_set_flags(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_fdstat_set_flags(vm); +} +fn wasi_fd_filestat_get(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_filestat_get(vm); +} +fn wasi_fd_prestat_get(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_prestat_get(vm); +} +fn wasi_fd_prestat_dir_name(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_prestat_dir_name(vm); +} +fn wasi_fd_read(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_read(vm); +} +fn wasi_fd_readdir(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_readdir(vm); +} +fn wasi_fd_seek(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_seek(vm); +} +fn wasi_fd_tell(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_tell(vm); +} +fn wasi_fd_write(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.fd_write(vm); +} +fn wasi_path_create_directory(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.path_create_directory(vm); +} +fn wasi_path_filestat_get(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.path_filestat_get(vm); +} +fn wasi_path_open(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.path_open(vm); +} +fn wasi_path_readlink(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.path_readlink(vm); +} +fn wasi_poll_oneoff(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.poll_oneoff(vm); +} +fn wasi_proc_exit(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.proc_exit(vm); +} +fn wasi_random_get(vm: *zware.VirtualMachine, _: usize) zware.WasmError!void { + return zware.wasi.random_get(vm); +} + +// Expose all WASI imports to the store +fn setupWasiImports(store: *zware.Store) !void { + const wasi_module = "wasi_snapshot_preview1"; + + // args_get(argv: **u8, argv_buf: *u8) -> errno + try store.exposeHostFunction(wasi_module, "args_get", wasi_args_get, 0, &[_]zware.ValType{ .I32, .I32 }, &[_]zware.ValType{.I32}); + + // args_sizes_get(argc: *u32, argv_buf_size: *u32) -> errno + try store.exposeHostFunction(wasi_module, "args_sizes_get", wasi_args_sizes_get, 0, &[_]zware.ValType{ .I32, .I32 }, &[_]zware.ValType{.I32}); + + // environ_get(environ: **u8, environ_buf: *u8) -> errno + try store.exposeHostFunction(wasi_module, "environ_get", wasi_environ_get, 0, &[_]zware.ValType{ .I32, .I32 }, &[_]zware.ValType{.I32}); + + // environ_sizes_get(environc: *u32, environ_buf_size: *u32) -> errno + try store.exposeHostFunction(wasi_module, "environ_sizes_get", wasi_environ_sizes_get, 0, &[_]zware.ValType{ .I32, .I32 }, &[_]zware.ValType{.I32}); + + // clock_time_get(id: u32, precision: u64, timestamp: *u64) -> errno + try store.exposeHostFunction(wasi_module, "clock_time_get", wasi_clock_time_get, 0, &[_]zware.ValType{ .I32, .I64, .I32 }, &[_]zware.ValType{.I32}); + + // fd_close(fd: i32) -> errno + try store.exposeHostFunction(wasi_module, "fd_close", wasi_fd_close, 0, &[_]zware.ValType{.I32}, &[_]zware.ValType{.I32}); + + // fd_fdstat_get(fd: i32, stat: *fdstat) -> errno + try store.exposeHostFunction(wasi_module, "fd_fdstat_get", wasi_fd_fdstat_get, 0, &[_]zware.ValType{ .I32, .I32 }, &[_]zware.ValType{.I32}); + + // fd_fdstat_set_flags(fd: i32, flags: u16) -> errno + try store.exposeHostFunction(wasi_module, "fd_fdstat_set_flags", wasi_fd_fdstat_set_flags, 0, &[_]zware.ValType{ .I32, .I32 }, &[_]zware.ValType{.I32}); + + // fd_filestat_get(fd: i32, stat: *filestat) -> errno + try store.exposeHostFunction(wasi_module, "fd_filestat_get", wasi_fd_filestat_get, 0, &[_]zware.ValType{ .I32, .I32 }, &[_]zware.ValType{.I32}); + + // fd_prestat_get(fd: i32, prestat: *prestat) -> errno + try store.exposeHostFunction(wasi_module, "fd_prestat_get", wasi_fd_prestat_get, 0, &[_]zware.ValType{ .I32, .I32 }, &[_]zware.ValType{.I32}); + + // fd_prestat_dir_name(fd: i32, path: *u8, path_len: u32) -> errno + try store.exposeHostFunction(wasi_module, "fd_prestat_dir_name", wasi_fd_prestat_dir_name, 0, &[_]zware.ValType{ .I32, .I32, .I32 }, &[_]zware.ValType{.I32}); + + // fd_read(fd: i32, iovs: *const iovec, iovs_len: u32, nread: *u32) -> errno + try store.exposeHostFunction(wasi_module, "fd_read", wasi_fd_read, 0, &[_]zware.ValType{ .I32, .I32, .I32, .I32 }, &[_]zware.ValType{.I32}); + + // fd_readdir(fd: i32, buf: *u8, buf_len: u32, cookie: u64, bufused: *u32) -> errno + try store.exposeHostFunction(wasi_module, "fd_readdir", wasi_fd_readdir, 0, &[_]zware.ValType{ .I32, .I32, .I32, .I64, .I32 }, &[_]zware.ValType{.I32}); + + // fd_seek(fd: i32, offset: i64, whence: u8, newoffset: *u64) -> errno + try store.exposeHostFunction(wasi_module, "fd_seek", wasi_fd_seek, 0, &[_]zware.ValType{ .I32, .I64, .I32, .I32 }, &[_]zware.ValType{.I32}); + + // fd_tell(fd: i32, offset: *u64) -> errno + try store.exposeHostFunction(wasi_module, "fd_tell", wasi_fd_tell, 0, &[_]zware.ValType{ .I32, .I32 }, &[_]zware.ValType{.I32}); + + // fd_write(fd: i32, iovs: *const ciovec, iovs_len: u32, nwritten: *u32) -> errno + try store.exposeHostFunction(wasi_module, "fd_write", wasi_fd_write, 0, &[_]zware.ValType{ .I32, .I32, .I32, .I32 }, &[_]zware.ValType{.I32}); + + // path_create_directory(fd: i32, path: *const u8, path_len: u32) -> errno + try store.exposeHostFunction(wasi_module, "path_create_directory", wasi_path_create_directory, 0, &[_]zware.ValType{ .I32, .I32, .I32 }, &[_]zware.ValType{.I32}); + + // path_filestat_get(fd: i32, flags: u32, path: *const u8, path_len: u32, buf: *filestat) -> errno + try store.exposeHostFunction(wasi_module, "path_filestat_get", wasi_path_filestat_get, 0, &[_]zware.ValType{ .I32, .I32, .I32, .I32, .I32 }, &[_]zware.ValType{.I32}); + + // path_open(fd: i32, dirflags: u32, path: *const u8, path_len: u32, oflags: u32, fs_rights_base: u64, fs_rights_inheriting: u64, fdflags: u32, opened_fd: *i32) -> errno + try store.exposeHostFunction(wasi_module, "path_open", wasi_path_open, 0, &[_]zware.ValType{ .I32, .I32, .I32, .I32, .I32, .I64, .I64, .I32, .I32 }, &[_]zware.ValType{.I32}); + + // path_readlink(fd: i32, path: *const u8, path_len: u32, buf: *u8, buf_len: u32, bufused: *u32) -> errno + try store.exposeHostFunction(wasi_module, "path_readlink", wasi_path_readlink, 0, &[_]zware.ValType{ .I32, .I32, .I32, .I32, .I32, .I32 }, &[_]zware.ValType{.I32}); + + // poll_oneoff(in: *const subscription, out: *event, nsubscriptions: u32, nevents: *u32) -> errno + try store.exposeHostFunction(wasi_module, "poll_oneoff", wasi_poll_oneoff, 0, &[_]zware.ValType{ .I32, .I32, .I32, .I32 }, &[_]zware.ValType{.I32}); + + // proc_exit(rval: i32) -> ! + try store.exposeHostFunction(wasi_module, "proc_exit", wasi_proc_exit, 0, &[_]zware.ValType{.I32}, &[_]zware.ValType{}); + + // random_get(buf: *u8, buf_len: u32) -> errno + try store.exposeHostFunction(wasi_module, "random_get", wasi_random_get, 0, &[_]zware.ValType{ .I32, .I32 }, &[_]zware.ValType{.I32}); +} + fn main2() !void { defer global.import_stubs.deinit(global.alloc); const full_cmdline = try std.process.argsAlloc(global.alloc); defer std.process.argsFree(global.alloc, full_cmdline); - if (full_cmdline.len <= 1) { - const stderr_fd = std.fs.File.stderr(); - var stderr_buf: [4096]u8 = undefined; - var stderr_writer = stderr_fd.writer(&stderr_buf); - const stderr = &stderr_writer.interface; - try stderr.writeAll("Usage: zware-run FILE.wasm FUNCTION\n"); - try stderr.flush(); - std.process.exit(0xff); - } + var config = try parseArgs(full_cmdline[1..]); + defer config.deinit(); - const pos_args = full_cmdline[1..]; - if (pos_args.len != 2) { - std.log.err("expected {} positional cmdline arguments but got {}", .{ 2, pos_args.len }); - std.process.exit(0xff); - } - const wasm_path = pos_args[0]; - const wasm_func_name = pos_args[1]; + const wasm_path = config.wasm_path; var store = zware.Store.init(global.alloc); defer store.deinit(); + // Setup WASI imports (always available) + try setupWasiImports(&store); + const wasm_content = content_blk: { var file = std.fs.cwd().openFile(wasm_path, .{}) catch |e| { std.log.err("failed to open '{s}': {s}", .{ wasm_path, @errorName(e) }); @@ -71,7 +336,9 @@ fn main2() !void { defer module.deinit(); try module.decode(); - const export_funcidx = try getExportFunction(&module, wasm_func_name); + // Determine which function to call + const func_name = config.function_name orelse "_start"; + const export_funcidx = try getExportFunction(&module, func_name); const export_funcdef = module.functions.list.items[export_funcidx]; const export_functype = try module.types.lookup(export_funcdef.typeidx); if (export_functype.params.len != 0) { @@ -82,6 +349,48 @@ fn main2() !void { var instance = zware.Instance.init(global.alloc, &store, module); defer if (enable_leak_detection) instance.deinit(); + // Setup WASI arguments + const wasi_args = try global.alloc.alloc([:0]u8, config.wasm_args.len + 1); + defer global.alloc.free(wasi_args); + + // First arg is the program name (wasm path) + wasi_args[0] = try global.alloc.dupeZ(u8, wasm_path); + for (config.wasm_args, 0..) |arg, i| { + wasi_args[i + 1] = try global.alloc.dupeZ(u8, arg); + } + + for (wasi_args) |arg| { + try instance.wasi_args.append(global.alloc, arg); + } + + // Setup WASI environment variables + var env_iter = config.env_vars.iterator(); + while (env_iter.next()) |entry| { + try instance.wasi_env.put(global.alloc, entry.key_ptr.*, entry.value_ptr.*); + } + + // Setup WASI stdio (fds 0, 1, 2) + if (config.inherit_stdio) { + try instance.inheritStdio(); + } + + // Setup WASI preopens (directory mappings) + // Preopens always start at fd 3, per WASI convention (fds 0-2 reserved for stdio) + var preopen_fd: i32 = 3; + var dir_iter = config.dir_mappings.iterator(); + while (dir_iter.next()) |entry| { + const guest_path = entry.key_ptr.*; + const host_path = entry.value_ptr.*; + + const dir = std.fs.cwd().openDir(host_path, .{}) catch |e| { + std.log.err("failed to open directory '{s}': {s}", .{ host_path, @errorName(e) }); + std.process.exit(0xff); + }; + + try instance.addWasiPreopen(preopen_fd, guest_path, dir.fd); + preopen_fd += 1; + } + try populateMissingImports(&store, &module); var zware_error: zware.Error = undefined; @@ -97,7 +406,7 @@ fn main2() !void { var in = [_]u64{}; const out_args = try global.alloc.alloc(u64, export_functype.results.len); defer global.alloc.free(out_args); - try instance.invoke(wasm_func_name, &in, out_args, .{}); + try instance.invoke(func_name, &in, out_args, .{}); std.log.info("{} output(s)", .{out_args.len}); for (out_args, 0..) |out_arg, out_index| { std.log.info("output {} {f}", .{ out_index, fmtValue(export_functype.results[out_index], out_arg) });