authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-21 19:22:42-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-23 22:15:12-08:00
loga29d79313a0c62843694ea723723447661239110
tree4d5cfd244696cd5933b8b815b5500398f0117f50
parent60481b82236fff1198975578c27c02ce6a7fa3fa

std.Io.Threaded: accept argv and environ on init

This is needed unfortunately for OpenBSD and Haiku for process executable path. I made it so that you can omit the options usually, but you get a compile error if you omit the options on those targets.

5 files changed, 103 insertions(+), 46 deletions(-)

lib/compiler/build_runner.zig+1-1
......@@ -39,7 +39,7 @@ pub fn main() !void {
3939
4040 const args = try process.argsAlloc(arena);
4141
42 var threaded: std.Io.Threaded = .init(gpa);
42 var threaded: std.Io.Threaded = .init(gpa, .{});
4343 defer threaded.deinit();
4444 const io = threaded.io();
4545
lib/compiler/test_runner.zig+2-2
......@@ -131,7 +131,7 @@ fn mainServer() !void {
131131
132132 .run_test => {
133133 testing.allocator_instance = .{};
134 testing.io_instance = .init(testing.allocator);
134 testing.io_instance = .init(testing.allocator, .{});
135135 log_err_count = 0;
136136 const index = try server.receiveBody_u32();
137137 const test_fn = builtin.test_functions[index];
......@@ -233,7 +233,7 @@ fn mainTerminal() void {
233233 var leaks: usize = 0;
234234 for (test_fn_list, 0..) |test_fn, i| {
235235 testing.allocator_instance = .{};
236 testing.io_instance = .init(testing.allocator);
236 testing.io_instance = .init(testing.allocator, .{});
237237 defer {
238238 testing.io_instance.deinit();
239239 if (testing.allocator_instance.deinit() == .leak) leaks += 1;
lib/std/Io/Threaded.zig+92-37
......@@ -29,23 +29,7 @@ join_requested: bool = false,
2929stack_size: usize,
3030/// All threads are spawned detached; this is how we wait until they all exit.
3131wait_group: std.Thread.WaitGroup = .{},
32/// Maximum thread pool size (excluding main thread) when dispatching async
33/// tasks. Until this limit, calls to `Io.async` when all threads are busy will
34/// cause a new thread to be spawned and permanently added to the pool. After
35/// this limit, calls to `Io.async` when all threads are busy run the task
36/// immediately.
37///
38/// Defaults to a number equal to logical CPU cores.
39///
40/// Protected by `mutex` once the I/O instance is already in use. See
41/// `setAsyncLimit`.
4232async_limit: Io.Limit,
43/// Maximum thread pool size (excluding main thread) for dispatching concurrent
44/// tasks. Until this limit, calls to `Io.concurrent` will increase the thread
45/// pool size.
46///
47/// concurrent tasks. After this number, calls to `Io.concurrent` return
48/// `error.ConcurrencyUnavailable`.
4933concurrent_limit: Io.Limit = .unlimited,
5034/// Error from calling `std.Thread.getCpuCount` in `init`.
5135cpu_count_error: ?std.Thread.CpuCountError,
......@@ -55,17 +39,7 @@ cpu_count_error: ?std.Thread.CpuCountError,
5539busy_count: usize = 0,
5640main_thread: Thread,
5741pid: Pid = .unknown,
58/// When a cancel request is made, blocking syscalls can be unblocked by
59/// issuing a signal. However, if the signal arrives after the check and before
60/// the syscall instruction, it is missed.
61///
62/// This option solves the race condition by retrying the signal delivery
63/// until it is acknowledged, with an exponential backoff.
64///
65/// Unfortunately, trying again until the cancellation request is acknowledged
66/// has been observed to be relatively slow, and usually strong cancellation
67/// guarantees are not needed, so this defaults to off.
68robust_cancel: RobustCancel = .disabled,
42robust_cancel: RobustCancel,
6943
7044wsa: if (is_windows) Wsa else struct {} = .{},
7145
......@@ -86,6 +60,32 @@ stderr_writer: File.Writer = .{
8660stderr_mode: Io.Terminal.Mode = .no_color,
8761stderr_writer_initialized: bool = false,
8862
63environ: Environ,
64args: Args,
65
66pub const Environ = switch (native_os) {
67 .openbsd, .haiku => struct {
68 PATH: ?[]const u8,
69
70 pub const empty: @This() = .{
71 .PATH = null,
72 };
73 },
74 else => struct {
75 pub const empty: @This() = .{};
76 },
77};
78
79pub const Args = switch (native_os) {
80 .openbsd, .haiku => struct {
81 list: []const []const u8,
82 pub const empty: @This() = .{ .list = &.{} };
83 },
84 else => struct {
85 pub const empty: @This() = .{};
86 },
87};
88
8989pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum {
9090 enabled,
9191 disabled,
......@@ -557,6 +557,54 @@ const Closure = struct {
557557 }
558558};
559559
560pub const InitOptions = struct {
561 /// Affects how many bytes are memory-mapped for threads.
562 stack_size: usize = std.Thread.SpawnConfig.default_stack_size,
563 /// Maximum thread pool size (excluding main thread) when dispatching async
564 /// tasks. Until this limit, calls to `Io.async` when all threads are busy will
565 /// cause a new thread to be spawned and permanently added to the pool. After
566 /// this limit, calls to `Io.async` when all threads are busy run the task
567 /// immediately.
568 ///
569 /// Defaults to a number equal to logical CPU cores.
570 ///
571 /// Protected by `Threaded.mutex` once the I/O instance is already in use. See
572 /// `setAsyncLimit`.
573 async_limit: ?Io.Limit = null,
574 /// Maximum thread pool size (excluding main thread) for dispatching concurrent
575 /// tasks. Until this limit, calls to `Io.concurrent` will increase the thread
576 /// pool size.
577 ///
578 /// concurrent tasks. After this number, calls to `Io.concurrent` return
579 /// `error.ConcurrencyUnavailable`.
580 concurrent_limit: Io.Limit = .unlimited,
581 /// When a cancel request is made, blocking syscalls can be unblocked by
582 /// issuing a signal. However, if the signal arrives after the check and before
583 /// the syscall instruction, it is missed.
584 ///
585 /// This option solves the race condition by retrying the signal delivery
586 /// until it is acknowledged, with an exponential backoff.
587 ///
588 /// Unfortunately, trying again until the cancellation request is acknowledged
589 /// has been observed to be relatively slow, and usually strong cancellation
590 /// guarantees are not needed, so this defaults to off.
591 robust_cancel: RobustCancel = .disabled,
592 /// Affects the following operations:
593 /// * `processExecutablePath` on OpenBSD and Haiku.
594 ///
595 /// The default value causes this to be a compile error on systems that need to
596 /// initialize this field. `Environ.empty` can be used to omit this field on
597 /// all targets.
598 environ: Environ = .{},
599 /// Affects the following operations:
600 /// * `processExecutablePath` on OpenBSD and Haiku.
601 ///
602 /// The default value causes this to be a compile error on systems that need to
603 /// initialize this field. `Args.empty` can be used to omit this field on all
604 /// targets.
605 args: Args = .{},
606};
607
560608/// Related:
561609/// * `init_single_threaded`
562610pub fn init(
......@@ -568,6 +616,7 @@ pub fn init(
568616 /// If these functions are avoided, then `Allocator.failing` may be passed
569617 /// here.
570618 gpa: Allocator,
619 options: InitOptions,
571620) Threaded {
572621 if (builtin.single_threaded) return .init_single_threaded;
573622
......@@ -575,8 +624,9 @@ pub fn init(
575624
576625 var t: Threaded = .{
577626 .allocator = gpa,
578 .stack_size = std.Thread.SpawnConfig.default_stack_size,
579 .async_limit = if (cpu_count) |n| .limited(n - 1) else |_| .nothing,
627 .stack_size = options.stack_size,
628 .async_limit = options.async_limit orelse if (cpu_count) |n| .limited(n - 1) else |_| .nothing,
629 .concurrent_limit = options.concurrent_limit,
580630 .cpu_count_error = if (cpu_count) |_| null else |e| e,
581631 .old_sig_io = undefined,
582632 .old_sig_pipe = undefined,
......@@ -586,6 +636,9 @@ pub fn init(
586636 .current_closure = null,
587637 .cancel_protection = undefined,
588638 },
639 .environ = options.environ,
640 .args = options.args,
641 .robust_cancel = options.robust_cancel,
589642 };
590643
591644 if (posix.Sigaction != void) {
......@@ -624,6 +677,9 @@ pub const init_single_threaded: Threaded = .{
624677 .current_closure = null,
625678 .cancel_protection = undefined,
626679 },
680 .robust_cancel = .disabled,
681 .environ = .empty,
682 .args = .empty,
627683};
628684
629685var global_single_threaded_instance: Threaded = .init_single_threaded;
......@@ -7186,18 +7242,17 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.Ex
71867242 }
71877243 },
71887244 .openbsd, .haiku => {
7189 // OpenBSD doesn't support getting the path of a running process, so try to guess it
7190 if (std.os.argv.len == 0)
7191 return error.FileNotFound;
7192
7193 const argv0 = std.mem.span(std.os.argv[0]);
7245 // The best we can do on these operating systems is check based on CLI args.
7246 const argv = t.args.list;
7247 if (argv.len == 0) return error.OperationUnsupported;
7248 const argv0 = argv[0];
71947249 if (std.mem.findScalar(u8, argv0, '/') != null) {
71957250 // argv[0] is a path (relative or absolute): use realpath(3) directly
71967251 const current_thread = Thread.getCurrent(t);
71977252 var resolved_buf: [std.c.PATH_MAX]u8 = undefined;
71987253 try current_thread.beginSyscall();
71997254 while (true) {
7200 if (std.c.realpath(std.os.argv[0], &resolved_buf)) |p| {
7255 if (std.c.realpath(argv[0], &resolved_buf)) |p| {
72017256 assert(p == &resolved_buf);
72027257 break current_thread.endSyscall();
72037258 } else switch (@as(std.c.E, @enumFromInt(std.c._errno().*))) {
......@@ -7229,12 +7284,12 @@ fn processExecutablePath(userdata: ?*anyopaque, out_buffer: []u8) std.process.Ex
72297284 } else if (argv0.len != 0) {
72307285 // argv[0] is not empty (and not a path): search PATH
72317286 const current_thread = Thread.getCurrent(t);
7232 const PATH = posix.getenvZ("PATH") orelse return error.FileNotFound;
7287 const PATH = t.environ.PATH orelse return error.FileNotFound;
72337288 var it = std.mem.tokenizeScalar(u8, PATH, ':');
72347289 it: while (it.next()) |dir| {
72357290 var resolved_path_buf: [std.c.PATH_MAX]u8 = undefined;
72367291 const resolved_path = std.fmt.bufPrintSentinel(&resolved_path_buf, "{s}/{s}", .{
7237 dir, std.os.argv[0],
7292 dir, argv[0],
72387293 }, 0) catch continue;
72397294
72407295 var resolved_buf: [std.c.PATH_MAX]u8 = undefined;
lib/std/Io/Threaded/test.zig+7-5
......@@ -1,3 +1,5 @@
1//! Tests belong here if they access internal state of std.Io.Threaded or
2//! otherwise assume details of that particular implementation.
13const builtin = @import("builtin");
24
35const std = @import("std");
......@@ -11,7 +13,7 @@ test "concurrent vs main prevents deadlock via oversubscription" {
1113 return error.SkipZigTest;
1214 }
1315
14 var threaded: Io.Threaded = .init(std.testing.allocator);
16 var threaded: Io.Threaded = .init(std.testing.allocator, .{});
1517 defer threaded.deinit();
1618 const io = threaded.io();
1719
......@@ -44,7 +46,7 @@ test "concurrent vs concurrent prevents deadlock via oversubscription" {
4446 return error.SkipZigTest;
4547 }
4648
47 var threaded: Io.Threaded = .init(std.testing.allocator);
49 var threaded: Io.Threaded = .init(std.testing.allocator, .{});
4850 defer threaded.deinit();
4951 const io = threaded.io();
5052
......@@ -78,7 +80,7 @@ test "async/concurrent context and result alignment" {
7880 var buffer: [2048]u8 align(@alignOf(ByteArray512)) = undefined;
7981 var fba: std.heap.FixedBufferAllocator = .init(&buffer);
8082
81 var threaded: std.Io.Threaded = .init(fba.allocator());
83 var threaded: std.Io.Threaded = .init(fba.allocator(), .{});
8284 defer threaded.deinit();
8385 const io = threaded.io();
8486
......@@ -111,7 +113,7 @@ test "Group.async context alignment" {
111113 var buffer: [2048]u8 align(@alignOf(ByteArray512)) = undefined;
112114 var fba: std.heap.FixedBufferAllocator = .init(&buffer);
113115
114 var threaded: std.Io.Threaded = .init(fba.allocator());
116 var threaded: std.Io.Threaded = .init(fba.allocator(), .{});
115117 defer threaded.deinit();
116118 const io = threaded.io();
117119
......@@ -131,7 +133,7 @@ fn returnArray() [32]u8 {
131133}
132134
133135test "async with array return type" {
134 var threaded: std.Io.Threaded = .init(std.testing.allocator);
136 var threaded: std.Io.Threaded = .init(std.testing.allocator, .{});
135137 defer threaded.deinit();
136138 const io = threaded.io();
137139
src/main.zig+1-1
......@@ -241,7 +241,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
241241 }
242242 }
243243
244 var threaded: Io.Threaded = .init(gpa);
244 var threaded: Io.Threaded = .init(gpa, .{});
245245 defer threaded.deinit();
246246 threaded_impl_ptr = &threaded;
247247 threaded.stack_size = thread_stack_size;