authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-05 10:41:14+00:00
committergravatar for jacobly@ziglang.orgJacob Young <jacobly@ziglang.org> 2025-12-14 18:42:51-05:00
logb599ac5e82930c83b0d7232258c0c208a189c73e
treedde03d659db7ab557227f734eace0a254fa1215e
parent31a26bf483ea5cb59d362810f1095426b3aeeb25

std.job: add Robust Jobserver implementation


5 files changed, 599 insertions(+), 0 deletions(-)

CMakeLists.txt+3
...@@ -449,6 +449,9 @@ set(ZIG_STAGE2_SOURCES...@@ -449,6 +449,9 @@ set(ZIG_STAGE2_SOURCES
449 lib/std/heap.zig449 lib/std/heap.zig
450 lib/std/heap/arena_allocator.zig450 lib/std/heap/arena_allocator.zig
451 lib/std/json.zig451 lib/std/json.zig
452 lib/std/job.zig
453 lib/std/job/Client.zig
454 lib/std/job/Server.zig
452 lib/std/leb128.zig455 lib/std/leb128.zig
453 lib/std/log.zig456 lib/std/log.zig
454 lib/std/macho.zig457 lib/std/macho.zig
lib/std/job.zig created+116
...@@ -0,0 +1,116 @@
1//! This namespace provides an implementation of the Robust Jobserver protocol:
2//! https://codeberg.org/mlugg/robust-jobserver/
3//!
4//! `Client` and `Server` currently both support the `sysvsem` and `win32pipe`
5//! communication methods, meaning this implementation is usable on most POSIX
6//! targets and on Windows.
7
8pub const Client = @import("job/Client.zig");
9pub const Server = @import("job/Server.zig");
10
11pub const Method = enum { sysvsem, win32pipe };
12
13pub const sysv_sem = struct {
14 pub const supported = switch (builtin.os.tag) {
15 .linux,
16 .illumos,
17 .haiku,
18
19 .freebsd,
20 .netbsd,
21 .openbsd,
22 .dragonfly,
23
24 .driverkit,
25 .ios,
26 .maccatalyst,
27 .macos,
28 .tvos,
29 .visionos,
30 .watchos,
31 => true,
32
33 else => false,
34 };
35
36 /// `semget(IPC_PRIVATE, 1, 0o777)`
37 pub fn create() error{ SystemResources, Unexpected }!i32 {
38 const res = system.create();
39 switch (std.posix.errno(res)) {
40 .SUCCESS => return @intCast(res),
41 .NOMEM => return error.SystemResources,
42 .NOSPC => return error.SystemResources,
43 else => |e| return std.posix.unexpectedErrno(e),
44 }
45 }
46 /// `semctl(id, 0, SETVAL, n)`
47 pub fn setValue(id: i32, n: u32) error{Unexpected}!void {
48 switch (std.posix.errno(system.setValue(id, n))) {
49 .SUCCESS => return,
50 else => |e| return std.posix.unexpectedErrno(e),
51 }
52 }
53 /// `semop(id, &.{.{ .sem_num = 0, .sem_op = delta, .sem_flg = SEM_UNDO }})`
54 pub fn modify(id: i32, delta: i16) error{
55 InvalidSemaphore,
56 AccessDenied,
57 SystemResources,
58 /// A signal interrupted a blocked call to `modify`.
59 /// This allows the caller to implement cancelation.
60 Interrupted,
61 Unexpected,
62 }!void {
63 while (true) {
64 switch (std.posix.errno(system.modify(id, delta))) {
65 .SUCCESS => return,
66 .ACCES => return error.AccessDenied,
67 .FBIG => return error.InvalidSemaphore,
68 .IDRM => return error.InvalidSemaphore,
69 .INTR => return error.Interrupted,
70 .INVAL => return error.InvalidSemaphore,
71 .NOMEM => return error.SystemResources,
72 .RANGE => return error.InvalidSemaphore,
73 else => |e| return std.posix.unexpectedErrno(e),
74 }
75 }
76 }
77
78 const system = if (builtin.link_libc) struct {
79 fn create() c_int {
80 return std.c.semget(.IPC_PRIVATE, 1, 0o777);
81 }
82 fn setValue(id: i32, n: u32) c_int {
83 return std.c.semctl(id, 0, std.posix.SETVAL, n);
84 }
85 fn modify(id: i32, delta: i16) c_int {
86 var ops: [1]std.posix.sembuf = .{.{
87 .sem_num = 0,
88 .sem_op = delta,
89 .sem_flg = std.posix.SEM_UNDO,
90 }};
91 return std.c.semop(id, &ops, ops.len);
92 }
93 } else switch (builtin.os.tag) {
94 .linux => struct {
95 fn create() usize {
96 const key: std.posix.key_t = .IPC_PRIVATE;
97 return std.os.linux.syscall3(.semget, @intFromEnum(key), 1, 0o777);
98 }
99 fn setValue(id: i32, n: u32) usize {
100 return std.os.linux.syscall4(.semctl, @intCast(id), 0, std.posix.SETVAL, n);
101 }
102 fn modify(id: i32, delta: i16) usize {
103 var ops: [1]std.posix.sembuf = .{.{
104 .sem_num = 0,
105 .sem_op = delta,
106 .sem_flg = std.posix.SEM_UNDO,
107 }};
108 return std.os.linux.syscall3(.semop, @intCast(id), @intFromPtr(&ops), ops.len);
109 }
110 },
111 else => unreachable,
112 };
113};
114
115const builtin = @import("builtin");
116const std = @import("std.zig");
lib/std/job/Client.zig created+197
...@@ -0,0 +1,197 @@
1impl: Impl,
2
3pub const InitError = error{
4 OutOfMemory,
5 /// There is no advertised job server.
6 NoServer,
7 /// The job server is advertising a communication method which is not known.
8 UnknownMethod,
9 /// The job server is advertising a communication method which is known but unsupported.
10 UnsupportedMethod,
11 /// A job server advertisement exists, but is malformed.
12 InvalidArgument,
13 /// The job server has shut down or is otherwise not available to connect to.
14 ServerFailed,
15 /// This process does not have permission to access the job server.
16 AccessDenied,
17};
18pub fn init(arena: Allocator, env: *const std.process.EnvMap) InitError!Client {
19 const env_val = env.get("ROBUST_JOBSERVER") orelse return error.NoServer;
20 const idx = std.mem.findScalar(u8, env_val, ':') orelse return error.InvalidArgument;
21 const method = std.meta.stringToEnum(job.Method, env_val[0..idx]) orelse return error.UnknownMethod;
22 switch (method) {
23 inline else => |m| {
24 const ImplTy = @FieldType(Impl, @tagName(m));
25 if (ImplTy == noreturn) return error.UnsupportedMethod;
26 return .{ .impl = @unionInit(
27 Impl,
28 @tagName(m),
29 try .init(arena, env_val[idx + 1 ..]),
30 ) };
31 },
32 }
33}
34pub fn deinit(c: *Client) void {
35 switch (c.impl) {
36 inline else => |*x| x.deinit(),
37 }
38 c.* = undefined;
39}
40
41pub const AcquireError = error{
42 /// The job server has shut down or is otherwise not available to connect to.
43 ServerFailed,
44 /// This process does not have permission to access the job server.
45 AccessDenied,
46 /// Insufficient resources are available to acquire a token.
47 SystemResources,
48 Unexpected,
49};
50pub fn acquire(c: *const Client) AcquireError!Token {
51 return switch (c.impl) {
52 inline else => |*impl| impl.acquire(),
53 };
54}
55
56const Impl = union(job.Method) {
57 sysvsem: if (job.sysv_sem.supported) SysVSem else noreturn,
58 win32pipe: if (builtin.target.os.tag == .windows) Win32Pipe else noreturn,
59};
60
61pub const Token = union(job.Method) {
62 sysvsem: if (job.sysv_sem.supported) SysVSem else noreturn,
63 win32pipe: if (builtin.target.os.tag == .windows) windows.HANDLE else noreturn,
64 pub fn release(t: Token) void {
65 switch (t) {
66 .sysvsem => |sem| while (true) {
67 return job.sysv_sem.modify(sem.set_id, 1) catch |err| switch (err) {
68 error.AccessDenied, error.InvalidSemaphore => {
69 // The semaphore broke somehow, but that's not our problem!
70 // (...at least, not until we next call `acquire`.)
71 },
72 error.SystemResources => unreachable, // the undo structure was already allocated in `acquire`
73 error.Interrupted => continue, // releasing can't block; just retry
74 error.Unexpected => {}, // already warned, nothing more we can do
75 };
76 },
77 .win32pipe => |handle| _ = windows.ntdll.NtClose(handle),
78 }
79 }
80};
81
82const SysVSem = struct {
83 set_id: i32,
84 fn init(arena: Allocator, arg: []const u8) InitError!SysVSem {
85 _ = arena;
86 const set_id = std.fmt.parseInt(i32, arg, 10) catch return error.InvalidArgument;
87 return .{ .set_id = set_id };
88 }
89 fn deinit(sem: SysVSem) void {
90 _ = sem;
91 }
92 fn acquire(sem: SysVSem) AcquireError!Token {
93 while (true) {
94 break job.sysv_sem.modify(sem.set_id, -1) catch |err| switch (err) {
95 error.InvalidSemaphore => return error.ServerFailed,
96 error.Interrupted => continue, // TODO: support cancelation
97 error.AccessDenied, error.SystemResources, error.Unexpected => |e| return e,
98 };
99 }
100 return .{ .sysvsem = sem };
101 }
102};
103
104const Win32Pipe = struct {
105 pipe_device: windows.HANDLE,
106 pipe_path: [:0]const u16,
107 fn init(arena: Allocator, arg: []const u8) InitError!Win32Pipe {
108 if (arg.len == 0) return error.InvalidArgument;
109 if (std.mem.findAny(u8, arg, "\\/\x00") != null) return error.InvalidArgument;
110 const pipe_path = std.unicode.wtf8ToWtf16LeAllocZ(
111 arena,
112 try std.fmt.allocPrint(arena, "\\??\\pipe\\{s}", .{arg}),
113 ) catch |err| switch (err) {
114 error.InvalidWtf8 => return error.InvalidArgument,
115 error.OutOfMemory => |e| return e,
116 };
117 const pipe_device = windows.OpenFile(
118 std.unicode.wtf8ToWtf16LeStringLiteral("\\??\\pipe\\"),
119 .{
120 .access_mask = .{
121 .SPECIFIC = .{ .FILE_PIPE = .{ .READ_ATTRIBUTES = true } },
122 .STANDARD = .{ .SYNCHRONIZE = true },
123 },
124 .share_access = .{ .READ = true, .WRITE = true },
125 .creation = .OPEN,
126 },
127 ) catch |err| {
128 // This fixed path should always be accessible on Windows.
129 std.debug.panic("unexpected error opening '\\??\\pipe\\': {t}", .{err});
130 };
131 errdefer _ = windows.ntdll.NtClose(pipe_device);
132 return .{
133 .pipe_device = pipe_device,
134 .pipe_path = pipe_path,
135 };
136 }
137 fn deinit(wp: *const Win32Pipe) void {
138 _ = windows.ntdll.NtClose(wp.pipe_device);
139 }
140 fn acquire(wp: *const Win32Pipe) AcquireError!Token {
141 const pipe_basename_offset = std.unicode.wtf8ToWtf16LeStringLiteral("\\??\\pipe\\").len;
142 const handle = while (true) {
143 if (windows.OpenFile(wp.pipe_path, .{
144 .access_mask = .{ .STANDARD = .{ .SYNCHRONIZE = true } },
145 .creation = .OPEN,
146 .share_access = .{},
147 })) |handle| {
148 return .{ .win32pipe = handle };
149 } else |err| switch (err) {
150 error.PipeBusy, error.NoDevice => {},
151
152 error.IsDir,
153 error.FileNotFound,
154 error.NameTooLong,
155 error.AntivirusInterference,
156 error.BadPathName,
157 => return error.ServerFailed,
158
159 error.AccessDenied,
160 error.Unexpected,
161 => |e| return e,
162
163 error.NotDir => unreachable, // we're not opening as a directory
164 error.PathAlreadyExists => unreachable, // we're not trying to create the path
165 error.WouldBlock => unreachable, // we're not using overlapped I/O
166 error.NetworkNotFound => unreachable, // we're not accessing a network device
167 }
168 const fpwfb: windows.FILE.PIPE.WAIT_FOR_BUFFER = .init(.{
169 .Timeout = windows.FILE.PIPE.WAIT_FOR_BUFFER.WAIT_FOREVER,
170 .Name = wp.pipe_path[pipe_basename_offset..],
171 });
172 windows.DeviceIoControl(
173 wp.pipe_device,
174 windows.FSCTL.PIPE.WAIT,
175 .{ .in = fpwfb.toBuffer() },
176 ) catch |err| switch (err) {
177 error.UnrecognizedVolume => unreachable, // not a volume
178 error.Pending => unreachable, // not using overlapped I/O
179 error.PipeClosing => return error.ServerFailed,
180 error.PipeAlreadyConnected => unreachable,
181 error.PipeAlreadyListening => unreachable,
182 error.Unexpected, error.AccessDenied => |e| return e,
183 };
184 continue;
185 };
186 return .{ .win32pipe = handle };
187 }
188};
189
190const builtin = @import("builtin");
191
192const std = @import("../std.zig");
193const Allocator = std.mem.Allocator;
194const job = std.job;
195const windows = std.os.windows;
196
197const Client = @This();
lib/std/job/Server.zig created+282
...@@ -0,0 +1,282 @@
1/// `null` means we are inheriting a jobserver instance from a parent process.
2impl: ?Impl,
3
4pub const InitError = error{ OutOfMemory, SystemResources, Unexpected };
5pub fn init(arena: Allocator, job_limit: u32, env: *std.process.EnvMap) InitError!Server {
6 assert(job_limit > 0);
7 if (env.get("ROBUST_JOBSERVER") != null) {
8 return .{ .impl = null };
9 }
10 const method: job.Method = switch (builtin.target.os.tag) {
11 .windows => .win32pipe,
12 else => .sysvsem,
13 };
14 const impl: @FieldType(Impl, @tagName(method)) = try .init(arena, job_limit);
15 const env_val = try std.fmt.allocPrint(arena, "{t}:{f}", .{ method, std.fmt.alt(impl, .formatEnvData) });
16 try env.put("ROBUST_JOBSERVER", env_val);
17 return .{ .impl = @unionInit(Impl, @tagName(method), impl) };
18}
19pub fn deinit(s: *Server) void {
20 if (s.impl) |*impl| {
21 switch (impl.*) {
22 inline else => |*x| x.deinit(),
23 }
24 }
25 s.* = undefined;
26}
27
28const Impl = union(job.Method) {
29 sysvsem: if (job.sysv_sem.supported) SysVSem else noreturn,
30 win32pipe: if (builtin.target.os.tag == .windows) Win32Pipe else noreturn,
31};
32
33const SysVSem = struct {
34 set_id: i32,
35 fn init(arena: Allocator, num_tokens: u32) InitError!SysVSem {
36 _ = arena;
37 const id = try job.sysv_sem.create();
38 try job.sysv_sem.setValue(id, num_tokens);
39 return .{ .set_id = id };
40 }
41 fn deinit(sem: SysVSem) void {
42 _ = sem;
43 }
44 pub fn formatEnvData(sem: SysVSem, w: *std.Io.Writer) std.Io.Writer.Error!void {
45 try w.print("{d}", .{sem.set_id});
46 }
47};
48
49const Win32Pipe = struct {
50 pipe_name: []const u8,
51 done_event: windows.HANDLE,
52 thread: std.Thread,
53
54 const Token = struct {
55 handle: windows.HANDLE,
56 iosb: windows.IO_STATUS_BLOCK,
57 dummy_read_buf: [1]u8,
58 };
59
60 var pipe_name_counter: std.atomic.Value(u32) = .init(0);
61 fn init(arena: Allocator, num_tokens: u32) InitError!Win32Pipe {
62 const pipe_name = try std.fmt.allocPrint(arena, "zig-jobserver-{d}-{x}", .{
63 windows.GetCurrentProcessId(),
64 std.crypto.random.int(u64),
65 });
66
67 const nt_path = std.unicode.wtf8ToWtf16LeAllocZ(
68 arena,
69 try std.fmt.allocPrint(arena, "\\??\\pipe\\{s}", .{pipe_name}),
70 ) catch |err| switch (err) {
71 error.InvalidWtf8 => unreachable,
72 error.OutOfMemory => |e| return e,
73 };
74
75 const tokens = try arena.alloc(Token, num_tokens);
76 @memset(tokens, .{
77 .handle = windows.INVALID_HANDLE_VALUE,
78 .iosb = undefined,
79 .dummy_read_buf = undefined,
80 });
81 errdefer for (tokens) |*token| {
82 if (token.handle != windows.INVALID_HANDLE_VALUE) {
83 _ = windows.ntdll.NtClose(token.handle);
84 }
85 };
86
87 for (tokens) |*t| {
88 var path: windows.UNICODE_STRING = .{
89 .Buffer = nt_path.ptr,
90 .Length = @intCast(@sizeOf(u16) * nt_path.len),
91 .MaximumLength = 0,
92 };
93 var iosb: windows.IO_STATUS_BLOCK = undefined;
94 switch (windows.ntdll.NtCreateNamedPipeFile(
95 &t.handle,
96 .{ .GENERIC = .{ .READ = true } },
97 &.{
98 .Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
99 .RootDirectory = null,
100 .ObjectName = &path,
101 .Attributes = .{},
102 .SecurityDescriptor = null,
103 .SecurityQualityOfService = null,
104 },
105 &iosb,
106 .{ .WRITE = true },
107 .OPEN_IF,
108 .{ .IO = .ASYNCHRONOUS },
109 .{ .TYPE = .BYTE_STREAM },
110 .{ .MODE = .BYTE_STREAM },
111 .{ .OPERATION = .QUEUE },
112 @intCast(num_tokens),
113 0,
114 0,
115 &((-120 * std.time.ns_per_s) / 100),
116 )) {
117 .SUCCESS => {},
118 .INSUFFICIENT_RESOURCES => return error.SystemResources,
119 else => |e| return windows.unexpectedStatus(e),
120 }
121 }
122
123 var done_event: windows.HANDLE = undefined;
124 switch (windows.ntdll.NtCreateEvent(
125 &done_event,
126 windows.ACCESS_MASK.Specific.Event.ALL_ACCESS,
127 null,
128 .Notification,
129 windows.FALSE,
130 )) {
131 .SUCCESS => {},
132 .INSUFFICIENT_RESOURCES => return error.SystemResources,
133 else => |e| return windows.unexpectedStatus(e),
134 }
135 errdefer _ = windows.ntdll.NtClose(done_event);
136
137 const thread = std.Thread.spawn(.{}, serve, .{ tokens, done_event }) catch |err| switch (err) {
138 error.SystemResources,
139 error.Unexpected,
140 error.OutOfMemory,
141 => |e| return e,
142
143 error.ThreadQuotaExceeded,
144 error.LockedMemoryLimitExceeded,
145 => return error.SystemResources,
146 };
147 errdefer comptime unreachable; // the thread is now running and owns `tokens`
148
149 return .{
150 .pipe_name = pipe_name,
151 .done_event = done_event,
152 .thread = thread,
153 };
154 }
155 fn deinit(wp: *const Win32Pipe) void {
156 _ = windows.ntdll.NtSetEvent(wp.done_event, null);
157 wp.thread.join();
158 _ = windows.ntdll.NtClose(wp.done_event);
159 }
160 pub fn formatEnvData(wp: Win32Pipe, w: *std.Io.Writer) std.Io.Writer.Error!void {
161 try w.writeAll(wp.pipe_name);
162 }
163
164 fn serve(tokens: []Token, done_event: windows.HANDLE) void {
165 defer {
166 for (tokens) |*token| {
167 _ = windows.ntdll.NtClose(token.handle);
168 }
169 }
170
171 for (tokens) |*t| serveToken(t, .connect);
172
173 while (true) {
174 switch (windows.ntdll.NtWaitForSingleObject(
175 done_event,
176 windows.TRUE,
177 null,
178 )) {
179 windows.NTSTATUS.ABANDONED_WAIT_0 => unreachable, // not a mutex
180 .USER_APC => continue,
181 windows.NTSTATUS.WAIT_0 => break,
182 .TIMEOUT => unreachable, // no timeout
183 else => |e| std.debug.panic("unexpected NTSTATUS=0x{x} in job server", .{@intFromEnum(e)}),
184 }
185 }
186 }
187 const Action = enum { connect, read, disconnect };
188 fn serveToken(token: *Token, first_action: Action) void {
189 action: switch (first_action) {
190 .connect => if (windows.DeviceIoControl(token.handle, windows.FSCTL.PIPE.LISTEN, .{
191 .apc_routine = &connectCompleted,
192 .apc_context = token,
193 .io_status_block = &token.iosb,
194 })) |_| {
195 return; // The APC has been queued and will continue the loop.
196 } else |err| switch (err) {
197 error.AccessDenied => unreachable, // we created the pipe
198 error.UnrecognizedVolume => unreachable, // it's not a volume
199 error.Pending => return,
200 error.PipeClosing => continue :action .disconnect,
201 error.PipeAlreadyConnected => continue :action .read,
202 error.PipeAlreadyListening => unreachable, // pipe is not in nonblocking mode
203 error.Unexpected => @panic("unexpected error in job server"),
204 },
205 .read => switch (windows.ntdll.NtReadFile(
206 token.handle,
207 null,
208 &readCompleted,
209 token,
210 &token.iosb,
211 &token.dummy_read_buf,
212 token.dummy_read_buf.len,
213 null,
214 null,
215 )) {
216 .PENDING => return,
217 .PIPE_BROKEN => continue :action .disconnect,
218 .SUCCESS => {
219 // The client isn't meant to write to the pipe---disconnect them as punishment.
220 return; // The APC has been queued and will do that for us.
221 },
222 else => |e| std.debug.panic("unexpected NTSTATUS=0x{x} in job server", .{@intFromEnum(e)}),
223 },
224 .disconnect => if (windows.DeviceIoControl(token.handle, windows.FSCTL.PIPE.DISCONNECT, .{
225 .apc_routine = &disconnectCompleted,
226 .apc_context = token,
227 .io_status_block = &token.iosb,
228 })) |_| {
229 return; // The APC has been queued and will continue the loop.
230 } else |err| switch (err) {
231 error.AccessDenied => unreachable, // we created the pipe
232 error.UnrecognizedVolume => unreachable, // it's not a volume
233 error.Pending => return,
234 error.PipeClosing => unreachable,
235 error.PipeAlreadyConnected => unreachable,
236 error.PipeAlreadyListening => unreachable,
237 error.Unexpected => @panic("unexpected error in job server"),
238 },
239 }
240 }
241 fn connectCompleted(
242 ctx: ?*anyopaque,
243 iosb: *windows.IO_STATUS_BLOCK,
244 _: windows.ULONG,
245 ) callconv(.winapi) void {
246 serveToken(@ptrCast(@alignCast(ctx)), switch (iosb.u.Status) {
247 .SUCCESS, .PIPE_CONNECTED => .read,
248 .PIPE_CLOSING => .disconnect,
249 else => |e| std.debug.panic("unexpected NTSTATUS=0x{x} in job server", .{@intFromEnum(e)}),
250 });
251 }
252 fn readCompleted(
253 ctx: ?*anyopaque,
254 iosb: *windows.IO_STATUS_BLOCK,
255 _: windows.ULONG,
256 ) callconv(.winapi) void {
257 serveToken(@ptrCast(@alignCast(ctx)), switch (iosb.u.Status) {
258 .SUCCESS, .PIPE_BROKEN => .disconnect,
259 else => |e| std.debug.panic("unexpected NTSTATUS=0x{x} in job server", .{@intFromEnum(e)}),
260 });
261 }
262 fn disconnectCompleted(
263 ctx: ?*anyopaque,
264 iosb: *windows.IO_STATUS_BLOCK,
265 _: windows.ULONG,
266 ) callconv(.winapi) void {
267 serveToken(@ptrCast(@alignCast(ctx)), switch (iosb.u.Status) {
268 .SUCCESS => .connect,
269 else => |e| std.debug.panic("unexpected NTSTATUS=0x{x} in job server", .{@intFromEnum(e)}),
270 });
271 }
272};
273
274const builtin = @import("builtin");
275
276const std = @import("std");
277const Allocator = std.mem.Allocator;
278const assert = std.debug.assert;
279const job = std.job;
280const windows = std.os.windows;
281
282const Server = @This();
lib/std/std.zig+1
...@@ -78,6 +78,7 @@ pub const hash = @import("hash.zig");...@@ -78,6 +78,7 @@ pub const hash = @import("hash.zig");
78pub const hash_map = @import("hash_map.zig");78pub const hash_map = @import("hash_map.zig");
79pub const heap = @import("heap.zig");79pub const heap = @import("heap.zig");
80pub const http = @import("http.zig");80pub const http = @import("http.zig");
81pub const job = @import("job.zig");
81pub const json = @import("json.zig");82pub const json = @import("json.zig");
82pub const leb = @import("leb128.zig");83pub const leb = @import("leb128.zig");
83pub const log = @import("log.zig");84pub const log = @import("log.zig");