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 @@...@@ -1,10 +1,8 @@
1const std = @import("std");1const std = @import("std");
22
3pub fn main(init: std.process.Init) !void {3pub fn main(init: std.process.Init) void {
4 const preopens = try std.fs.wasi.preopensAlloc(init.arena.allocator());4 for (init.preopens.map.keys(), 0..) |preopen, i| {
55 std.log.info("{d}: {s}", .{ i, preopen });
6 for (preopens.names, 0..) |preopen, i| {
7 std.debug.print("{d}: {s}\n", .{ i, preopen });
8 }6 }
9}7}
108
lib/std/fs.zig-1
...@@ -4,7 +4,6 @@ const std = @import("std.zig");...@@ -4,7 +4,6 @@ const std = @import("std.zig");
44
5/// Deprecated, use `std.Io.Dir.path`.5/// Deprecated, use `std.Io.Dir.path`.
6pub const path = @import("fs/path.zig");6pub const path = @import("fs/path.zig");
7pub const wasi = @import("fs/wasi.zig");
87
9pub const base64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".*;8pub 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) {...@@ -288,8 +288,9 @@ pub const oflags_t = packed struct(u16) {
288 _: u12 = 0,288 _: u12 = 0,
289};289};
290290
291pub const preopentype_t = u8;291pub const preopentype_t = enum(u8) {
292pub const PREOPENTYPE_DIR: preopentype_t = 0;292 DIR = 0,
293};
293294
294pub const prestat_t = extern struct {295pub const prestat_t = extern struct {
295 pr_type: preopentype_t,296 pr_type: preopentype_t,
lib/std/process.zig+5
...@@ -18,6 +18,7 @@ const max_path_bytes = std.fs.max_path_bytes;...@@ -18,6 +18,7 @@ const max_path_bytes = std.fs.max_path_bytes;
18pub const Child = @import("process/Child.zig");18pub const Child = @import("process/Child.zig");
19pub const Args = @import("process/Args.zig");19pub const Args = @import("process/Args.zig");
20pub const Environ = @import("process/Environ.zig");20pub const Environ = @import("process/Environ.zig");
21pub const Preopens = @import("process/Preopens.zig");
2122
22/// This is the global, process-wide protection to coordinate stderr writes.23/// This is the global, process-wide protection to coordinate stderr writes.
23///24///
...@@ -48,6 +49,10 @@ pub const Init = struct {...@@ -48,6 +49,10 @@ pub const Init = struct {
48 io: Io,49 io: Io,
49 /// Environment variables, initialized with `gpa`. Not threadsafe.50 /// Environment variables, initialized with `gpa`. Not threadsafe.
50 environ_map: *Environ.Map,51 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
52 /// Alternative to `Init` as the first parameter of the main function.57 /// Alternative to `Init` as the first parameter of the main function.
53 pub const Minimal = struct {58 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...@@ -708,6 +708,9 @@ inline fn callMain(args: std.process.Args.Vector, environ: std.process.Environ.B
708 std.process.fatal("failed to parse environment variables: {t}", .{err});708 std.process.fatal("failed to parse environment variables: {t}", .{err});
709 defer environ_map.deinit();709 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
711 return wrapMain(root.main(.{714 return wrapMain(root.main(.{
712 .minimal = .{715 .minimal = .{
713 .args = .{ .vector = args },716 .args = .{ .vector = args },
...@@ -717,6 +720,7 @@ inline fn callMain(args: std.process.Args.Vector, environ: std.process.Environ.B...@@ -717,6 +720,7 @@ inline fn callMain(args: std.process.Args.Vector, environ: std.process.Environ.B
717 .gpa = gpa,720 .gpa = gpa,
718 .io = threaded.io(),721 .io = threaded.io(),
719 .environ_map = &environ_map,722 .environ_map = &environ_map,
723 .preopens = preopens,
720 }));724 }));
721}725}
722726
src/Compilation.zig+7-9
...@@ -758,10 +758,7 @@ pub const Directories = struct {...@@ -758,10 +758,7 @@ pub const Directories = struct {
758 search,758 search,
759 global,759 global,
760 },760 },
761 wasi_preopens: switch (builtin.target.os.tag) {761 preopens: std.process.Preopens,
762 .wasi => fs.wasi.Preopens,
763 else => void,
764 },
765 self_exe_path: switch (builtin.target.os.tag) {762 self_exe_path: switch (builtin.target.os.tag) {
766 .wasi => void,763 .wasi => void,
767 else => []const u8,764 else => []const u8,
...@@ -776,7 +773,7 @@ pub const Directories = struct {...@@ -776,7 +773,7 @@ pub const Directories = struct {
776773
777 const zig_lib: Cache.Directory = d: {774 const zig_lib: Cache.Directory = d: {
778 if (override_zig_lib) |path| break :d openUnresolved(arena, io, cwd, path, .@"zig lib");775 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");
780 break :d introspect.findZigLibDirFromSelfExe(arena, io, cwd, self_exe_path) catch |err| {777 break :d introspect.findZigLibDirFromSelfExe(arena, io, cwd, self_exe_path) catch |err| {
781 fatal("unable to find zig installation directory '{s}': {t}", .{ self_exe_path, err });778 fatal("unable to find zig installation directory '{s}': {t}", .{ self_exe_path, err });
782 };779 };
...@@ -784,7 +781,7 @@ pub const Directories = struct {...@@ -784,7 +781,7 @@ pub const Directories = struct {
784781
785 const global_cache: Cache.Directory = d: {782 const global_cache: Cache.Directory = d: {
786 if (override_global_cache) |path| break :d openUnresolved(arena, io, cwd, path, .@"global cache");783 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");
788 const path = introspect.resolveGlobalCacheDir(arena, environ_map) catch |err| {785 const path = introspect.resolveGlobalCacheDir(arena, environ_map) catch |err| {
789 fatal("unable to resolve zig cache directory: {t}", .{err});786 fatal("unable to resolve zig cache directory: {t}", .{err});
790 };787 };
...@@ -817,11 +814,12 @@ pub const Directories = struct {...@@ -817,11 +814,12 @@ pub const Directories = struct {
817 .local_cache = local_cache,814 .local_cache = local_cache,
818 };815 };
819 }816 }
820 fn openWasiPreopen(preopens: fs.wasi.Preopens, name: []const u8) Cache.Directory {817 fn getPreopen(preopens: std.process.Preopens, name: []const u8) Cache.Directory {
821 return .{818 return .{
822 .path = if (std.mem.eql(u8, name, ".")) null else name,819 .path = if (std.mem.eql(u8, name, ".")) null else name,
823 .handle = .{820 .handle = switch (preopens.get(name) orelse fatal("preopen not found: '{s}'", .{name})) {
824 .handle = preopens.find(name) orelse fatal("WASI preopen not found: '{s}'", .{name}),821 .file => fatal("preopen {s} is not a directory", .{name}),
822 .dir => |d| d,
825 },823 },
826 };824 };
827 }825 }
src/main.zig+7-7
...@@ -55,11 +55,11 @@ pub const std_options_cwd = if (native_os == .wasi) wasi_cwd else null;...@@ -55,11 +55,11 @@ pub const std_options_cwd = if (native_os == .wasi) wasi_cwd else null;
55pub const panic = crash_report.panic;55pub const panic = crash_report.panic;
56pub const debug = crash_report.debug;56pub const debug = crash_report.debug;
5757
58var wasi_preopens: fs.wasi.Preopens = undefined;58var preopens: std.process.Preopens = .empty;
59pub fn wasi_cwd() Io.Dir {59pub fn wasi_cwd() Io.Dir {
60 // Expect the first preopen to be current working directory.60 // Expect the first preopen to be current working directory.
61 const cwd_fd: std.posix.fd_t = 3;61 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], "."));
63 return .{ .handle = cwd_fd };63 return .{ .handle = cwd_fd };
64}64}
6565
...@@ -210,7 +210,7 @@ pub fn main(init: std.process.Init.Minimal) anyerror!void {...@@ -210,7 +210,7 @@ pub fn main(init: std.process.Init.Minimal) anyerror!void {
210 }210 }
211211
212 if (native_os == .wasi) {212 if (native_os == .wasi) {
213 wasi_preopens = try fs.wasi.preopensAlloc(arena);213 preopens = try .init(arena);
214 }214 }
215215
216 return mainArgs(gpa, arena, io, args, &environ_map);216 return mainArgs(gpa, arena, io, args, &environ_map);
...@@ -360,7 +360,7 @@ fn mainArgs(...@@ -360,7 +360,7 @@ fn mainArgs(
360 io,360 io,
361 &stdout_writer.interface,361 &stdout_writer.interface,
362 args,362 args,
363 if (native_os == .wasi) wasi_preopens,363 preopens,
364 &host,364 &host,
365 environ_map,365 environ_map,
366 );366 );
...@@ -3107,7 +3107,7 @@ fn buildOutputType(...@@ -3107,7 +3107,7 @@ fn buildOutputType(
3107 else => .search,3107 else => .search,
3108 };3108 };
3109 },3109 },
3110 if (native_os == .wasi) wasi_preopens,3110 preopens,
3111 self_exe_path,3111 self_exe_path,
3112 environ_map,3112 environ_map,
3113 );3113 );
...@@ -5141,7 +5141,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8,...@@ -5141,7 +5141,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8,
5141 if (override_local_cache_dir) |d| break :path d;5141 if (override_local_cache_dir) |d| break :path d;
5142 break :path try build_root.directory.join(arena, &.{introspect.default_local_zig_cache_basename});5142 break :path try build_root.directory.join(arena, &.{introspect.default_local_zig_cache_basename});
5143 } },5143 } },
5144 {},5144 .empty,
5145 self_exe_path,5145 self_exe_path,
5146 environ_map,5146 environ_map,
5147 );5147 );
...@@ -5556,7 +5556,7 @@ fn jitCmd(...@@ -5556,7 +5556,7 @@ fn jitCmd(
5556 override_lib_dir,5556 override_lib_dir,
5557 override_global_cache_dir,5557 override_global_cache_dir,
5558 .global,5558 .global,
5559 if (native_os == .wasi) wasi_preopens,5559 preopens,
5560 self_exe_path,5560 self_exe_path,
5561 environ_map,5561 environ_map,
5562 );5562 );
src/print_env.zig+2-5
...@@ -14,10 +14,7 @@ pub fn cmdEnv(...@@ -14,10 +14,7 @@ pub fn cmdEnv(
14 io: Io,14 io: Io,
15 out: *std.Io.Writer,15 out: *std.Io.Writer,
16 args: []const []const u8,16 args: []const []const u8,
17 wasi_preopens: switch (builtin.target.os.tag) {17 preopens: std.process.Preopens,
18 .wasi => std.fs.wasi.Preopens,
19 else => void,
20 },
21 host: *const std.Target,18 host: *const std.Target,
22 environ_map: *std.process.Environ.Map,19 environ_map: *std.process.Environ.Map,
23) !void {20) !void {
...@@ -37,7 +34,7 @@ pub fn cmdEnv(...@@ -37,7 +34,7 @@ pub fn cmdEnv(
37 override_lib_dir,34 override_lib_dir,
38 override_global_cache_dir,35 override_global_cache_dir,
39 .global,36 .global,
40 if (builtin.target.os.tag == .wasi) wasi_preopens,37 preopens,
41 if (builtin.target.os.tag != .wasi) self_exe_path,38 if (builtin.target.os.tag != .wasi) self_exe_path,
42 environ_map,39 environ_map,
43 );40 );