authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-07 12:09:09-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-01-08 05:06:31+01:00
log6a5bb3ede36ab9dd7a5ce95e1339ca4e138886fc
tree95c2341b00e3adf9e9ea29ccdd391d8a5898c4e6
parentd2d8b969a1674a6583292631ca7decc94cb56145

std: find a better home for the "preopens" concept


10 files changed, 106 insertions(+), 84 deletions(-)

doc/langref/wasi_preopens.zig+3-5
......@@ -1,10 +1,8 @@
11const std = @import("std");
22
3pub fn main(init: std.process.Init) !void {
4 const preopens = try std.fs.wasi.preopensAlloc(init.arena.allocator());
5
6 for (preopens.names, 0..) |preopen, i| {
7 std.debug.print("{d}: {s}\n", .{ i, preopen });
3pub fn main(init: std.process.Init) void {
4 for (init.preopens.map.keys(), 0..) |preopen, i| {
5 std.log.info("{d}: {s}", .{ i, preopen });
86 }
97}
108
lib/std/fs.zig-1
......@@ -4,7 +4,6 @@ const std = @import("std.zig");
44
55/// Deprecated, use `std.Io.Dir.path`.
66pub const path = @import("fs/path.zig");
7pub const wasi = @import("fs/wasi.zig");
87
98pub const base64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".*;
109
lib/std/fs/wasi.zig deleted-55
......@@ -1,55 +0,0 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const mem = std.mem;
4const math = std.math;
5const fs = std.fs;
6const assert = std.debug.assert;
7const Allocator = mem.Allocator;
8const wasi = std.os.wasi;
9const fd_t = wasi.fd_t;
10const prestat_t = wasi.prestat_t;
11
12pub const Preopens = struct {
13 // Indexed by file descriptor number.
14 names: []const []const u8,
15
16 pub fn find(p: Preopens, name: []const u8) ?std.posix.fd_t {
17 for (p.names, 0..) |elem_name, i| {
18 if (mem.eql(u8, elem_name, name)) {
19 return @intCast(i);
20 }
21 }
22 return null;
23 }
24};
25
26pub fn preopensAlloc(gpa: Allocator) Allocator.Error!Preopens {
27 var names: std.ArrayList([]const u8) = .empty;
28 defer names.deinit(gpa);
29
30 try names.ensureUnusedCapacity(gpa, 3);
31
32 names.appendAssumeCapacity("stdin"); // 0
33 names.appendAssumeCapacity("stdout"); // 1
34 names.appendAssumeCapacity("stderr"); // 2
35 while (true) {
36 const fd = @as(wasi.fd_t, @intCast(names.items.len));
37 var prestat: prestat_t = undefined;
38 switch (wasi.fd_prestat_get(fd, &prestat)) {
39 .SUCCESS => {},
40 .OPNOTSUPP, .BADF => return .{ .names = try names.toOwnedSlice(gpa) },
41 else => @panic("fd_prestat_get: unexpected error"),
42 }
43 try names.ensureUnusedCapacity(gpa, 1);
44 // This length does not include a null byte. Let's keep it this way to
45 // gently encourage WASI implementations to behave properly.
46 const name_len = prestat.u.dir.pr_name_len;
47 const name = try gpa.alloc(u8, name_len);
48 errdefer gpa.free(name);
49 switch (wasi.fd_prestat_dir_name(fd, name.ptr, name.len)) {
50 .SUCCESS => {},
51 else => @panic("fd_prestat_dir_name: unexpected error"),
52 }
53 names.appendAssumeCapacity(name);
54 }
55}
lib/std/os/wasi.zig+3-2
......@@ -288,8 +288,9 @@ pub const oflags_t = packed struct(u16) {
288288 _: u12 = 0,
289289};
290290
291pub const preopentype_t = u8;
292pub const PREOPENTYPE_DIR: preopentype_t = 0;
291pub const preopentype_t = enum(u8) {
292 DIR = 0,
293};
293294
294295pub const prestat_t = extern struct {
295296 pr_type: preopentype_t,
lib/std/process.zig+5
......@@ -18,6 +18,7 @@ const max_path_bytes = std.fs.max_path_bytes;
1818pub const Child = @import("process/Child.zig");
1919pub const Args = @import("process/Args.zig");
2020pub const Environ = @import("process/Environ.zig");
21pub const Preopens = @import("process/Preopens.zig");
2122
2223/// This is the global, process-wide protection to coordinate stderr writes.
2324///
......@@ -48,6 +49,10 @@ pub const Init = struct {
4849 io: Io,
4950 /// Environment variables, initialized with `gpa`. Not threadsafe.
5051 environ_map: *Environ.Map,
52 /// Named files that have been provided by the parent process. This is
53 /// mainly useful on WASI, but can be used on other systems to mimic the
54 /// behavior with respect to stdio.
55 preopens: Preopens,
5156
5257 /// Alternative to `Init` as the first parameter of the main function.
5358 pub const Minimal = struct {
lib/std/process/Preopens.zig created+75
......@@ -0,0 +1,75 @@
1const Preopens = @This();
2
3const builtin = @import("builtin");
4const native_os = builtin.os.tag;
5
6const std = @import("../std.zig");
7const Io = std.Io;
8const Allocator = std.mem.Allocator;
9
10map: Map,
11
12pub const empty: Preopens = switch (native_os) {
13 .wasi => .{ .map = .empty },
14 else => .{ .map = {} },
15};
16
17pub const Map = switch (native_os) {
18 // Indexed by file descriptor number.
19 .wasi => std.StringArrayHashMapUnmanaged(void),
20 else => void,
21};
22
23pub const Resource = union(enum) {
24 file: Io.File,
25 dir: Io.Dir,
26};
27
28pub fn get(p: *const Preopens, name: []const u8) ?Resource {
29 switch (native_os) {
30 .wasi => {
31 const index = p.map.getIndex(name) orelse return null;
32 if (index <= 2) return .{ .file = .{ .handle = @intCast(index) } };
33 return .{ .dir = .{ .handle = @intCast(index) } };
34 },
35 else => {
36 if (std.mem.eql(u8, name, "stdin")) return .{ .file = .stdin() };
37 if (std.mem.eql(u8, name, "stdout")) return .{ .file = .stdout() };
38 if (std.mem.eql(u8, name, "stderr")) return .{ .file = .stderr() };
39 return null;
40 },
41 }
42}
43
44pub const InitError = Allocator.Error || error{Unexpected};
45
46pub fn init(arena: Allocator) InitError!Preopens {
47 if (native_os != .wasi) return .{ .map = {} };
48 const wasi = std.os.wasi;
49 var map: Map = .empty;
50
51 try map.ensureUnusedCapacity(arena, 3);
52
53 map.putAssumeCapacityNoClobber("stdin", {}); // 0
54 map.putAssumeCapacityNoClobber("stdout", {}); // 1
55 map.putAssumeCapacityNoClobber("stderr", {}); // 2
56 while (true) {
57 const fd: wasi.fd_t = @intCast(map.entries.len);
58 var prestat: wasi.prestat_t = undefined;
59 switch (wasi.fd_prestat_get(fd, &prestat)) {
60 .SUCCESS => {},
61 .OPNOTSUPP, .BADF => return .{ .map = map },
62 else => return error.Unexpected,
63 }
64 try map.ensureUnusedCapacity(arena, 1);
65 // This length does not include a null byte. Let's keep it this way to
66 // gently encourage WASI implementations to behave properly.
67 const name_len = prestat.u.dir.pr_name_len;
68 const name = try arena.alloc(u8, name_len);
69 switch (wasi.fd_prestat_dir_name(fd, name.ptr, name.len)) {
70 .SUCCESS => {},
71 else => return error.Unexpected,
72 }
73 map.putAssumeCapacityNoClobber(name, {});
74 }
75}
lib/std/start.zig+4
......@@ -708,6 +708,9 @@ inline fn callMain(args: std.process.Args.Vector, environ: std.process.Environ.B
708708 std.process.fatal("failed to parse environment variables: {t}", .{err});
709709 defer environ_map.deinit();
710710
711 const preopens = std.process.Preopens.init(arena_allocator.allocator()) catch |err|
712 std.process.fatal("failed to init preopens: {t}", .{err});
713
711714 return wrapMain(root.main(.{
712715 .minimal = .{
713716 .args = .{ .vector = args },
......@@ -717,6 +720,7 @@ inline fn callMain(args: std.process.Args.Vector, environ: std.process.Environ.B
717720 .gpa = gpa,
718721 .io = threaded.io(),
719722 .environ_map = &environ_map,
723 .preopens = preopens,
720724 }));
721725}
722726
src/Compilation.zig+7-9
......@@ -758,10 +758,7 @@ pub const Directories = struct {
758758 search,
759759 global,
760760 },
761 wasi_preopens: switch (builtin.target.os.tag) {
762 .wasi => fs.wasi.Preopens,
763 else => void,
764 },
761 preopens: std.process.Preopens,
765762 self_exe_path: switch (builtin.target.os.tag) {
766763 .wasi => void,
767764 else => []const u8,
......@@ -776,7 +773,7 @@ pub const Directories = struct {
776773
777774 const zig_lib: Cache.Directory = d: {
778775 if (override_zig_lib) |path| break :d openUnresolved(arena, io, cwd, path, .@"zig lib");
779 if (wasi) break :d openWasiPreopen(wasi_preopens, "/lib");
776 if (wasi) break :d getPreopen(preopens, "/lib");
780777 break :d introspect.findZigLibDirFromSelfExe(arena, io, cwd, self_exe_path) catch |err| {
781778 fatal("unable to find zig installation directory '{s}': {t}", .{ self_exe_path, err });
782779 };
......@@ -784,7 +781,7 @@ pub const Directories = struct {
784781
785782 const global_cache: Cache.Directory = d: {
786783 if (override_global_cache) |path| break :d openUnresolved(arena, io, cwd, path, .@"global cache");
787 if (wasi) break :d openWasiPreopen(wasi_preopens, "/cache");
784 if (wasi) break :d getPreopen(preopens, "/cache");
788785 const path = introspect.resolveGlobalCacheDir(arena, environ_map) catch |err| {
789786 fatal("unable to resolve zig cache directory: {t}", .{err});
790787 };
......@@ -817,11 +814,12 @@ pub const Directories = struct {
817814 .local_cache = local_cache,
818815 };
819816 }
820 fn openWasiPreopen(preopens: fs.wasi.Preopens, name: []const u8) Cache.Directory {
817 fn getPreopen(preopens: std.process.Preopens, name: []const u8) Cache.Directory {
821818 return .{
822819 .path = if (std.mem.eql(u8, name, ".")) null else name,
823 .handle = .{
824 .handle = preopens.find(name) orelse fatal("WASI preopen not found: '{s}'", .{name}),
820 .handle = switch (preopens.get(name) orelse fatal("preopen not found: '{s}'", .{name})) {
821 .file => fatal("preopen {s} is not a directory", .{name}),
822 .dir => |d| d,
825823 },
826824 };
827825 }
src/main.zig+7-7
......@@ -55,11 +55,11 @@ pub const std_options_cwd = if (native_os == .wasi) wasi_cwd else null;
5555pub const panic = crash_report.panic;
5656pub const debug = crash_report.debug;
5757
58var wasi_preopens: fs.wasi.Preopens = undefined;
58var preopens: std.process.Preopens = .empty;
5959pub fn wasi_cwd() Io.Dir {
6060 // Expect the first preopen to be current working directory.
6161 const cwd_fd: std.posix.fd_t = 3;
62 assert(mem.eql(u8, wasi_preopens.names[cwd_fd], "."));
62 assert(mem.eql(u8, preopens.map.keys()[cwd_fd], "."));
6363 return .{ .handle = cwd_fd };
6464}
6565
......@@ -210,7 +210,7 @@ pub fn main(init: std.process.Init.Minimal) anyerror!void {
210210 }
211211
212212 if (native_os == .wasi) {
213 wasi_preopens = try fs.wasi.preopensAlloc(arena);
213 preopens = try .init(arena);
214214 }
215215
216216 return mainArgs(gpa, arena, io, args, &environ_map);
......@@ -360,7 +360,7 @@ fn mainArgs(
360360 io,
361361 &stdout_writer.interface,
362362 args,
363 if (native_os == .wasi) wasi_preopens,
363 preopens,
364364 &host,
365365 environ_map,
366366 );
......@@ -3107,7 +3107,7 @@ fn buildOutputType(
31073107 else => .search,
31083108 };
31093109 },
3110 if (native_os == .wasi) wasi_preopens,
3110 preopens,
31113111 self_exe_path,
31123112 environ_map,
31133113 );
......@@ -5141,7 +5141,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8,
51415141 if (override_local_cache_dir) |d| break :path d;
51425142 break :path try build_root.directory.join(arena, &.{introspect.default_local_zig_cache_basename});
51435143 } },
5144 {},
5144 .empty,
51455145 self_exe_path,
51465146 environ_map,
51475147 );
......@@ -5556,7 +5556,7 @@ fn jitCmd(
55565556 override_lib_dir,
55575557 override_global_cache_dir,
55585558 .global,
5559 if (native_os == .wasi) wasi_preopens,
5559 preopens,
55605560 self_exe_path,
55615561 environ_map,
55625562 );
src/print_env.zig+2-5
......@@ -14,10 +14,7 @@ pub fn cmdEnv(
1414 io: Io,
1515 out: *std.Io.Writer,
1616 args: []const []const u8,
17 wasi_preopens: switch (builtin.target.os.tag) {
18 .wasi => std.fs.wasi.Preopens,
19 else => void,
20 },
17 preopens: std.process.Preopens,
2118 host: *const std.Target,
2219 environ_map: *std.process.Environ.Map,
2320) !void {
......@@ -37,7 +34,7 @@ pub fn cmdEnv(
3734 override_lib_dir,
3835 override_global_cache_dir,
3936 .global,
40 if (builtin.target.os.tag == .wasi) wasi_preopens,
37 preopens,
4138 if (builtin.target.os.tag != .wasi) self_exe_path,
4239 environ_map,
4340 );