diff --git a/build.zig b/build.zig index bc44f24a..643cc99f 100644 --- a/build.zig +++ b/build.zig @@ -28,16 +28,18 @@ pub fn build(b: *std.Build) void { options.addOption([]const u8, "version", build_zon.version); // Library module (for use as dependency and test root). - // link_libc is required because wasi.zig / cache.zig / platform.zig use - // `std.c.*` for POSIX operations that std.posix lost in Zig 0.16 - // (fsync, mkdirat, unlinkat, renameat, dup, pread/pwrite, futimens, …). - // On Linux the build is strict about this; macOS happens to auto-link - // libc for `extern "c"` decls but both platforms need it. + // link_libc = false post-W46 migration. WASI fd I/O and path-based ops + // now go through `platform.pfd*` helpers (Linux syscalls / Mac libSystem + // auto-link / Win32 kernel32). Env vars come from `std.process.Environ.Map` + // captured in `cli.main` (W46 Phase 1e). The `std.c.*` references that + // survive are all inside `else` branches of `switch (comptime builtin.os.tag)` + // blocks, so they are comptime-pruned on Linux/Windows and still resolve + // to libSystem on Mac. const mod = b.addModule("zwasm", .{ .root_source_file = b.path("src/types.zig"), .target = target, .optimize = optimize, - .link_libc = true, + .link_libc = false, }); mod.addOptions("build_options", options); @@ -55,7 +57,7 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/cli.zig"), .target = target, .optimize = optimize, - .link_libc = true, + .link_libc = false, }); cli_mod.addOptions("build_options", options); const cli = b.addExecutable(.{ @@ -82,7 +84,7 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path(ex.src), .target = target, .optimize = optimize, - .link_libc = true, + .link_libc = false, }); ex_mod.addImport("zwasm", mod); const ex_exe = b.addExecutable(.{ @@ -99,7 +101,7 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("test/e2e/e2e_runner.zig"), .target = target, .optimize = optimize, - .link_libc = true, + .link_libc = false, }); e2e_mod.addImport("zwasm", mod); const e2e = b.addExecutable(.{ @@ -116,7 +118,7 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("bench/fib_bench.zig"), .target = target, .optimize = optimize, - .link_libc = true, + .link_libc = false, }); bench_mod.addImport("zwasm", mod); const bench = b.addExecutable(.{ @@ -135,7 +137,7 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/fuzz_loader.zig"), .target = target, .optimize = optimize, - .link_libc = true, + .link_libc = false, }); fuzz_mod.addImport("zwasm", mod); const fuzz = b.addExecutable(.{ @@ -148,7 +150,7 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/fuzz_wat_loader.zig"), .target = target, .optimize = optimize, - .link_libc = true, + .link_libc = false, }); fuzz_wat_mod.addImport("zwasm", mod); const fuzz_wat = b.addExecutable(.{ @@ -162,6 +164,12 @@ pub fn build(b: *std.Build) void { // Default to ReleaseSafe: Zig 0.15's Debug-mode shared libraries // crash on Linux x86_64 due to GPA/PIC codegen issues (see #11). // Users embedding zwasm want optimized code anyway. + // + // C API targets keep link_libc = true: `src/c_api.zig` uses + // `std.heap.c_allocator` as the default backing allocator, which + // requires libc on every platform (Mac libSystem auto-linked, Linux + // glibc/musl, Windows msvcrt). Consumers of libzwasm are C programs + // that always link libc anyway, so this costs them nothing. const lib_optimize = b.option(bool, "lib-debug", "Build libraries in Debug mode (default: false)") orelse false; const lib_shared_mod = b.createModule(.{ .root_source_file = b.path("src/c_api.zig"), diff --git a/src/cli.zig b/src/cli.zig index ae8e55df..2a690114 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -23,6 +23,7 @@ const component_mod = @import("component.zig"); const guard_mod = @import("guard.zig"); const jit_mod = vm_mod.jit_mod; const cache_mod = @import("cache.zig"); +const platform = @import("platform.zig"); /// Process-wide Io handle. Populated once at the top of `main` from the /// `std.process.Init` the compiler-generated entrypoint hands us, and read @@ -40,6 +41,12 @@ pub fn main(init: std.process.Init) !void { cli_io = init.io; + // Expose the process environment to `platform` so subsequent callers + // (`appCacheDir`, `tempDirPath`) can look up HOME / TMPDIR / … without + // going through `std.c.getenv` — a prerequisite for dropping + // `link_libc = true` on Linux (W46 Phase 1e). + platform.setEnvironMap(init.environ_map); + // `init.gpa` is a DebugAllocator-backed allocator in Debug builds // (leak-checked by start.zig) and the appropriate production allocator // otherwise. `init.arena` is the process arena — args/environ live there. diff --git a/src/platform.zig b/src/platform.zig index ea7e18dc..24475662 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -115,17 +115,33 @@ const FILE_END: windows.DWORD = 2; const ERROR_HANDLE_EOF: windows.DWORD = 38; const ERROR_BROKEN_PIPE: windows.DWORD = 109; -// Linux direct-syscall helpers. Handles the `usize` return convention -// (errno encoded as negative values cast to usize) and keeps `std.c._errno` -// in sync so existing `cErrnoToWasi`-style callers keep working during the -// un-link-libc migration (W46). +// Thread-local errno set by pfd helpers. Read via `pfdErrno()`. Callers that +// previously consulted `std.c._errno().*` should use `pfdErrno()` instead so +// the code path does not depend on libc being linked. +pub threadlocal var pfd_last_errno: std.posix.E = .SUCCESS; + +pub fn pfdErrno() std.posix.E { + return pfd_last_errno; +} + +/// Copy libc's thread-local errno slot into `pfd_last_errno`. Call this after +/// a `std.c.*` call has returned a failure so that `pfdErrno()` reflects the +/// actual failure. Mac/BSD code paths use this; Linux pfd helpers set +/// `pfd_last_errno` directly from the syscall return value. +pub fn syncErrnoFromLibC() void { + switch (comptime builtin.os.tag) { + .linux, .windows => {}, + else => pfd_last_errno = @enumFromInt(std.c._errno().*), + } +} + +// Linux direct-syscall helpers. Syscalls return errno as negative values +// cast to `usize`; convert that into the POSIX-style (-1, errno-in-slot) +// convention that upstream callers already expect. fn linuxResultAsIsize(rc: usize) isize { const e = std.os.linux.errno(rc); if (e != .SUCCESS) { - // Mirror errno into libc's thread-local slot so that callers that - // read `std.c._errno()` see the failure. Direct Linux syscalls - // don't touch this slot on their own. - std.c._errno().* = @intFromEnum(e); + pfd_last_errno = e; return -1; } return @bitCast(rc); @@ -134,7 +150,7 @@ fn linuxResultAsIsize(rc: usize) isize { fn linuxResultAsI32(rc: usize) i32 { const e = std.os.linux.errno(rc); if (e != .SUCCESS) { - std.c._errno().* = @intFromEnum(e); + pfd_last_errno = e; return -1; } return 0; @@ -143,23 +159,45 @@ fn linuxResultAsI32(rc: usize) i32 { fn linuxResultAsI64(rc: usize) i64 { const e = std.os.linux.errno(rc); if (e != .SUCCESS) { - std.c._errno().* = @intFromEnum(e); + pfd_last_errno = e; return -1; } return @as(i64, @bitCast(@as(u64, rc))); } +// Mac/BSD helpers — call after a `std.c.*` invocation so `pfd_last_errno` +// reflects the failure. The `if` is comptime-inert on Linux/Windows because +// the whole `else =>` arm is comptime-pruned. +fn cResultAsIsize(rc: isize) isize { + if (rc < 0) pfd_last_errno = @enumFromInt(std.c._errno().*); + return rc; +} + +fn cResultAsI32(rc: c_int) i32 { + const r: i32 = @intCast(rc); + if (r != 0) pfd_last_errno = @enumFromInt(std.c._errno().*); + return r; +} + +fn cResultAsI64(rc: std.c.off_t) i64 { + if (rc < 0) pfd_last_errno = @enumFromInt(std.c._errno().*); + return @intCast(rc); +} + /// POSIX-style write. Returns bytes written (>= 0) or -1 on error. pub fn pfdWrite(handle: std.posix.fd_t, buf: []const u8) isize { switch (comptime builtin.os.tag) { .windows => { var written: windows.DWORD = 0; const ok = WriteFile(handle, buf.ptr, @intCast(buf.len), &written, null); - if (ok == windows.BOOL.FALSE) return -1; + if (ok == windows.BOOL.FALSE) { + pfd_last_errno = .IO; + return -1; + } return @intCast(written); }, .linux => return linuxResultAsIsize(std.os.linux.write(handle, buf.ptr, buf.len)), - else => return std.c.write(handle, buf.ptr, buf.len), + else => return cResultAsIsize(std.c.write(handle, buf.ptr, buf.len)), } } @@ -172,12 +210,13 @@ pub fn pfdRead(handle: std.posix.fd_t, buf: []u8) isize { if (ok == windows.BOOL.FALSE) { const err = GetLastError(); if (err == ERROR_BROKEN_PIPE or err == ERROR_HANDLE_EOF) return 0; + pfd_last_errno = .IO; return -1; } return @intCast(got); }, .linux => return linuxResultAsIsize(std.os.linux.read(handle, buf.ptr, buf.len)), - else => return std.c.read(handle, buf.ptr, buf.len), + else => return cResultAsIsize(std.c.read(handle, buf.ptr, buf.len)), } } @@ -194,12 +233,13 @@ pub fn pfdPread(handle: std.posix.fd_t, buf: []u8, offset: u64) isize { if (ok == windows.BOOL.FALSE) { const err = GetLastError(); if (err == ERROR_BROKEN_PIPE or err == ERROR_HANDLE_EOF) return 0; + pfd_last_errno = .IO; return -1; } return @intCast(got); }, .linux => return linuxResultAsIsize(std.os.linux.pread(handle, buf.ptr, buf.len, @intCast(offset))), - else => return std.c.pread(handle, buf.ptr, buf.len, @intCast(offset)), + else => return cResultAsIsize(std.c.pread(handle, buf.ptr, buf.len, @intCast(offset))), } } @@ -213,11 +253,14 @@ pub fn pfdPwrite(handle: std.posix.fd_t, buf: []const u8, offset: u64) isize { }; var written: windows.DWORD = 0; const ok = WriteFile(handle, buf.ptr, @intCast(buf.len), &written, &ov); - if (ok == windows.BOOL.FALSE) return -1; + if (ok == windows.BOOL.FALSE) { + pfd_last_errno = .IO; + return -1; + } return @intCast(written); }, .linux => return linuxResultAsIsize(std.os.linux.pwrite(handle, buf.ptr, buf.len, @intCast(offset))), - else => return std.c.pwrite(handle, buf.ptr, buf.len, @intCast(offset)), + else => return cResultAsIsize(std.c.pwrite(handle, buf.ptr, buf.len, @intCast(offset))), } } @@ -230,15 +273,21 @@ pub fn pfdSeek(handle: std.posix.fd_t, offset: i64, whence: c_int) i64 { std.posix.SEEK.SET => FILE_BEGIN, std.posix.SEEK.CUR => FILE_CURRENT, std.posix.SEEK.END => FILE_END, - else => return -1, + else => { + pfd_last_errno = .INVAL; + return -1; + }, }; var new_pos: windows.LARGE_INTEGER = 0; const ok = SetFilePointerEx(handle, offset, &new_pos, method); - if (ok == windows.BOOL.FALSE) return -1; + if (ok == windows.BOOL.FALSE) { + pfd_last_errno = .IO; + return -1; + } return new_pos; }, .linux => return linuxResultAsI64(std.os.linux.lseek(handle, offset, @intCast(whence))), - else => return std.c.lseek(handle, offset, whence), + else => return cResultAsI64(std.c.lseek(handle, offset, whence)), } } @@ -258,7 +307,7 @@ pub fn pfdMkdirAt(dirfd: std.posix.fd_t, path: [*:0]const u8, mode: u32) i32 { switch (comptime builtin.os.tag) { .windows => return -1, .linux => return linuxResultAsI32(std.os.linux.mkdirat(dirfd, path, mode)), - else => return @intCast(std.c.mkdirat(dirfd, path, @intCast(mode))), + else => return cResultAsI32(std.c.mkdirat(dirfd, path, @intCast(mode))), } } @@ -266,7 +315,7 @@ pub fn pfdUnlinkAt(dirfd: std.posix.fd_t, path: [*:0]const u8, flags: u32) i32 { switch (comptime builtin.os.tag) { .windows => return -1, .linux => return linuxResultAsI32(std.os.linux.unlinkat(dirfd, path, flags)), - else => return @intCast(std.c.unlinkat(dirfd, path, @intCast(flags))), + else => return cResultAsI32(std.c.unlinkat(dirfd, path, @intCast(flags))), } } @@ -279,7 +328,7 @@ pub fn pfdRenameAt( switch (comptime builtin.os.tag) { .windows => return -1, .linux => return linuxResultAsI32(std.os.linux.renameat(old_dirfd, old_path, new_dirfd, new_path)), - else => return @intCast(std.c.renameat(old_dirfd, old_path, new_dirfd, new_path)), + else => return cResultAsI32(std.c.renameat(old_dirfd, old_path, new_dirfd, new_path)), } } @@ -287,7 +336,7 @@ pub fn pfdReadlinkAt(dirfd: std.posix.fd_t, path: [*:0]const u8, buf: []u8) isiz switch (comptime builtin.os.tag) { .windows => return -1, .linux => return linuxResultAsIsize(std.os.linux.readlinkat(dirfd, path, buf.ptr, buf.len)), - else => return std.c.readlinkat(dirfd, path, buf.ptr, buf.len), + else => return cResultAsIsize(std.c.readlinkat(dirfd, path, buf.ptr, buf.len)), } } @@ -298,28 +347,70 @@ pub fn pfdDup(fd: std.posix.fd_t) i32 { const rc = std.os.linux.dup(fd); const e = std.os.linux.errno(rc); if (e != .SUCCESS) { - std.c._errno().* = @intFromEnum(e); + pfd_last_errno = e; return -1; } return @intCast(rc); }, - else => return @intCast(std.c.dup(fd)), + else => return cResultAsI32(std.c.dup(fd)), } } pub fn pfdFsync(handle: std.posix.fd_t) i32 { switch (comptime builtin.os.tag) { - .windows => return if (FlushFileBuffers(handle) == windows.BOOL.FALSE) -1 else 0, - .linux => { - const rc = std.os.linux.fsync(handle); - const e = std.os.linux.errno(rc); - if (e != .SUCCESS) { - std.c._errno().* = @intFromEnum(e); + .windows => { + if (FlushFileBuffers(handle) == windows.BOOL.FALSE) { + pfd_last_errno = .IO; return -1; } return 0; }, - else => return std.c.fsync(handle), + .linux => return linuxResultAsI32(std.os.linux.fsync(handle)), + else => return cResultAsI32(std.c.fsync(handle)), + } +} + +pub fn pfdDup2(oldfd: std.posix.fd_t, newfd: std.posix.fd_t) i32 { + switch (comptime builtin.os.tag) { + .windows => return -1, + .linux => return linuxResultAsI32(std.os.linux.dup2(oldfd, newfd)), + else => return cResultAsI32(std.c.dup2(oldfd, newfd)), + } +} + +pub fn pfdPipe(fds: *[2]std.posix.fd_t) i32 { + switch (comptime builtin.os.tag) { + .windows => return -1, + .linux => return linuxResultAsI32(std.os.linux.pipe(fds)), + else => return cResultAsI32(std.c.pipe(fds)), + } +} + +/// Sleep for the given number of nanoseconds. Best-effort — short-sleep +/// tests use this to give other threads time to start. +pub fn pfdSleepNs(ns: u64) void { + switch (comptime builtin.os.tag) { + .windows => { + const K32 = struct { + extern "kernel32" fn Sleep(dwMilliseconds: windows.DWORD) callconv(.winapi) void; + }; + const ms: windows.DWORD = @intCast(@max(ns / 1_000_000, 1)); + K32.Sleep(ms); + }, + .linux => { + const req: std.os.linux.timespec = .{ + .sec = @intCast(ns / 1_000_000_000), + .nsec = @intCast(ns % 1_000_000_000), + }; + _ = std.os.linux.nanosleep(&req, null); + }, + else => { + const req: std.posix.timespec = .{ + .sec = @intCast(ns / 1_000_000_000), + .nsec = @intCast(ns % 1_000_000_000), + }; + _ = std.c.nanosleep(&req, null); + }, } } @@ -433,6 +524,19 @@ pub fn flushInstructionCache(ptr: [*]const u8, len: usize) void { } } +// Process-wide environment table captured at program start via `setEnvironMap`. +// This lets `envPath` / `appCacheDir` / `tempDirPath` look up variables without +// calling libc's `getenv` — the last remaining blocker for dropping +// `link_libc = true` on Linux (W46 Phase 1e). +var env_map_ref: ?*const std.process.Environ.Map = null; + +/// Capture the process's environment block. Call once at program start +/// (from `main(init: std.process.Init)`). Tests that never exercise +/// `envPath` may skip calling this; `envPath` returns null when unset. +pub fn setEnvironMap(m: *const std.process.Environ.Map) void { + env_map_ref = m; +} + pub fn appCacheDir(alloc: std.mem.Allocator, app_name: []const u8) ![]u8 { if (builtin.os.tag == .windows) { // Zig 0.16 removed `std.fs.getAppDataDir`. Build the path ourselves @@ -443,8 +547,8 @@ pub fn appCacheDir(alloc: std.mem.Allocator, app_name: []const u8) ![]u8 { return std.fmt.allocPrint(alloc, "{s}\\{s}", .{ base, app_name }); } - const home_ptr = std.c.getenv("HOME") orelse return error.NoCacheDir; - const home = std.mem.span(home_ptr); + const home = (try envPath(alloc, "HOME")) orelse return error.NoCacheDir; + defer alloc.free(home); return std.fmt.allocPrint(alloc, "{s}/.cache/{s}", .{ home, app_name }); } @@ -460,12 +564,8 @@ pub fn tempDirPath(alloc: std.mem.Allocator) ![]u8 { } fn envPath(alloc: std.mem.Allocator, name: []const u8) !?[]u8 { - var name_buf: [256]u8 = undefined; - if (name.len >= name_buf.len) return error.OutOfMemory; - @memcpy(name_buf[0..name.len], name); - name_buf[name.len] = 0; - const val_ptr = std.c.getenv(@ptrCast(&name_buf)) orelse return null; - const val = std.mem.span(val_ptr); + const m = env_map_ref orelse return null; + const val = m.get(name) orelse return null; if (val.len == 0) return null; return try alloc.dupe(u8, val); } diff --git a/src/trace.zig b/src/trace.zig index 7879a5ac..cc9d209a 100644 --- a/src/trace.zig +++ b/src/trace.zig @@ -68,7 +68,7 @@ fn stderrPrint(comptime fmt: []const u8, args: anytype) void { std.os.windows.peb().ProcessParameters.hStdError else std.posix.STDERR_FILENO; - _ = std.c.write(stderr_fd, msg.ptr, msg.len); + _ = platform.pfdWrite(stderr_fd, msg); } pub fn traceJitCompile(tc: *const TraceConfig, func_idx: u32, ir_count: u32, code_size: u32) void { diff --git a/src/vm.zig b/src/vm.zig index 5a551a6d..1100ab0d 100644 --- a/src/vm.zig +++ b/src/vm.zig @@ -10237,18 +10237,8 @@ test "armJitFuel — cancellable = false prevents capping" { } // Small cross-platform ~1ms sleep used by the cancellation tests below. -// `std.posix.timespec` is `void` on Windows, so the nanosleep-based path -// cannot even be *constructed* on Windows — branch at comptime. fn sleepOneMillisecondForCancelTest() void { - if (builtin.os.tag == .windows) { - const K32 = struct { - extern "kernel32" fn Sleep(dwMilliseconds: u32) callconv(.winapi) void; - }; - K32.Sleep(1); - } else { - const req: std.posix.timespec = .{ .sec = 0, .nsec = 1 * std.time.ns_per_ms }; - _ = std.c.nanosleep(&req, null); - } + @import("platform.zig").pfdSleepNs(std.time.ns_per_ms); } test "Cancellation — cancel flag stops interpreter loop" { diff --git a/src/wasi.zig b/src/wasi.zig index 09cb24d5..91eb819c 100644 --- a/src/wasi.zig +++ b/src/wasi.zig @@ -157,11 +157,7 @@ const HostHandle = struct { } fn close(self: HostHandle) void { - if (builtin.os.tag == .windows) { - _ = windows.CloseHandle(self.raw); - } else { - _ = std.c.close(self.raw); - } + platform.pfdClose(self.raw); } fn stat(self: HostHandle, io: std.Io) !std.Io.File.Stat { @@ -186,7 +182,7 @@ const HostHandle = struct { } break :blk dup_handle; } else blk: { - const rc = std.c.dup(self.raw); + const rc = platform.pfdDup(self.raw); if (rc < 0) return error.Unexpected; break :blk rc; }; @@ -288,11 +284,7 @@ pub const WasiContext = struct { } fn closeHandle(handle: std.Io.File.Handle) void { - if (builtin.os.tag == .windows) { - _ = windows.CloseHandle(handle); - } else { - _ = std.c.close(handle); - } + platform.pfdClose(handle); } pub fn deinit(self: *WasiContext) void { @@ -520,9 +512,13 @@ pub fn fdSize(fd: posix.fd_t) ?u64 { return @intCast(end); } -/// Read libc errno and map to a WASI Errno. +/// Map the most recent platform errno (set by `platform.pfd*` helpers or +/// by explicit `platform.syncErrnoFromLibC()` calls after a raw `std.c.*` +/// invocation) to a WASI `Errno`. Replaces the pre-W46 variant that read +/// `std.c._errno().*` directly — keeping it libc-free is what lets Linux +/// builds drop `link_libc = true`. fn cErrnoToWasi() Errno { - const e: std.posix.E = @enumFromInt(std.c._errno().*); + const e = platform.pfdErrno(); return switch (e) { .ACCES => .ACCES, .AGAIN => .AGAIN, @@ -1568,7 +1564,21 @@ pub fn fd_datasync(ctx: *anyopaque, _: usize) anyerror!void { return; } } else { - if (std.c.fdatasync(host_fd) != 0) { + const failed = switch (comptime builtin.os.tag) { + .linux => blk: { + const rc = std.os.linux.fdatasync(host_fd); + const e = std.os.linux.errno(rc); + if (e != .SUCCESS) platform.pfd_last_errno = e; + break :blk e != .SUCCESS; + }, + .windows => unreachable, + else => blk: { + const rc = std.c.fdatasync(host_fd); + if (rc != 0) platform.syncErrnoFromLibC(); + break :blk rc != 0; + }, + }; + if (failed) { try pushErrno(vm, cErrnoToWasi()); return; } @@ -1597,16 +1607,9 @@ pub fn fd_sync(ctx: *anyopaque, _: usize) anyerror!void { }; if (wasi.getHostFd(fd)) |host_fd| { - if (builtin.os.tag == .windows) { - if (platform.FlushFileBuffers(host_fd) == windows.BOOL.FALSE) { - try pushErrno(vm, .IO); - return; - } - } else { - if (std.c.fsync(host_fd) != 0) { - try pushErrno(vm, cErrnoToWasi()); - return; - } + if (platform.pfdFsync(host_fd) != 0) { + try pushErrno(vm, cErrnoToWasi()); + return; } try pushErrno(vm, .SUCCESS); } else { @@ -1946,19 +1949,21 @@ pub fn fd_fdstat_set_flags(ctx: *anyopaque, _: usize) anyerror!void { if (fdflags & 0x04 != 0) os_flags |= @as(u32, @bitCast(posix.O{ .NONBLOCK = true })); if (fdflags & 0x10 != 0) os_flags |= @as(u32, @bitCast(posix.O{ .SYNC = true })); - if (comptime builtin.os.tag == .linux) { - const linux = std.os.linux; - const rc = linux.fcntl(host_fd, linux.F.SETFL, @as(usize, os_flags)); - if (posix.errno(rc) != .SUCCESS) { - try pushErrno(vm, .IO); - return; - } - } else { - const rc = std.c.fcntl(host_fd, std.c.F.SETFL, os_flags); - if (rc < 0) { - try pushErrno(vm, .IO); - return; - } + const failed = switch (comptime builtin.os.tag) { + .linux => blk: { + const linux = std.os.linux; + const rc = linux.fcntl(host_fd, linux.F.SETFL, @as(usize, os_flags)); + break :blk posix.errno(rc) != .SUCCESS; + }, + .windows => false, // fcntl not meaningful on Windows; treat as success + else => blk: { + const rc = std.c.fcntl(host_fd, std.c.F.SETFL, os_flags); + break :blk rc < 0; + }, + }; + if (failed) { + try pushErrno(vm, .IO); + return; } try pushErrno(vm, .SUCCESS); } @@ -1999,7 +2004,21 @@ pub fn fd_filestat_set_size(ctx: *anyopaque, _: usize) anyerror!void { }; if (wasi.getHostFd(fd)) |host_fd| { - if (std.c.ftruncate(host_fd, @bitCast(size)) != 0) { + const failed = switch (comptime builtin.os.tag) { + .linux => blk: { + const rc = std.os.linux.ftruncate(host_fd, @bitCast(size)); + const e = std.os.linux.errno(rc); + if (e != .SUCCESS) platform.pfd_last_errno = e; + break :blk e != .SUCCESS; + }, + .windows => unreachable, + else => blk: { + const rc = std.c.ftruncate(host_fd, @bitCast(size)); + if (rc != 0) platform.syncErrnoFromLibC(); + break :blk rc != 0; + }, + }; + if (failed) { try pushErrno(vm, cErrnoToWasi()); return; } @@ -2058,10 +2077,14 @@ pub fn fd_filestat_set_times(ctx: *anyopaque, _: usize) anyerror!void { // utimensat(fd, NULL, times, 0) == futimens(fd, times) const rc = std.os.linux.utimensat(host_fd, null, ×, 0); const e = std.os.linux.errno(rc); - if (e != .SUCCESS) std.c._errno().* = @intFromEnum(e); + if (e != .SUCCESS) platform.pfd_last_errno = e; break :blk e != .SUCCESS; }, - else => std.c.futimens(host_fd, ×) != 0, + else => blk: { + const rc = std.c.futimens(host_fd, ×); + if (rc != 0) platform.syncErrnoFromLibC(); + break :blk rc != 0; + }, }; if (failed) { try pushErrno(vm, cErrnoToWasi()); @@ -2550,7 +2573,21 @@ pub fn path_symlink(ctx: *anyopaque, _: usize) anyerror!void { try pushErrno(vm, .NAMETOOLONG); return; }; - if (std.c.symlinkat(old_z.ptr, host_fd, new_z.ptr) != 0) { + const failed = switch (comptime builtin.os.tag) { + .linux => blk: { + const rc = std.os.linux.symlinkat(old_z.ptr, host_fd, new_z.ptr); + const e = std.os.linux.errno(rc); + if (e != .SUCCESS) platform.pfd_last_errno = e; + break :blk e != .SUCCESS; + }, + .windows => unreachable, + else => blk: { + const rc = std.c.symlinkat(old_z.ptr, host_fd, new_z.ptr); + if (rc != 0) platform.syncErrnoFromLibC(); + break :blk rc != 0; + }, + }; + if (failed) { try pushErrno(vm, cErrnoToWasi()); return; } @@ -2606,7 +2643,21 @@ pub fn path_link(ctx: *anyopaque, _: usize) anyerror!void { try pushErrno(vm, .NAMETOOLONG); return; }; - if (std.c.linkat(old_host_fd, old_z.ptr, new_host_fd, new_z.ptr, 0) != 0) { + const failed = switch (comptime builtin.os.tag) { + .linux => blk: { + const rc = std.os.linux.linkat(old_host_fd, old_z.ptr, new_host_fd, new_z.ptr, 0); + const e = std.os.linux.errno(rc); + if (e != .SUCCESS) platform.pfd_last_errno = e; + break :blk e != .SUCCESS; + }, + .windows => unreachable, + else => blk: { + const rc = std.c.linkat(old_host_fd, old_z.ptr, new_host_fd, new_z.ptr, 0); + if (rc != 0) platform.syncErrnoFromLibC(); + break :blk rc != 0; + }, + }; + if (failed) { try pushErrno(vm, cErrnoToWasi()); return; } @@ -2866,16 +2917,16 @@ test "WASI — fd_write via 07_wasi_hello.wasm" { // Create pipe for capturing stdout var pipe_fds: [2]posix.fd_t = undefined; - if (std.c.pipe(&pipe_fds) != 0) return error.SkipZigTest; + if (platform.pfdPipe(&pipe_fds) != 0) return error.SkipZigTest; const pipe = pipe_fds; - defer _ = std.c.close(pipe[0]); + defer platform.pfdClose(pipe[0]); // Redirect stdout to pipe write end - const saved_stdout = std.c.dup(@as(posix.fd_t, 1)); + const saved_stdout = platform.pfdDup(@as(posix.fd_t, 1)); if (saved_stdout < 0) return error.SkipZigTest; - defer _ = std.c.close(saved_stdout); - if (std.c.dup2(pipe[1], @as(posix.fd_t, 1)) < 0) return error.SkipZigTest; - _ = std.c.close(pipe[1]); + defer platform.pfdClose(saved_stdout); + if (platform.pfdDup2(pipe[1], @as(posix.fd_t, 1)) < 0) return error.SkipZigTest; + platform.pfdClose(pipe[1]); // Run _start var vm_inst = Vm.init(alloc); @@ -2886,11 +2937,11 @@ test "WASI — fd_write via 07_wasi_hello.wasm" { }; // Restore stdout - _ = std.c.dup2(saved_stdout, @as(posix.fd_t, 1)); + _ = platform.pfdDup2(saved_stdout, @as(posix.fd_t, 1)); // Read captured output var buf: [256]u8 = undefined; - const n_rc = std.c.read(pipe[0], &buf, buf.len); + const n_rc = platform.pfdRead(pipe[0], buf[0..]); if (n_rc < 0) return error.SkipZigTest; const output = buf[0..@intCast(n_rc)]; @@ -3427,9 +3478,9 @@ test "stdio override: custom fd replaces default" { // Create a pipe to use as custom stdout var pipe_fds: [2]std.posix.fd_t = undefined; - if (std.c.pipe(&pipe_fds) != 0) return error.SkipZigTest; + if (platform.pfdPipe(&pipe_fds) != 0) return error.SkipZigTest; const pipe = pipe_fds; - defer _ = std.c.close(pipe[0]); + defer platform.pfdClose(pipe[0]); // Set stdout (fd 1) to write end of pipe, with ownership (runtime closes it) ctx.setStdioFd(1, pipe[1], .own); @@ -3448,10 +3499,10 @@ test "stdio override: borrow mode does not close fd on deinit" { const alloc = testing.allocator; var pipe_fds: [2]std.posix.fd_t = undefined; - if (std.c.pipe(&pipe_fds) != 0) return error.SkipZigTest; + if (platform.pfdPipe(&pipe_fds) != 0) return error.SkipZigTest; const pipe = pipe_fds; - defer _ = std.c.close(pipe[0]); - defer _ = std.c.close(pipe[1]); + defer platform.pfdClose(pipe[0]); + defer platform.pfdClose(pipe[1]); { var ctx = WasiContext.init(alloc); @@ -3461,7 +3512,7 @@ test "stdio override: borrow mode does not close fd on deinit" { // pipe[1] should still be valid (borrowed, not closed by deinit) // Writing to it should succeed - const written_rc = std.c.write(pipe[1], "ok", 2); + const written_rc = platform.pfdWrite(pipe[1], "ok"); try testing.expect(written_rc == 2); }