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

This commit is contained in:
Andrew Kelley
2026-01-07 12:09:09 -08:00
parent d2d8b969a1
commit 6a5bb3ede3
10 changed files with 106 additions and 84 deletions
+3 -5
View File
@@ -1,10 +1,8 @@
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const preopens = try std.fs.wasi.preopensAlloc(init.arena.allocator());
for (preopens.names, 0..) |preopen, i| {
std.debug.print("{d}: {s}\n", .{ i, preopen });
pub fn main(init: std.process.Init) void {
for (init.preopens.map.keys(), 0..) |preopen, i| {
std.log.info("{d}: {s}", .{ i, preopen });
}
}
-1
View File
@@ -4,7 +4,6 @@ const std = @import("std.zig");
/// Deprecated, use `std.Io.Dir.path`.
pub const path = @import("fs/path.zig");
pub const wasi = @import("fs/wasi.zig");
pub const base64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".*;
-55
View File
@@ -1,55 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const mem = std.mem;
const math = std.math;
const fs = std.fs;
const assert = std.debug.assert;
const Allocator = mem.Allocator;
const wasi = std.os.wasi;
const fd_t = wasi.fd_t;
const prestat_t = wasi.prestat_t;
pub const Preopens = struct {
// Indexed by file descriptor number.
names: []const []const u8,
pub fn find(p: Preopens, name: []const u8) ?std.posix.fd_t {
for (p.names, 0..) |elem_name, i| {
if (mem.eql(u8, elem_name, name)) {
return @intCast(i);
}
}
return null;
}
};
pub fn preopensAlloc(gpa: Allocator) Allocator.Error!Preopens {
var names: std.ArrayList([]const u8) = .empty;
defer names.deinit(gpa);
try names.ensureUnusedCapacity(gpa, 3);
names.appendAssumeCapacity("stdin"); // 0
names.appendAssumeCapacity("stdout"); // 1
names.appendAssumeCapacity("stderr"); // 2
while (true) {
const fd = @as(wasi.fd_t, @intCast(names.items.len));
var prestat: prestat_t = undefined;
switch (wasi.fd_prestat_get(fd, &prestat)) {
.SUCCESS => {},
.OPNOTSUPP, .BADF => return .{ .names = try names.toOwnedSlice(gpa) },
else => @panic("fd_prestat_get: unexpected error"),
}
try names.ensureUnusedCapacity(gpa, 1);
// This length does not include a null byte. Let's keep it this way to
// gently encourage WASI implementations to behave properly.
const name_len = prestat.u.dir.pr_name_len;
const name = try gpa.alloc(u8, name_len);
errdefer gpa.free(name);
switch (wasi.fd_prestat_dir_name(fd, name.ptr, name.len)) {
.SUCCESS => {},
else => @panic("fd_prestat_dir_name: unexpected error"),
}
names.appendAssumeCapacity(name);
}
}
+3 -2
View File
@@ -288,8 +288,9 @@ pub const oflags_t = packed struct(u16) {
_: u12 = 0,
};
pub const preopentype_t = u8;
pub const PREOPENTYPE_DIR: preopentype_t = 0;
pub const preopentype_t = enum(u8) {
DIR = 0,
};
pub const prestat_t = extern struct {
pr_type: preopentype_t,
+5
View File
@@ -18,6 +18,7 @@ const max_path_bytes = std.fs.max_path_bytes;
pub const Child = @import("process/Child.zig");
pub const Args = @import("process/Args.zig");
pub const Environ = @import("process/Environ.zig");
pub const Preopens = @import("process/Preopens.zig");
/// This is the global, process-wide protection to coordinate stderr writes.
///
@@ -48,6 +49,10 @@ pub const Init = struct {
io: Io,
/// Environment variables, initialized with `gpa`. Not threadsafe.
environ_map: *Environ.Map,
/// Named files that have been provided by the parent process. This is
/// mainly useful on WASI, but can be used on other systems to mimic the
/// behavior with respect to stdio.
preopens: Preopens,
/// Alternative to `Init` as the first parameter of the main function.
pub const Minimal = struct {
+75
View File
@@ -0,0 +1,75 @@
const Preopens = @This();
const builtin = @import("builtin");
const native_os = builtin.os.tag;
const std = @import("../std.zig");
const Io = std.Io;
const Allocator = std.mem.Allocator;
map: Map,
pub const empty: Preopens = switch (native_os) {
.wasi => .{ .map = .empty },
else => .{ .map = {} },
};
pub const Map = switch (native_os) {
// Indexed by file descriptor number.
.wasi => std.StringArrayHashMapUnmanaged(void),
else => void,
};
pub const Resource = union(enum) {
file: Io.File,
dir: Io.Dir,
};
pub fn get(p: *const Preopens, name: []const u8) ?Resource {
switch (native_os) {
.wasi => {
const index = p.map.getIndex(name) orelse return null;
if (index <= 2) return .{ .file = .{ .handle = @intCast(index) } };
return .{ .dir = .{ .handle = @intCast(index) } };
},
else => {
if (std.mem.eql(u8, name, "stdin")) return .{ .file = .stdin() };
if (std.mem.eql(u8, name, "stdout")) return .{ .file = .stdout() };
if (std.mem.eql(u8, name, "stderr")) return .{ .file = .stderr() };
return null;
},
}
}
pub const InitError = Allocator.Error || error{Unexpected};
pub fn init(arena: Allocator) InitError!Preopens {
if (native_os != .wasi) return .{ .map = {} };
const wasi = std.os.wasi;
var map: Map = .empty;
try map.ensureUnusedCapacity(arena, 3);
map.putAssumeCapacityNoClobber("stdin", {}); // 0
map.putAssumeCapacityNoClobber("stdout", {}); // 1
map.putAssumeCapacityNoClobber("stderr", {}); // 2
while (true) {
const fd: wasi.fd_t = @intCast(map.entries.len);
var prestat: wasi.prestat_t = undefined;
switch (wasi.fd_prestat_get(fd, &prestat)) {
.SUCCESS => {},
.OPNOTSUPP, .BADF => return .{ .map = map },
else => return error.Unexpected,
}
try map.ensureUnusedCapacity(arena, 1);
// This length does not include a null byte. Let's keep it this way to
// gently encourage WASI implementations to behave properly.
const name_len = prestat.u.dir.pr_name_len;
const name = try arena.alloc(u8, name_len);
switch (wasi.fd_prestat_dir_name(fd, name.ptr, name.len)) {
.SUCCESS => {},
else => return error.Unexpected,
}
map.putAssumeCapacityNoClobber(name, {});
}
}
+4
View File
@@ -708,6 +708,9 @@ inline fn callMain(args: std.process.Args.Vector, environ: std.process.Environ.B
std.process.fatal("failed to parse environment variables: {t}", .{err});
defer environ_map.deinit();
const preopens = std.process.Preopens.init(arena_allocator.allocator()) catch |err|
std.process.fatal("failed to init preopens: {t}", .{err});
return wrapMain(root.main(.{
.minimal = .{
.args = .{ .vector = args },
@@ -717,6 +720,7 @@ inline fn callMain(args: std.process.Args.Vector, environ: std.process.Environ.B
.gpa = gpa,
.io = threaded.io(),
.environ_map = &environ_map,
.preopens = preopens,
}));
}
+7 -9
View File
@@ -758,10 +758,7 @@ pub const Directories = struct {
search,
global,
},
wasi_preopens: switch (builtin.target.os.tag) {
.wasi => fs.wasi.Preopens,
else => void,
},
preopens: std.process.Preopens,
self_exe_path: switch (builtin.target.os.tag) {
.wasi => void,
else => []const u8,
@@ -776,7 +773,7 @@ pub const Directories = struct {
const zig_lib: Cache.Directory = d: {
if (override_zig_lib) |path| break :d openUnresolved(arena, io, cwd, path, .@"zig lib");
if (wasi) break :d openWasiPreopen(wasi_preopens, "/lib");
if (wasi) break :d getPreopen(preopens, "/lib");
break :d introspect.findZigLibDirFromSelfExe(arena, io, cwd, self_exe_path) catch |err| {
fatal("unable to find zig installation directory '{s}': {t}", .{ self_exe_path, err });
};
@@ -784,7 +781,7 @@ pub const Directories = struct {
const global_cache: Cache.Directory = d: {
if (override_global_cache) |path| break :d openUnresolved(arena, io, cwd, path, .@"global cache");
if (wasi) break :d openWasiPreopen(wasi_preopens, "/cache");
if (wasi) break :d getPreopen(preopens, "/cache");
const path = introspect.resolveGlobalCacheDir(arena, environ_map) catch |err| {
fatal("unable to resolve zig cache directory: {t}", .{err});
};
@@ -817,11 +814,12 @@ pub const Directories = struct {
.local_cache = local_cache,
};
}
fn openWasiPreopen(preopens: fs.wasi.Preopens, name: []const u8) Cache.Directory {
fn getPreopen(preopens: std.process.Preopens, name: []const u8) Cache.Directory {
return .{
.path = if (std.mem.eql(u8, name, ".")) null else name,
.handle = .{
.handle = preopens.find(name) orelse fatal("WASI preopen not found: '{s}'", .{name}),
.handle = switch (preopens.get(name) orelse fatal("preopen not found: '{s}'", .{name})) {
.file => fatal("preopen {s} is not a directory", .{name}),
.dir => |d| d,
},
};
}
+7 -7
View File
@@ -55,11 +55,11 @@ pub const std_options_cwd = if (native_os == .wasi) wasi_cwd else null;
pub const panic = crash_report.panic;
pub const debug = crash_report.debug;
var wasi_preopens: fs.wasi.Preopens = undefined;
var preopens: std.process.Preopens = .empty;
pub fn wasi_cwd() Io.Dir {
// Expect the first preopen to be current working directory.
const cwd_fd: std.posix.fd_t = 3;
assert(mem.eql(u8, wasi_preopens.names[cwd_fd], "."));
assert(mem.eql(u8, preopens.map.keys()[cwd_fd], "."));
return .{ .handle = cwd_fd };
}
@@ -210,7 +210,7 @@ pub fn main(init: std.process.Init.Minimal) anyerror!void {
}
if (native_os == .wasi) {
wasi_preopens = try fs.wasi.preopensAlloc(arena);
preopens = try .init(arena);
}
return mainArgs(gpa, arena, io, args, &environ_map);
@@ -360,7 +360,7 @@ fn mainArgs(
io,
&stdout_writer.interface,
args,
if (native_os == .wasi) wasi_preopens,
preopens,
&host,
environ_map,
);
@@ -3107,7 +3107,7 @@ fn buildOutputType(
else => .search,
};
},
if (native_os == .wasi) wasi_preopens,
preopens,
self_exe_path,
environ_map,
);
@@ -5141,7 +5141,7 @@ fn cmdBuild(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8,
if (override_local_cache_dir) |d| break :path d;
break :path try build_root.directory.join(arena, &.{introspect.default_local_zig_cache_basename});
} },
{},
.empty,
self_exe_path,
environ_map,
);
@@ -5556,7 +5556,7 @@ fn jitCmd(
override_lib_dir,
override_global_cache_dir,
.global,
if (native_os == .wasi) wasi_preopens,
preopens,
self_exe_path,
environ_map,
);
+2 -5
View File
@@ -14,10 +14,7 @@ pub fn cmdEnv(
io: Io,
out: *std.Io.Writer,
args: []const []const u8,
wasi_preopens: switch (builtin.target.os.tag) {
.wasi => std.fs.wasi.Preopens,
else => void,
},
preopens: std.process.Preopens,
host: *const std.Target,
environ_map: *std.process.Environ.Map,
) !void {
@@ -37,7 +34,7 @@ pub fn cmdEnv(
override_lib_dir,
override_global_cache_dir,
.global,
if (builtin.target.os.tag == .wasi) wasi_preopens,
preopens,
if (builtin.target.os.tag != .wasi) self_exe_path,
environ_map,
);