| 1 | //! This struct represents a kernel thread. |
| 2 | const Thread = @This(); |
| 3 | |
| 4 | const builtin = @import("builtin"); |
| 5 | const target = builtin.target; |
| 6 | const native_os = builtin.os.tag; |
| 7 | |
| 8 | const std = @import("std.zig"); |
| 9 | const Io = std.Io; |
| 10 | const math = std.math; |
| 11 | const assert = std.debug.assert; |
| 12 | const posix = std.posix; |
| 13 | const windows = std.os.windows; |
| 14 | const testing = std.testing; |
| 15 | |
| 16 | pub const use_pthreads = native_os != .windows and native_os != .wasi and builtin.link_libc; |
| 17 | |
| 18 | const Impl = if (native_os == .windows) |
| 19 | WindowsThreadImpl |
| 20 | else if (use_pthreads) |
| 21 | PosixThreadImpl |
| 22 | else if (native_os == .linux) |
| 23 | LinuxThreadImpl |
| 24 | else if (native_os == .wasi) |
| 25 | WasiThreadImpl |
| 26 | else |
| 27 | UnsupportedImpl; |
| 28 | |
| 29 | impl: Impl, |
| 30 | |
| 31 | pub const max_name_len = switch (native_os) { |
| 32 | .linux => 15, |
| 33 | .windows => 31, |
| 34 | .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => 63, |
| 35 | .netbsd => 31, |
| 36 | .freebsd => 15, |
| 37 | .openbsd => 23, |
| 38 | .dragonfly => 1023, |
| 39 | .illumos => 31, |
| 40 | // https://github.com/SerenityOS/serenity/blob/6b4c300353da49d3508b5442cf61da70bd04d757/Kernel/Tasks/Thread.h#L102 |
| 41 | .serenity => 63, |
| 42 | else => 0, |
| 43 | }; |
| 44 | |
| 45 | pub const SetNameError = error{ |
| 46 | NameTooLong, |
| 47 | Unsupported, |
| 48 | Unexpected, |
| 49 | InvalidWtf8, |
| 50 | } || posix.PrctlError || Io.File.Writer.Error || Io.File.OpenError || std.mem.PrintError; |
| 51 | |
| 52 | pub fn setName(self: Thread, io: Io, name: []const u8) SetNameError!void { |
| 53 | if (name.len > max_name_len) return error.NameTooLong; |
| 54 | |
| 55 | const name_with_terminator = blk: { |
| 56 | var name_buf: [max_name_len:0]u8 = undefined; |
| 57 | @memcpy(name_buf[0..name.len], name); |
| 58 | name_buf[name.len] = 0; |
| 59 | break :blk name_buf[0..name.len :0]; |
| 60 | }; |
| 61 | |
| 62 | switch (native_os) { |
| 63 | .linux => if (use_pthreads) { |
| 64 | if (self.getHandle() == std.c.pthread_self()) { |
| 65 | // Set the name of the calling thread (no thread id required). |
| 66 | assert(try posix.prctl(.SET_NAME, .{@intFromPtr(name_with_terminator.ptr)}) == 0); |
| 67 | return; |
| 68 | } else { |
| 69 | const err = std.c.pthread_setname_np(self.getHandle(), name_with_terminator.ptr); |
| 70 | switch (@as(posix.E, @fromBackingInt(@intCast(err)))) { |
| 71 | .SUCCESS => return, |
| 72 | .RANGE => unreachable, |
| 73 | else => |e| return posix.unexpectedErrno(e), |
| 74 | } |
| 75 | } |
| 76 | } else { |
| 77 | var buf: [32]u8 = undefined; |
| 78 | const path = try std.mem.print(&buf, "/proc/self/task/{d}/comm", .{self.getHandle()}); |
| 79 | |
| 80 | const file = try Io.Dir.cwd().openFile(io, path, .{ .mode = .write_only }); |
| 81 | defer file.close(io); |
| 82 | |
| 83 | try file.writeStreamingAll(io, name); |
| 84 | return; |
| 85 | }, |
| 86 | .windows => { |
| 87 | var buf: [max_name_len]u16 = undefined; |
| 88 | switch (windows.ntdll.NtSetInformationThread( |
| 89 | self.getHandle(), |
| 90 | .NameInformation, |
| 91 | &windows.UNICODE_STRING.init(buf[0..try std.unicode.wtf8ToWtf16Le(&buf, name)]), |
| 92 | @sizeOf(windows.UNICODE_STRING), |
| 93 | )) { |
| 94 | .SUCCESS => return, |
| 95 | .NOT_IMPLEMENTED => return error.Unsupported, |
| 96 | else => |err| return windows.unexpectedStatus(err), |
| 97 | } |
| 98 | }, |
| 99 | .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => if (use_pthreads) { |
| 100 | // There doesn't seem to be a way to set the name for an arbitrary thread, only the current one. |
| 101 | if (self.getHandle() != std.c.pthread_self()) return error.Unsupported; |
| 102 | |
| 103 | const err = std.c.pthread_setname_np(name_with_terminator.ptr); |
| 104 | switch (@as(posix.E, @fromBackingInt(@intCast(err)))) { |
| 105 | .SUCCESS => return, |
| 106 | else => |e| return posix.unexpectedErrno(e), |
| 107 | } |
| 108 | }, |
| 109 | .serenity => if (use_pthreads) { |
| 110 | const err = std.c.pthread_setname_np(self.getHandle(), name_with_terminator.ptr); |
| 111 | switch (@as(posix.E, @fromBackingInt(@intCast(err)))) { |
| 112 | .SUCCESS => return, |
| 113 | .NAMETOOLONG => unreachable, |
| 114 | .SRCH => unreachable, |
| 115 | else => |e| return posix.unexpectedErrno(e), |
| 116 | } |
| 117 | }, |
| 118 | .netbsd, .illumos => if (use_pthreads) { |
| 119 | const err = std.c.pthread_setname_np(self.getHandle(), name_with_terminator.ptr, null); |
| 120 | switch (@as(posix.E, @fromBackingInt(@intCast(err)))) { |
| 121 | .SUCCESS => return, |
| 122 | .INVAL => unreachable, |
| 123 | .SRCH => unreachable, |
| 124 | .NOMEM => unreachable, |
| 125 | else => |e| return posix.unexpectedErrno(e), |
| 126 | } |
| 127 | }, |
| 128 | .freebsd, .openbsd => if (use_pthreads) { |
| 129 | // Use pthread_set_name_np for FreeBSD because pthread_setname_np is FreeBSD 12.2+ only. |
| 130 | // TODO maybe revisit this if depending on FreeBSD 12.2+ is acceptable because |
| 131 | // pthread_setname_np can return an error. |
| 132 | |
| 133 | std.c.pthread_set_name_np(self.getHandle(), name_with_terminator.ptr); |
| 134 | return; |
| 135 | }, |
| 136 | .dragonfly => if (use_pthreads) { |
| 137 | const err = std.c.pthread_setname_np(self.getHandle(), name_with_terminator.ptr); |
| 138 | switch (@as(posix.E, @fromBackingInt(@intCast(err)))) { |
| 139 | .SUCCESS => return, |
| 140 | .INVAL => unreachable, |
| 141 | .FAULT => unreachable, |
| 142 | .NAMETOOLONG => unreachable, // already checked |
| 143 | .SRCH => unreachable, |
| 144 | else => |e| return posix.unexpectedErrno(e), |
| 145 | } |
| 146 | }, |
| 147 | else => {}, |
| 148 | } |
| 149 | return error.Unsupported; |
| 150 | } |
| 151 | |
| 152 | pub const GetNameError = error{ |
| 153 | Unsupported, |
| 154 | Unexpected, |
| 155 | } || posix.PrctlError || posix.ReadError || Io.File.OpenError || std.mem.PrintError; |
| 156 | |
| 157 | /// On Windows, the result is encoded as [WTF-8](https://wtf-8.codeberg.page/). |
| 158 | /// On other platforms, the result is an opaque sequence of bytes with no particular encoding. |
| 159 | pub fn getName(self: Thread, buffer_ptr: *[max_name_len:0]u8) GetNameError!?[]const u8 { |
| 160 | buffer_ptr[max_name_len] = 0; |
| 161 | var buffer: [:0]u8 = buffer_ptr; |
| 162 | |
| 163 | switch (native_os) { |
| 164 | .linux => if (use_pthreads) { |
| 165 | if (self.getHandle() == std.c.pthread_self()) { |
| 166 | // Get the name of the calling thread (no thread id required). |
| 167 | assert(try posix.prctl(.GET_NAME, .{@intFromPtr(buffer.ptr)}) == 0); |
| 168 | return std.mem.sliceTo(buffer, 0); |
| 169 | } else { |
| 170 | const err = std.c.pthread_getname_np(self.getHandle(), buffer.ptr, max_name_len + 1); |
| 171 | switch (@as(posix.E, @fromBackingInt(@intCast(err)))) { |
| 172 | .SUCCESS => return std.mem.sliceTo(buffer, 0), |
| 173 | .RANGE => unreachable, |
| 174 | else => |e| return posix.unexpectedErrno(e), |
| 175 | } |
| 176 | } |
| 177 | } else { |
| 178 | var buf: [32]u8 = undefined; |
| 179 | const path = try std.mem.print(&buf, "/proc/self/task/{d}/comm", .{self.getHandle()}); |
| 180 | |
| 181 | const io = std.Options.debug_io; |
| 182 | |
| 183 | const file = try Io.Dir.cwd().openFile(io, path, .{}); |
| 184 | defer file.close(io); |
| 185 | |
| 186 | var file_reader = file.readerStreaming(io, &.{}); |
| 187 | const data_len = file_reader.interface.readSliceShort(buffer_ptr[0 .. max_name_len + 1]) catch |err| switch (err) { |
| 188 | error.ReadFailed => return file_reader.err.?, |
| 189 | }; |
| 190 | return if (data_len >= 1) buffer[0 .. data_len - 1] else null; |
| 191 | }, |
| 192 | .windows => { |
| 193 | const buf_capacity = @sizeOf(windows.UNICODE_STRING) + (@sizeOf(u16) * max_name_len); |
| 194 | var buf: [buf_capacity]u8 align(@alignOf(windows.UNICODE_STRING)) = undefined; |
| 195 | |
| 196 | switch (windows.ntdll.NtQueryInformationThread( |
| 197 | self.getHandle(), |
| 198 | .NameInformation, |
| 199 | &buf, |
| 200 | buf_capacity, |
| 201 | null, |
| 202 | )) { |
| 203 | .SUCCESS => { |
| 204 | const string: *const windows.UNICODE_STRING = @ptrCast(&buf); |
| 205 | const len = std.unicode.wtf16LeToWtf8(buffer, string.slice()); |
| 206 | return if (len > 0) buffer[0..len] else null; |
| 207 | }, |
| 208 | .NOT_IMPLEMENTED => return error.Unsupported, |
| 209 | else => |err| return windows.unexpectedStatus(err), |
| 210 | } |
| 211 | }, |
| 212 | .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => if (use_pthreads) { |
| 213 | const err = std.c.pthread_getname_np(self.getHandle(), buffer.ptr, max_name_len + 1); |
| 214 | switch (@as(posix.E, @fromBackingInt(@intCast(err)))) { |
| 215 | .SUCCESS => return std.mem.sliceTo(buffer, 0), |
| 216 | .SRCH => unreachable, |
| 217 | else => |e| return posix.unexpectedErrno(e), |
| 218 | } |
| 219 | }, |
| 220 | .serenity => if (use_pthreads) { |
| 221 | const err = std.c.pthread_getname_np(self.getHandle(), buffer.ptr, max_name_len + 1); |
| 222 | switch (@as(posix.E, @fromBackingInt(@intCast(err)))) { |
| 223 | .SUCCESS => return, |
| 224 | .NAMETOOLONG => unreachable, |
| 225 | .SRCH => unreachable, |
| 226 | .FAULT => unreachable, |
| 227 | else => |e| return posix.unexpectedErrno(e), |
| 228 | } |
| 229 | }, |
| 230 | .netbsd, .illumos => if (use_pthreads) { |
| 231 | const err = std.c.pthread_getname_np(self.getHandle(), buffer.ptr, max_name_len + 1); |
| 232 | switch (@as(posix.E, @fromBackingInt(@intCast(err)))) { |
| 233 | .SUCCESS => return std.mem.sliceTo(buffer, 0), |
| 234 | .INVAL => unreachable, |
| 235 | .SRCH => unreachable, |
| 236 | else => |e| return posix.unexpectedErrno(e), |
| 237 | } |
| 238 | }, |
| 239 | .freebsd, .openbsd => if (use_pthreads) { |
| 240 | // Use pthread_get_name_np for FreeBSD because pthread_getname_np is FreeBSD 12.2+ only. |
| 241 | // TODO maybe revisit this if depending on FreeBSD 12.2+ is acceptable because pthread_getname_np can return an error. |
| 242 | |
| 243 | std.c.pthread_get_name_np(self.getHandle(), buffer.ptr, max_name_len + 1); |
| 244 | return std.mem.sliceTo(buffer, 0); |
| 245 | }, |
| 246 | .dragonfly => if (use_pthreads) { |
| 247 | const err = std.c.pthread_getname_np(self.getHandle(), buffer.ptr, max_name_len + 1); |
| 248 | switch (@as(posix.E, @fromBackingInt(@intCast(err)))) { |
| 249 | .SUCCESS => return std.mem.sliceTo(buffer, 0), |
| 250 | .INVAL => unreachable, |
| 251 | .FAULT => unreachable, |
| 252 | .SRCH => unreachable, |
| 253 | else => |e| return posix.unexpectedErrno(e), |
| 254 | } |
| 255 | }, |
| 256 | else => {}, |
| 257 | } |
| 258 | return error.Unsupported; |
| 259 | } |
| 260 | |
| 261 | /// Represents an ID per thread guaranteed to be unique only within a process. |
| 262 | pub const Id = switch (native_os) { |
| 263 | .linux, |
| 264 | .dragonfly, |
| 265 | .netbsd, |
| 266 | .freebsd, |
| 267 | .openbsd, |
| 268 | .haiku, |
| 269 | .wasi, |
| 270 | .serenity, |
| 271 | => u32, |
| 272 | .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => u64, |
| 273 | .windows => windows.DWORD, |
| 274 | else => usize, |
| 275 | }; |
| 276 | |
| 277 | /// Returns the platform ID of the callers thread. |
| 278 | /// Attempts to use thread locals and avoid syscalls when possible. |
| 279 | pub fn getCurrentId() Id { |
| 280 | return Impl.getCurrentId(); |
| 281 | } |
| 282 | |
| 283 | pub const CpuCountError = error{ |
| 284 | PermissionDenied, |
| 285 | SystemResources, |
| 286 | Unsupported, |
| 287 | Unexpected, |
| 288 | }; |
| 289 | |
| 290 | /// Returns the platforms view on the number of logical CPU cores available. |
| 291 | /// |
| 292 | /// Returned value guaranteed to be >= 1. |
| 293 | pub fn getCpuCount() CpuCountError!usize { |
| 294 | return try Impl.getCpuCount(); |
| 295 | } |
| 296 | |
| 297 | /// Configuration options for hints on how to spawn threads. |
| 298 | pub const SpawnConfig = struct { |
| 299 | // TODO compile-time call graph analysis to determine stack upper bound |
| 300 | // https://github.com/ziglang/zig/issues/157 |
| 301 | |
| 302 | /// Size in bytes of the Thread's stack |
| 303 | stack_size: usize = default_stack_size, |
| 304 | /// The allocator to be used to allocate memory for the to-be-spawned thread |
| 305 | allocator: ?std.mem.Allocator = null, |
| 306 | |
| 307 | pub const default_stack_size = 16 * 1024 * 1024; |
| 308 | }; |
| 309 | |
| 310 | pub const SpawnError = error{ |
| 311 | /// A system-imposed limit on the number of threads was encountered. |
| 312 | /// There are a number of limits that may trigger this error: |
| 313 | /// * the RLIMIT_NPROC soft resource limit (set via setrlimit(2)), |
| 314 | /// which limits the number of processes and threads for a real |
| 315 | /// user ID, was reached; |
| 316 | /// * the kernel's system-wide limit on the number of processes and |
| 317 | /// threads, /proc/sys/kernel/threads-max, was reached (see |
| 318 | /// proc(5)); |
| 319 | /// * the maximum number of PIDs, /proc/sys/kernel/pid_max, was |
| 320 | /// reached (see proc(5)); or |
| 321 | /// * the PID limit (pids.max) imposed by the cgroup "process num‐ |
| 322 | /// ber" (PIDs) controller was reached. |
| 323 | ThreadQuotaExceeded, |
| 324 | |
| 325 | /// The kernel cannot allocate sufficient memory to allocate a task structure |
| 326 | /// for the child, or to copy those parts of the caller's context that need to |
| 327 | /// be copied. |
| 328 | SystemResources, |
| 329 | |
| 330 | /// Not enough userland memory to spawn the thread. |
| 331 | OutOfMemory, |
| 332 | |
| 333 | /// `mlockall` is enabled, and the memory needed to spawn the thread |
| 334 | /// would exceed the limit. |
| 335 | LockedMemoryLimitExceeded, |
| 336 | |
| 337 | Unexpected, |
| 338 | }; |
| 339 | |
| 340 | /// Spawns a new thread which executes `function` using `args` and returns a handle to the spawned thread. |
| 341 | /// `config` can be used as hints to the platform for how to spawn and execute the `function`. |
| 342 | /// The caller must eventually either call `join()` to wait for the thread to finish and free its resources |
| 343 | /// or call `detach()` to excuse the caller from calling `join()` and have the thread clean up its resources on completion. |
| 344 | pub fn spawn(config: SpawnConfig, comptime function: anytype, args: anytype) SpawnError!Thread { |
| 345 | if (builtin.single_threaded) { |
| 346 | @compileError("Cannot spawn thread when building in single-threaded mode"); |
| 347 | } |
| 348 | |
| 349 | const impl = try Impl.spawn(config, function, args); |
| 350 | return Thread{ .impl = impl }; |
| 351 | } |
| 352 | |
| 353 | /// Represents a kernel thread handle. |
| 354 | /// May be an integer or a pointer depending on the platform. |
| 355 | pub const Handle = Impl.ThreadHandle; |
| 356 | |
| 357 | /// Returns the handle of this thread |
| 358 | pub fn getHandle(self: Thread) Handle { |
| 359 | return self.impl.getHandle(); |
| 360 | } |
| 361 | |
| 362 | /// Release the obligation of the caller to call `join()` and have the thread clean up its own resources on completion. |
| 363 | /// Once called, this consumes the Thread object and invoking any other functions on it is considered undefined behavior. |
| 364 | pub fn detach(self: Thread) void { |
| 365 | return self.impl.detach(); |
| 366 | } |
| 367 | |
| 368 | /// Waits for the thread to complete, then deallocates any resources created on `spawn()`. |
| 369 | /// Once called, this consumes the Thread object and invoking any other functions on it is considered undefined behavior. |
| 370 | pub fn join(self: Thread) void { |
| 371 | return self.impl.join(); |
| 372 | } |
| 373 | |
| 374 | pub const YieldError = error{ |
| 375 | /// The system is not configured to allow yielding |
| 376 | SystemCannotYield, |
| 377 | }; |
| 378 | |
| 379 | /// Yields the current thread potentially allowing other threads to run. |
| 380 | pub fn yield() YieldError!void { |
| 381 | if (native_os == .windows) switch (windows.ntdll.NtYieldExecution()) { |
| 382 | .SUCCESS, .NO_YIELD_PERFORMED => return, |
| 383 | else => return error.SystemCannotYield, |
| 384 | }; |
| 385 | switch (posix.errno(posix.system.sched_yield())) { |
| 386 | .SUCCESS => return, |
| 387 | .NOSYS => return error.SystemCannotYield, |
| 388 | else => return error.SystemCannotYield, |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | /// State to synchronize detachment of spawner thread to spawned thread |
| 393 | const Completion = std.atomic.Value(enum(if (builtin.zig_backend == .stage2_riscv64) u32 else u8) { |
| 394 | running, |
| 395 | detached, |
| 396 | completed, |
| 397 | }); |
| 398 | |
| 399 | /// Performs implementation-agnostic thread setup (`maybeAttachSignalStack`), then calls the given |
| 400 | /// thread entry point `f` with `args` and handles the result. |
| 401 | fn callFn(comptime f: anytype, args: anytype) switch (Impl) { |
| 402 | WindowsThreadImpl => windows.NTSTATUS, |
| 403 | LinuxThreadImpl => u8, |
| 404 | PosixThreadImpl => ?*anyopaque, |
| 405 | else => unreachable, |
| 406 | } { |
| 407 | maybeAttachSignalStack(); |
| 408 | |
| 409 | const default_value = switch (Impl) { |
| 410 | WindowsThreadImpl => .SUCCESS, |
| 411 | LinuxThreadImpl => 0, |
| 412 | PosixThreadImpl => null, |
| 413 | else => unreachable, |
| 414 | }; |
| 415 | const bad_fn_ret = "expected return type of startFn to be 'u8', 'noreturn', '!noreturn', 'void', or '!void'"; |
| 416 | |
| 417 | switch (@typeInfo(@typeInfo(@TypeOf(f)).@"fn".return_type.?)) { |
| 418 | .noreturn => { |
| 419 | @call(.auto, f, args); |
| 420 | }, |
| 421 | .void => { |
| 422 | @call(.auto, f, args); |
| 423 | return default_value; |
| 424 | }, |
| 425 | .int => |info| { |
| 426 | if (info.bits != 8) { |
| 427 | @compileError(bad_fn_ret); |
| 428 | } |
| 429 | |
| 430 | const status = @call(.auto, f, args); |
| 431 | switch (Impl) { |
| 432 | WindowsThreadImpl => return @fromBackingInt(@intCast(status)), |
| 433 | LinuxThreadImpl => return status, |
| 434 | // pthreads don't support exit status, ignore value |
| 435 | PosixThreadImpl => return default_value, |
| 436 | else => unreachable, |
| 437 | } |
| 438 | }, |
| 439 | .error_union => |info| { |
| 440 | switch (info.payload) { |
| 441 | void, noreturn => { |
| 442 | @call(.auto, f, args) catch |err| { |
| 443 | std.debug.print("error: {s}\n", .{@errorName(err)}); |
| 444 | if (@errorReturnTrace()) |trace| { |
| 445 | std.debug.dumpErrorReturnTrace(trace); |
| 446 | } |
| 447 | }; |
| 448 | |
| 449 | return default_value; |
| 450 | }, |
| 451 | else => { |
| 452 | @compileError(bad_fn_ret); |
| 453 | }, |
| 454 | } |
| 455 | }, |
| 456 | else => { |
| 457 | @compileError(bad_fn_ret); |
| 458 | }, |
| 459 | } |
| 460 | } |
| 461 | |
| 462 | /// We can't compile error in the `Impl` switch statement as its eagerly evaluated. |
| 463 | /// So instead, we compile-error on the methods themselves for platforms which don't support threads. |
| 464 | const UnsupportedImpl = struct { |
| 465 | pub const ThreadHandle = void; |
| 466 | |
| 467 | fn getCurrentId() usize { |
| 468 | return unsupported({}); |
| 469 | } |
| 470 | |
| 471 | fn getCpuCount() !usize { |
| 472 | return unsupported({}); |
| 473 | } |
| 474 | |
| 475 | fn spawn(config: SpawnConfig, comptime f: anytype, args: anytype) !Impl { |
| 476 | return unsupported(.{ config, f, args }); |
| 477 | } |
| 478 | |
| 479 | fn getHandle(self: Impl) ThreadHandle { |
| 480 | return unsupported(self); |
| 481 | } |
| 482 | |
| 483 | fn detach(self: Impl) void { |
| 484 | return unsupported(self); |
| 485 | } |
| 486 | |
| 487 | fn join(self: Impl) void { |
| 488 | return unsupported(self); |
| 489 | } |
| 490 | |
| 491 | fn unsupported(unused: anytype) noreturn { |
| 492 | _ = unused; |
| 493 | @compileError("Unsupported operating system " ++ @tagName(native_os)); |
| 494 | } |
| 495 | }; |
| 496 | |
| 497 | const WindowsThreadImpl = struct { |
| 498 | pub const ThreadHandle = windows.HANDLE; |
| 499 | |
| 500 | fn getCurrentId() windows.DWORD { |
| 501 | return windows.GetCurrentThreadId(); |
| 502 | } |
| 503 | |
| 504 | fn getCpuCount() !usize { |
| 505 | // Faster than calling into GetSystemInfo(), even if amortized. |
| 506 | return windows.peb().NumberOfProcessors; |
| 507 | } |
| 508 | |
| 509 | thread: *ThreadCompletion, |
| 510 | |
| 511 | const ThreadCompletion = struct { |
| 512 | completion: Completion, |
| 513 | heap_ptr: windows.PVOID, |
| 514 | heap_handle: *windows.HEAP, |
| 515 | thread_handle: windows.HANDLE = undefined, |
| 516 | |
| 517 | fn free(self: ThreadCompletion) void { |
| 518 | const status = windows.ntdll.RtlFreeHeap(self.heap_handle, .{}, self.heap_ptr); |
| 519 | assert(status != 0); |
| 520 | } |
| 521 | }; |
| 522 | |
| 523 | fn spawn(config: SpawnConfig, comptime f: anytype, args: anytype) !Impl { |
| 524 | const Args = @TypeOf(args); |
| 525 | const Instance = struct { |
| 526 | fn_args: Args, |
| 527 | thread: ThreadCompletion, |
| 528 | |
| 529 | fn entryFn(raw_ptr: windows.PVOID) callconv(.winapi) windows.NTSTATUS { |
| 530 | const self: *@This() = @ptrCast(@alignCast(raw_ptr)); |
| 531 | defer switch (self.thread.completion.swap(.completed, .seq_cst)) { |
| 532 | .running => {}, |
| 533 | .completed => unreachable, |
| 534 | .detached => self.thread.free(), |
| 535 | }; |
| 536 | return callFn(f, self.fn_args); |
| 537 | } |
| 538 | }; |
| 539 | |
| 540 | const heap_handle = windows.GetProcessHeap() orelse return error.OutOfMemory; |
| 541 | const alloc_bytes = @alignOf(Instance) + @sizeOf(Instance); |
| 542 | const alloc_ptr = windows.ntdll.RtlAllocateHeap(heap_handle, .{}, alloc_bytes) orelse return error.OutOfMemory; |
| 543 | errdefer assert(windows.ntdll.RtlFreeHeap(heap_handle, .{}, alloc_ptr) != 0); |
| 544 | |
| 545 | const instance_bytes = @as([*]u8, @ptrCast(alloc_ptr))[0..alloc_bytes]; |
| 546 | var fba = std.heap.FixedBufferAllocator.init(instance_bytes); |
| 547 | const instance = fba.allocator().create(Instance) catch unreachable; |
| 548 | instance.* = .{ |
| 549 | .fn_args = args, |
| 550 | .thread = .{ |
| 551 | .completion = Completion.init(.running), |
| 552 | .heap_ptr = alloc_ptr, |
| 553 | .heap_handle = heap_handle, |
| 554 | }, |
| 555 | }; |
| 556 | |
| 557 | // Windows appears to only support SYSTEM.BASIC_INFORMATION.AllocationGranularity |
| 558 | // minimum stack size. Going lower makes it default to that specified in the executable |
| 559 | // (~1mb). Its also fine if the limit here is incorrect as stack size is only a hint. |
| 560 | const stack_size = @max(64 * 1024, std.math.lossyCast(u32, config.stack_size)); |
| 561 | |
| 562 | // Intended to be equivalent to a kernel32.CreateThread call with no flags set. |
| 563 | // However, CreateThread is just a wrapper around CreateRemoteThreadEx, |
| 564 | // so that's the more relevant function in this context. |
| 565 | // |
| 566 | // https://github.com/wine-mirror/wine/blob/3d128be6400b3869119d293d0c8fa9e7702978f8/dlls/kernelbase/thread.c#L85 |
| 567 | instance.thread.thread_handle = blk: { |
| 568 | var active_ctx: ?windows.HANDLE = undefined; |
| 569 | // Note: Can return null on SUCCESS |
| 570 | switch (windows.ntdll.RtlGetActiveActivationContext(&active_ctx)) { |
| 571 | .SUCCESS => {}, |
| 572 | else => |status| return windows.unexpectedStatus(status), |
| 573 | } |
| 574 | defer if (active_ctx) |ctx| windows.ntdll.RtlReleaseActivationContext(ctx); |
| 575 | |
| 576 | var teb: *windows.TEB = undefined; |
| 577 | var attr_list = windows.PS.ATTRIBUTE.LIST{ |
| 578 | .TotalLength = @sizeOf(windows.PS.ATTRIBUTE.LIST), |
| 579 | .Attributes = .{ |
| 580 | .{ |
| 581 | .Attribute = .TEB_ADDRESS, |
| 582 | .Size = @sizeOf(*windows.TEB), |
| 583 | .u = .{ |
| 584 | .ValuePtr = @ptrCast(&teb), |
| 585 | }, |
| 586 | .ReturnLength = null, |
| 587 | }, |
| 588 | }, |
| 589 | }; |
| 590 | |
| 591 | var thread_handle: windows.HANDLE = undefined; |
| 592 | switch (windows.ntdll.NtCreateThreadEx( |
| 593 | &thread_handle, |
| 594 | .{ .MAXIMUM_ALLOWED = true }, |
| 595 | &.{}, |
| 596 | windows.GetCurrentProcess(), |
| 597 | Instance.entryFn, |
| 598 | instance, |
| 599 | .{ .CREATE_SUSPENDED = true }, |
| 600 | 0, |
| 601 | @fromBackingInt(@intCast(stack_size)), |
| 602 | .default, |
| 603 | &attr_list, |
| 604 | )) { |
| 605 | .SUCCESS => {}, |
| 606 | else => |status| return windows.unexpectedStatus(status), |
| 607 | } |
| 608 | |
| 609 | if (active_ctx) |ctx| { |
| 610 | var cookie: windows.ULONG = 0; |
| 611 | switch (windows.ntdll.RtlActivateActivationContextEx(0, teb, ctx, &cookie)) { |
| 612 | .SUCCESS => {}, |
| 613 | else => |status| return windows.unexpectedStatus(status), |
| 614 | } |
| 615 | } |
| 616 | |
| 617 | switch (windows.ntdll.NtResumeThread(thread_handle, null)) { |
| 618 | .SUCCESS => {}, |
| 619 | else => |status| return windows.unexpectedStatus(status), |
| 620 | } |
| 621 | |
| 622 | break :blk thread_handle; |
| 623 | }; |
| 624 | |
| 625 | return Impl{ .thread = &instance.thread }; |
| 626 | } |
| 627 | |
| 628 | fn getHandle(self: Impl) ThreadHandle { |
| 629 | return self.thread.thread_handle; |
| 630 | } |
| 631 | |
| 632 | fn detach(self: Impl) void { |
| 633 | windows.CloseHandle(self.thread.thread_handle); |
| 634 | switch (self.thread.completion.swap(.detached, .seq_cst)) { |
| 635 | .running => {}, |
| 636 | .completed => self.thread.free(), |
| 637 | .detached => unreachable, |
| 638 | } |
| 639 | } |
| 640 | |
| 641 | fn join(self: Impl) void { |
| 642 | switch (windows.ntdll.NtWaitForSingleObject(self.thread.thread_handle, .FALSE, null)) { |
| 643 | windows.NTSTATUS.WAIT_0 => {}, |
| 644 | else => |status| windows.unexpectedStatus(status) catch unreachable, |
| 645 | } |
| 646 | windows.CloseHandle(self.thread.thread_handle); |
| 647 | assert(self.thread.completion.load(.seq_cst) == .completed); |
| 648 | self.thread.free(); |
| 649 | } |
| 650 | }; |
| 651 | |
| 652 | const PosixThreadImpl = struct { |
| 653 | const c = std.c; |
| 654 | |
| 655 | pub const ThreadHandle = c.pthread_t; |
| 656 | |
| 657 | fn getCurrentId() Id { |
| 658 | switch (native_os) { |
| 659 | .linux => { |
| 660 | return LinuxThreadImpl.getCurrentId(); |
| 661 | }, |
| 662 | .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => { |
| 663 | var thread_id: u64 = undefined; |
| 664 | // Pass thread=null to get the current thread ID. |
| 665 | assert(c.pthread_threadid_np(null, &thread_id) == 0); |
| 666 | return thread_id; |
| 667 | }, |
| 668 | .dragonfly => { |
| 669 | return @as(u32, @bitCast(c.lwp_gettid())); |
| 670 | }, |
| 671 | .netbsd => { |
| 672 | return @as(u32, @bitCast(c._lwp_self())); |
| 673 | }, |
| 674 | .freebsd => { |
| 675 | return @as(u32, @bitCast(c.pthread_getthreadid_np())); |
| 676 | }, |
| 677 | .openbsd => { |
| 678 | return @as(u32, @bitCast(c.getthrid())); |
| 679 | }, |
| 680 | .haiku => { |
| 681 | return @as(u32, @bitCast(c.find_thread(null))); |
| 682 | }, |
| 683 | .serenity => { |
| 684 | return @as(u32, @bitCast(c.pthread_self())); |
| 685 | }, |
| 686 | else => { |
| 687 | return @intFromPtr(c.pthread_self()); |
| 688 | }, |
| 689 | } |
| 690 | } |
| 691 | |
| 692 | fn getCpuCount() !usize { |
| 693 | switch (native_os) { |
| 694 | .linux => { |
| 695 | return LinuxThreadImpl.getCpuCount(); |
| 696 | }, |
| 697 | .emscripten => { |
| 698 | return @as(usize, @intCast(std.os.emscripten.emscripten_num_logical_cores())); |
| 699 | }, |
| 700 | .openbsd => { |
| 701 | var count: c_int = undefined; |
| 702 | var count_size: usize = @sizeOf(c_int); |
| 703 | const mib = [_]c_int{ std.c.CTL.HW, std.c.HW.NCPUONLINE }; |
| 704 | posix.sysctl(&mib, &count, &count_size, null, 0) catch |err| switch (err) { |
| 705 | error.NameTooLong, error.UnknownName => unreachable, |
| 706 | else => |e| return e, |
| 707 | }; |
| 708 | return @as(usize, @intCast(count)); |
| 709 | }, |
| 710 | .illumos, .serenity => { |
| 711 | // The "proper" way to get the cpu count would be to query |
| 712 | // /dev/kstat via ioctls, and traverse a linked list for each |
| 713 | // cpu. (illumos) |
| 714 | const rc = c.sysconf(@backingInt(std.c._SC.NPROCESSORS_ONLN)); |
| 715 | return switch (posix.errno(rc)) { |
| 716 | .SUCCESS => @as(usize, @intCast(rc)), |
| 717 | else => |err| posix.unexpectedErrno(err), |
| 718 | }; |
| 719 | }, |
| 720 | .haiku => { |
| 721 | var system_info: std.c.system_info = undefined; |
| 722 | return switch (std.c.get_system_info(&system_info)) { |
| 723 | 0 => @as(usize, @intCast(system_info.cpu_count)), |
| 724 | else => error.Unexpected, |
| 725 | }; |
| 726 | }, |
| 727 | else => { |
| 728 | var count: c_int = undefined; |
| 729 | var count_len: usize = @sizeOf(c_int); |
| 730 | const name = comptime if (target.os.tag.isDarwin()) "hw.logicalcpu" else "hw.ncpu"; |
| 731 | switch (posix.errno(posix.system.sysctlbyname(name, &count, &count_len, null, 0))) { |
| 732 | .SUCCESS => return @intCast(count), |
| 733 | .FAULT => unreachable, |
| 734 | .PERM => return error.PermissionDenied, |
| 735 | .NOMEM => return error.SystemResources, |
| 736 | .NOENT => unreachable, |
| 737 | else => |err| return posix.unexpectedErrno(err), |
| 738 | } |
| 739 | }, |
| 740 | } |
| 741 | } |
| 742 | |
| 743 | handle: ThreadHandle, |
| 744 | |
| 745 | fn spawn(config: SpawnConfig, comptime f: anytype, args: anytype) !Impl { |
| 746 | const Args = @TypeOf(args); |
| 747 | const allocator = std.heap.c_allocator; |
| 748 | |
| 749 | const Instance = struct { |
| 750 | fn entryFn(raw_arg: ?*anyopaque) callconv(.c) ?*anyopaque { |
| 751 | const args_ptr: *Args = @ptrCast(@alignCast(raw_arg)); |
| 752 | defer allocator.destroy(args_ptr); |
| 753 | return callFn(f, args_ptr.*); |
| 754 | } |
| 755 | }; |
| 756 | |
| 757 | const args_ptr = try allocator.create(Args); |
| 758 | args_ptr.* = args; |
| 759 | errdefer allocator.destroy(args_ptr); |
| 760 | |
| 761 | var attr: c.pthread_attr_t = undefined; |
| 762 | if (c.pthread_attr_init(&attr) != .SUCCESS) return error.SystemResources; |
| 763 | defer assert(c.pthread_attr_destroy(&attr) == .SUCCESS); |
| 764 | |
| 765 | // Use the same set of parameters used by the libc-less impl. |
| 766 | const stack_size = @max(config.stack_size, 16 * 1024); |
| 767 | assert(c.pthread_attr_setstacksize(&attr, stack_size) == .SUCCESS); |
| 768 | assert(c.pthread_attr_setguardsize(&attr, std.heap.pageSize()) == .SUCCESS); |
| 769 | |
| 770 | var handle: c.pthread_t = undefined; |
| 771 | switch (c.pthread_create( |
| 772 | &handle, |
| 773 | &attr, |
| 774 | Instance.entryFn, |
| 775 | @ptrCast(args_ptr), |
| 776 | )) { |
| 777 | .SUCCESS => return Impl{ .handle = handle }, |
| 778 | .AGAIN => return error.SystemResources, |
| 779 | .PERM => unreachable, |
| 780 | .INVAL => unreachable, |
| 781 | else => |err| return posix.unexpectedErrno(err), |
| 782 | } |
| 783 | } |
| 784 | |
| 785 | fn getHandle(self: Impl) ThreadHandle { |
| 786 | return self.handle; |
| 787 | } |
| 788 | |
| 789 | fn detach(self: Impl) void { |
| 790 | switch (c.pthread_detach(self.handle)) { |
| 791 | .SUCCESS => {}, |
| 792 | .INVAL => unreachable, // thread handle is not joinable |
| 793 | .SRCH => unreachable, // thread handle is invalid |
| 794 | else => unreachable, |
| 795 | } |
| 796 | } |
| 797 | |
| 798 | fn join(self: Impl) void { |
| 799 | switch (c.pthread_join(self.handle, null)) { |
| 800 | .SUCCESS => {}, |
| 801 | .INVAL => unreachable, // thread handle is not joinable (or another thread is already joining in) |
| 802 | .SRCH => unreachable, // thread handle is invalid |
| 803 | .DEADLK => unreachable, // two threads tried to join each other |
| 804 | else => unreachable, |
| 805 | } |
| 806 | } |
| 807 | }; |
| 808 | |
| 809 | const WasiThreadImpl = struct { |
| 810 | thread: *WasiThread, |
| 811 | |
| 812 | pub const ThreadHandle = i32; |
| 813 | threadlocal var tls_thread_id: Id = 0; |
| 814 | |
| 815 | const WasiThread = struct { |
| 816 | /// Thread ID |
| 817 | tid: std.atomic.Value(i32) = std.atomic.Value(i32).init(0), |
| 818 | /// Contains all memory which was allocated to bootstrap this thread, including: |
| 819 | /// - Guard page |
| 820 | /// - Stack |
| 821 | /// - TLS segment |
| 822 | /// - `Instance` |
| 823 | /// All memory is freed upon call to `join` |
| 824 | memory: []u8, |
| 825 | /// The allocator used to allocate the thread's memory, |
| 826 | /// which is also used during `join` to ensure clean-up. |
| 827 | allocator: std.mem.Allocator, |
| 828 | /// The current state of the thread. |
| 829 | state: State = State.init(.running), |
| 830 | }; |
| 831 | |
| 832 | /// A meta-data structure used to bootstrap a thread |
| 833 | const Instance = struct { |
| 834 | thread: WasiThread, |
| 835 | /// Contains the offset to the new __tls_base. |
| 836 | /// The offset starting from the memory's base. |
| 837 | tls_offset: usize, |
| 838 | /// Contains the offset to the stack for the newly spawned thread. |
| 839 | /// The offset is calculated starting from the memory's base. |
| 840 | stack_offset: usize, |
| 841 | /// Contains the raw pointer value to the wrapper which holds all arguments |
| 842 | /// for the callback. |
| 843 | raw_ptr: usize, |
| 844 | /// Function pointer to a wrapping function which will call the user's |
| 845 | /// function upon thread spawn. The above mentioned pointer will be passed |
| 846 | /// to this function pointer as its argument. |
| 847 | call_back: *const fn (usize) void, |
| 848 | /// When a thread is in `detached` state, we must free all of its memory |
| 849 | /// upon thread completion. However, as this is done while still within |
| 850 | /// the thread, we must first jump back to the main thread's stack or else |
| 851 | /// we end up freeing the stack that we're currently using. |
| 852 | original_stack_pointer: [*]u8, |
| 853 | }; |
| 854 | |
| 855 | const State = std.atomic.Value(enum(u8) { running, completed, detached }); |
| 856 | |
| 857 | fn getCurrentId() Id { |
| 858 | return tls_thread_id; |
| 859 | } |
| 860 | |
| 861 | fn getCpuCount() error{Unsupported}!noreturn { |
| 862 | return error.Unsupported; |
| 863 | } |
| 864 | |
| 865 | fn getHandle(self: Impl) ThreadHandle { |
| 866 | return self.thread.tid.load(.seq_cst); |
| 867 | } |
| 868 | |
| 869 | fn detach(self: Impl) void { |
| 870 | switch (self.thread.state.swap(.detached, .seq_cst)) { |
| 871 | .running => {}, |
| 872 | .completed => self.join(), |
| 873 | .detached => unreachable, |
| 874 | } |
| 875 | } |
| 876 | |
| 877 | fn join(self: Impl) void { |
| 878 | defer { |
| 879 | // Create a copy of the allocator so we do not free the reference to the |
| 880 | // original allocator while freeing the memory. |
| 881 | var allocator = self.thread.allocator; |
| 882 | allocator.free(self.thread.memory); |
| 883 | } |
| 884 | |
| 885 | while (true) { |
| 886 | const tid = self.thread.tid.load(.seq_cst); |
| 887 | if (tid == 0) break; |
| 888 | |
| 889 | const result = asm ( |
| 890 | \\ local.get %[ptr] |
| 891 | \\ local.get %[expected] |
| 892 | \\ i64.const -1 # infinite |
| 893 | \\ memory.atomic.wait32 0 |
| 894 | \\ local.set %[ret] |
| 895 | : [ret] "=r" (-> u32), |
| 896 | : [ptr] "r" (&self.thread.tid.raw), |
| 897 | [expected] "r" (tid), |
| 898 | ); |
| 899 | switch (result) { |
| 900 | 0 => continue, // ok |
| 901 | 1 => continue, // expected =! loaded |
| 902 | 2 => unreachable, // timeout (infinite) |
| 903 | else => unreachable, |
| 904 | } |
| 905 | } |
| 906 | } |
| 907 | |
| 908 | fn spawn(config: std.Thread.SpawnConfig, comptime f: anytype, args: anytype) SpawnError!WasiThreadImpl { |
| 909 | if (config.allocator == null) { |
| 910 | @panic("an allocator is required to spawn a WASI thread"); |
| 911 | } |
| 912 | |
| 913 | // Wrapping struct required to hold the user-provided function arguments. |
| 914 | const Wrapper = struct { |
| 915 | args: @TypeOf(args), |
| 916 | fn entry(ptr: usize) void { |
| 917 | const w: *@This() = @ptrFromInt(ptr); |
| 918 | const bad_fn_ret = "expected return type of startFn to be 'u8', 'noreturn', 'void', or '!void'"; |
| 919 | switch (@typeInfo(@typeInfo(@TypeOf(f)).@"fn".return_type.?)) { |
| 920 | .noreturn, .void => { |
| 921 | @call(.auto, f, w.args); |
| 922 | }, |
| 923 | .int => |info| { |
| 924 | if (info.bits != 8) { |
| 925 | @compileError(bad_fn_ret); |
| 926 | } |
| 927 | _ = @call(.auto, f, w.args); // WASI threads don't support exit status, ignore value |
| 928 | }, |
| 929 | .error_union => |info| { |
| 930 | if (info.payload != void) { |
| 931 | @compileError(bad_fn_ret); |
| 932 | } |
| 933 | @call(.auto, f, w.args) catch |err| { |
| 934 | std.debug.print("error: {s}\n", .{@errorName(err)}); |
| 935 | if (@errorReturnTrace()) |trace| { |
| 936 | std.debug.dumpErrorReturnTrace(trace); |
| 937 | } |
| 938 | }; |
| 939 | }, |
| 940 | else => { |
| 941 | @compileError(bad_fn_ret); |
| 942 | }, |
| 943 | } |
| 944 | } |
| 945 | }; |
| 946 | |
| 947 | var stack_offset: usize = undefined; |
| 948 | var tls_offset: usize = undefined; |
| 949 | var wrapper_offset: usize = undefined; |
| 950 | var instance_offset: usize = undefined; |
| 951 | |
| 952 | // Calculate the bytes we have to allocate to store all thread information, including: |
| 953 | // - The actual stack for the thread |
| 954 | // - The TLS segment |
| 955 | // - `Instance` - containing information about how to call the user's function. |
| 956 | const map_bytes = blk: { |
| 957 | // start with atleast a single page, which is used as a guard to prevent |
| 958 | // other threads clobbering our new thread. |
| 959 | // Unfortunately, WebAssembly has no notion of read-only segments, so this |
| 960 | // is only a best effort. |
| 961 | var bytes: usize = std.wasm.page_size; |
| 962 | |
| 963 | bytes = std.mem.alignForward(usize, bytes, 16); // align stack to 16 bytes |
| 964 | stack_offset = bytes; |
| 965 | bytes += @max(std.wasm.page_size, config.stack_size); |
| 966 | |
| 967 | bytes = std.mem.alignForward(usize, bytes, __tls_align()); |
| 968 | tls_offset = bytes; |
| 969 | bytes += __tls_size(); |
| 970 | |
| 971 | bytes = std.mem.alignForward(usize, bytes, @alignOf(Wrapper)); |
| 972 | wrapper_offset = bytes; |
| 973 | bytes += @sizeOf(Wrapper); |
| 974 | |
| 975 | bytes = std.mem.alignForward(usize, bytes, @alignOf(Instance)); |
| 976 | instance_offset = bytes; |
| 977 | bytes += @sizeOf(Instance); |
| 978 | |
| 979 | bytes = std.mem.alignForward(usize, bytes, std.wasm.page_size); |
| 980 | break :blk bytes; |
| 981 | }; |
| 982 | |
| 983 | // Allocate the amount of memory required for all meta data. |
| 984 | const allocated_memory = try config.allocator.?.alloc(u8, map_bytes); |
| 985 | |
| 986 | const wrapper: *Wrapper = @ptrCast(@alignCast(&allocated_memory[wrapper_offset])); |
| 987 | wrapper.* = .{ .args = args }; |
| 988 | |
| 989 | const instance: *Instance = @ptrCast(@alignCast(&allocated_memory[instance_offset])); |
| 990 | instance.* = .{ |
| 991 | .thread = .{ .memory = allocated_memory, .allocator = config.allocator.? }, |
| 992 | .tls_offset = tls_offset, |
| 993 | .stack_offset = stack_offset, |
| 994 | .raw_ptr = @intFromPtr(wrapper), |
| 995 | .call_back = &Wrapper.entry, |
| 996 | .original_stack_pointer = __get_stack_pointer(), |
| 997 | }; |
| 998 | |
| 999 | const tid = spawnWasiThread(instance); |
| 1000 | // The specification says any value lower than 0 indicates an error. |
| 1001 | // The values of such error are unspecified. WASI-Libc treats it as EAGAIN. |
| 1002 | if (tid < 0) { |
| 1003 | return error.SystemResources; |
| 1004 | } |
| 1005 | instance.thread.tid.store(tid, .seq_cst); |
| 1006 | |
| 1007 | return .{ .thread = &instance.thread }; |
| 1008 | } |
| 1009 | |
| 1010 | comptime { |
| 1011 | if (!builtin.single_threaded) { |
| 1012 | @export(&wasi_thread_start, .{ .name = "wasi_thread_start" }); |
| 1013 | } |
| 1014 | } |
| 1015 | |
| 1016 | /// Called by the host environment after thread creation. |
| 1017 | fn wasi_thread_start(tid: i32, arg: *Instance) callconv(.c) void { |
| 1018 | comptime assert(!builtin.single_threaded); |
| 1019 | __set_stack_pointer(arg.thread.memory.ptr + arg.stack_offset); |
| 1020 | __wasm_init_tls(arg.thread.memory.ptr + arg.tls_offset); |
| 1021 | @atomicStore(u32, &WasiThreadImpl.tls_thread_id, @intCast(tid), .seq_cst); |
| 1022 | |
| 1023 | // Finished bootstrapping, call user's procedure. |
| 1024 | arg.call_back(arg.raw_ptr); |
| 1025 | |
| 1026 | switch (arg.thread.state.swap(.completed, .seq_cst)) { |
| 1027 | .running => { |
| 1028 | // reset the Thread ID |
| 1029 | asm volatile ( |
| 1030 | \\ local.get %[ptr] |
| 1031 | \\ i32.const 0 |
| 1032 | \\ i32.atomic.store 0 |
| 1033 | : |
| 1034 | : [ptr] "r" (&arg.thread.tid.raw), |
| 1035 | ); |
| 1036 | |
| 1037 | // Wake the main thread listening to this thread |
| 1038 | asm volatile ( |
| 1039 | \\ local.get %[ptr] |
| 1040 | \\ i32.const 1 # waiters |
| 1041 | \\ memory.atomic.notify 0 |
| 1042 | \\ drop # no need to know the waiters |
| 1043 | : |
| 1044 | : [ptr] "r" (&arg.thread.tid.raw), |
| 1045 | ); |
| 1046 | }, |
| 1047 | .completed => unreachable, |
| 1048 | .detached => { |
| 1049 | // restore the original stack pointer so we can free the memory |
| 1050 | // without having to worry about freeing the stack |
| 1051 | __set_stack_pointer(arg.original_stack_pointer); |
| 1052 | // Ensure a copy so we don't free the allocator reference itself |
| 1053 | var allocator = arg.thread.allocator; |
| 1054 | allocator.free(arg.thread.memory); |
| 1055 | }, |
| 1056 | } |
| 1057 | } |
| 1058 | |
| 1059 | /// Asks the host to create a new thread for us. |
| 1060 | /// Newly created thread will call `wasi_tread_start` with the thread ID as well |
| 1061 | /// as the input `arg` that was provided to `spawnWasiThread` |
| 1062 | const spawnWasiThread = @"thread-spawn"; |
| 1063 | extern "wasi" fn @"thread-spawn"(arg: *Instance) i32; |
| 1064 | |
| 1065 | /// Initializes the TLS data segment starting at `memory`. |
| 1066 | /// This is a synthetic function, generated by the linker. |
| 1067 | extern fn __wasm_init_tls(memory: [*]u8) void; |
| 1068 | |
| 1069 | /// Returns a pointer to the base of the TLS data segment for the current thread |
| 1070 | inline fn __tls_base() [*]u8 { |
| 1071 | return asm ( |
| 1072 | \\ .globaltype __tls_base, i32 |
| 1073 | \\ global.get __tls_base |
| 1074 | \\ local.set %[ret] |
| 1075 | : [ret] "=r" (-> [*]u8), |
| 1076 | ); |
| 1077 | } |
| 1078 | |
| 1079 | /// Returns the size of the TLS segment |
| 1080 | inline fn __tls_size() u32 { |
| 1081 | return asm volatile ( |
| 1082 | \\ .globaltype __tls_size, i32, immutable |
| 1083 | \\ global.get __tls_size |
| 1084 | \\ local.set %[ret] |
| 1085 | : [ret] "=r" (-> u32), |
| 1086 | ); |
| 1087 | } |
| 1088 | |
| 1089 | /// Returns the alignment of the TLS segment |
| 1090 | inline fn __tls_align() u32 { |
| 1091 | return asm ( |
| 1092 | \\ .globaltype __tls_align, i32, immutable |
| 1093 | \\ global.get __tls_align |
| 1094 | \\ local.set %[ret] |
| 1095 | : [ret] "=r" (-> u32), |
| 1096 | ); |
| 1097 | } |
| 1098 | |
| 1099 | /// Allows for setting the stack pointer in the WebAssembly module. |
| 1100 | inline fn __set_stack_pointer(addr: [*]u8) void { |
| 1101 | asm volatile ( |
| 1102 | \\ local.get %[ptr] |
| 1103 | \\ global.set __stack_pointer |
| 1104 | : |
| 1105 | : [ptr] "r" (addr), |
| 1106 | ); |
| 1107 | } |
| 1108 | |
| 1109 | /// Returns the current value of the stack pointer |
| 1110 | inline fn __get_stack_pointer() [*]u8 { |
| 1111 | return asm ( |
| 1112 | \\ global.get __stack_pointer |
| 1113 | \\ local.set %[stack_ptr] |
| 1114 | : [stack_ptr] "=r" (-> [*]u8), |
| 1115 | ); |
| 1116 | } |
| 1117 | }; |
| 1118 | |
| 1119 | const LinuxThreadImpl = struct { |
| 1120 | const linux = std.os.linux; |
| 1121 | |
| 1122 | pub const ThreadHandle = i32; |
| 1123 | |
| 1124 | threadlocal var tls_thread_id: ?Id = null; |
| 1125 | |
| 1126 | fn getCurrentId() Id { |
| 1127 | return tls_thread_id orelse { |
| 1128 | const tid: u32 = @bitCast(linux.gettid()); |
| 1129 | tls_thread_id = tid; |
| 1130 | return tid; |
| 1131 | }; |
| 1132 | } |
| 1133 | |
| 1134 | fn getCpuCount() !usize { |
| 1135 | const cpu_set = try posix.sched_getaffinity(0); |
| 1136 | return posix.CPU_COUNT(cpu_set); |
| 1137 | } |
| 1138 | |
| 1139 | thread: *ThreadCompletion, |
| 1140 | |
| 1141 | const ThreadCompletion = struct { |
| 1142 | completion: Completion = Completion.init(.running), |
| 1143 | child_tid: std.atomic.Value(i32) = std.atomic.Value(i32).init(1), |
| 1144 | parent_tid: i32 = undefined, |
| 1145 | mapped: []align(std.heap.page_size_min) u8, |
| 1146 | |
| 1147 | // On SPARC, the kernel needs to be able to restore the current register window from the |
| 1148 | // stack when returning from a syscall. That presents a bit of a problem in `freeAndExit` |
| 1149 | // since we're deallocating the stack! The good news is that, since we do not care about |
| 1150 | // the contents of the incoming and local registers at that point, we can just tell the |
| 1151 | // kernel that our stack is this undefined global buffer. |
| 1152 | var sparc_exit_stack: [192]u8 align(16) = undefined; |
| 1153 | |
| 1154 | /// Calls `munmap(mapped.ptr, mapped.len)` then `exit(1)` without touching the stack (which lives in `mapped.ptr`). |
| 1155 | /// Ported over from musl libc's pthread detached implementation: |
| 1156 | /// https://github.com/ifduyue/musl/search?q=__unmapself |
| 1157 | fn freeAndExit(self: *ThreadCompletion) noreturn { |
| 1158 | // If we do not reset the child_tidptr to null here, the kernel would later write the |
| 1159 | // value zero to that address, which is inside the block we're unmapping below, after |
| 1160 | // our thread exits. This can sometimes corrupt memory in other mmap blocks from |
| 1161 | // unrelated concurrent threads. |
| 1162 | _ = linux.set_tid_address(null); |
| 1163 | // If a signal were delivered between SYS_munmap and SYS_exit, any installed signal |
| 1164 | // handler would immediately segfault due to the stack being unmapped. To avoid this, |
| 1165 | // we need to mask all signals before entering the inline asm. |
| 1166 | posix.sigprocmask(std.posix.SIG.BLOCK, &std.os.linux.sigfillset(), null); |
| 1167 | switch (target.cpu.arch) { |
| 1168 | .x86 => asm volatile ( |
| 1169 | \\ movl $91, %%eax # SYS_munmap |
| 1170 | \\ int $128 |
| 1171 | \\ movl $1, %%eax # SYS_exit |
| 1172 | \\ movl $0, %%ebx |
| 1173 | \\ int $128 |
| 1174 | : |
| 1175 | : [ptr] "{ebx}" (@intFromPtr(self.mapped.ptr)), |
| 1176 | [len] "{ecx}" (self.mapped.len), |
| 1177 | ), |
| 1178 | .x86_64 => asm volatile (switch (target.abi) { |
| 1179 | .gnux32, .muslx32, .x32 => |
| 1180 | \\ movl $0x4000000b, %%eax # SYS_munmap |
| 1181 | \\ syscall |
| 1182 | \\ movl $0x4000003c, %%eax # SYS_exit |
| 1183 | \\ xor %%rdi, %%rdi |
| 1184 | \\ syscall |
| 1185 | , |
| 1186 | else => |
| 1187 | \\ movl $11, %%eax # SYS_munmap |
| 1188 | \\ syscall |
| 1189 | \\ movl $60, %%eax # SYS_exit |
| 1190 | \\ xor %%rdi, %%rdi |
| 1191 | \\ syscall |
| 1192 | , |
| 1193 | } |
| 1194 | : |
| 1195 | : [ptr] "{rdi}" (@intFromPtr(self.mapped.ptr)), |
| 1196 | [len] "{rsi}" (self.mapped.len), |
| 1197 | ), |
| 1198 | .arm, .armeb, .thumb, .thumbeb => asm volatile ( |
| 1199 | \\ mov r7, #91 // SYS_munmap |
| 1200 | \\ svc 0 |
| 1201 | \\ mov r7, #1 // SYS_exit |
| 1202 | \\ mov r0, #0 |
| 1203 | \\ svc 0 |
| 1204 | : |
| 1205 | : [ptr] "{r0}" (@intFromPtr(self.mapped.ptr)), |
| 1206 | [len] "{r1}" (self.mapped.len), |
| 1207 | ), |
| 1208 | .aarch64, .aarch64_be => asm volatile ( |
| 1209 | \\ mov x8, #215 // SYS_munmap |
| 1210 | \\ svc 0 |
| 1211 | \\ mov x8, #93 // SYS_exit |
| 1212 | \\ mov x0, #0 |
| 1213 | \\ svc 0 |
| 1214 | : |
| 1215 | : [ptr] "{x0}" (@intFromPtr(self.mapped.ptr)), |
| 1216 | [len] "{x1}" (self.mapped.len), |
| 1217 | ), |
| 1218 | .alpha => asm volatile ( |
| 1219 | \\ ldi $0, 73 # SYS_munmap |
| 1220 | \\ callsys |
| 1221 | \\ ldi $0, 1 # SYS_exit |
| 1222 | \\ ldi $16, 0 |
| 1223 | \\ callsys |
| 1224 | : |
| 1225 | : [ptr] "{$16}" (@intFromPtr(self.mapped.ptr)), |
| 1226 | [len] "{$17}" (self.mapped.len), |
| 1227 | ), |
| 1228 | .arc, .arceb => asm volatile ( |
| 1229 | \\ mov r8, 215 # SYS_munmap |
| 1230 | \\ trap_s 0 |
| 1231 | \\ mov r8, 93 # SYS_exit |
| 1232 | \\ mov r0, 0 |
| 1233 | \\ trap_s 0 |
| 1234 | : |
| 1235 | : [ptr] "{r0}" (@intFromPtr(self.mapped.ptr)), |
| 1236 | [len] "{r1}" (self.mapped.len), |
| 1237 | ), |
| 1238 | .hexagon => asm volatile ( |
| 1239 | \\ r6 = #215 // SYS_munmap |
| 1240 | \\ trap0(#1) |
| 1241 | \\ r6 = #93 // SYS_exit |
| 1242 | \\ r0 = #0 |
| 1243 | \\ trap0(#1) |
| 1244 | : |
| 1245 | : [ptr] "{r0}" (@intFromPtr(self.mapped.ptr)), |
| 1246 | [len] "{r1}" (self.mapped.len), |
| 1247 | ), |
| 1248 | .hppa => asm volatile ( |
| 1249 | \\ ble 0x100(%%sr2, %%r0) |
| 1250 | \\ ldi 91, %%r20 /* SYS_munmap */ |
| 1251 | \\ ldi 0, %%r26 |
| 1252 | \\ ble 0x100(%%sr2, %%r0) |
| 1253 | \\ ldi 1, %%r20 /* SYS_exit */ |
| 1254 | : |
| 1255 | : [ptr] "{r26}" (@intFromPtr(self.mapped.ptr)), |
| 1256 | [len] "{r25}" (self.mapped.len), |
| 1257 | ), |
| 1258 | .m68k => asm volatile ( |
| 1259 | \\ move.l #91, %%d0 // SYS_munmap |
| 1260 | \\ trap #0 |
| 1261 | \\ move.l #1, %%d0 // SYS_exit |
| 1262 | \\ move.l #0, %%d1 |
| 1263 | \\ trap #0 |
| 1264 | : |
| 1265 | : [ptr] "{d1}" (@intFromPtr(self.mapped.ptr)), |
| 1266 | [len] "{d2}" (self.mapped.len), |
| 1267 | ), |
| 1268 | .microblaze, .microblazeel => asm volatile ( |
| 1269 | \\ ori r12, r0, 91 # SYS_munmap |
| 1270 | \\ brki r14, 0x8 |
| 1271 | \\ ori r12, r0, 1 # SYS_exit |
| 1272 | \\ ori r5, r0, 0 |
| 1273 | \\ brki r14, 0x8 |
| 1274 | : |
| 1275 | : [ptr] "{r5}" (@intFromPtr(self.mapped.ptr)), |
| 1276 | [len] "{r6}" (self.mapped.len), |
| 1277 | ), |
| 1278 | .mips, .mipsel => asm volatile ( |
| 1279 | \\ li $v0, 4091 # SYS_munmap |
| 1280 | \\ syscall |
| 1281 | \\ li $v0, 4001 # SYS_exit |
| 1282 | \\ li $a0, 0 |
| 1283 | \\ syscall |
| 1284 | : |
| 1285 | : [ptr] "{$4}" (@intFromPtr(self.mapped.ptr)), |
| 1286 | [len] "{$5}" (self.mapped.len), |
| 1287 | ), |
| 1288 | .mips64, .mips64el => asm volatile (switch (target.abi) { |
| 1289 | .gnuabin32, .muslabin32, .abin32 => |
| 1290 | \\ li $v0, 6011 # SYS_munmap |
| 1291 | \\ syscall |
| 1292 | \\ li $v0, 6058 # SYS_exit |
| 1293 | \\ li $a0, 0 |
| 1294 | \\ syscall |
| 1295 | , |
| 1296 | else => |
| 1297 | \\ li $v0, 5011 # SYS_munmap |
| 1298 | \\ syscall |
| 1299 | \\ li $v0, 5058 # SYS_exit |
| 1300 | \\ li $a0, 0 |
| 1301 | \\ syscall |
| 1302 | , |
| 1303 | } |
| 1304 | : |
| 1305 | : [ptr] "{$4}" (@intFromPtr(self.mapped.ptr)), |
| 1306 | [len] "{$5}" (self.mapped.len), |
| 1307 | ), |
| 1308 | .or1k => asm volatile ( |
| 1309 | \\ l.ori r11, r0, 215 # SYS_munmap |
| 1310 | \\ l.sys 1 |
| 1311 | \\ l.ori r11, r0, 93 # SYS_exit |
| 1312 | \\ l.ori r3, r0, 0 |
| 1313 | \\ l.sys 1 |
| 1314 | : |
| 1315 | : [ptr] "{r3}" (@intFromPtr(self.mapped.ptr)), |
| 1316 | [len] "{r4}" (self.mapped.len), |
| 1317 | ), |
| 1318 | .powerpc, .powerpcle, .powerpc64, .powerpc64le => asm volatile ( |
| 1319 | \\ li 0, 91 # SYS_munmap |
| 1320 | \\ sc |
| 1321 | \\ li 0, 1 # SYS_exit |
| 1322 | \\ li 3, 0 |
| 1323 | \\ sc |
| 1324 | : |
| 1325 | : [ptr] "{r3}" (@intFromPtr(self.mapped.ptr)), |
| 1326 | [len] "{r4}" (self.mapped.len), |
| 1327 | ), |
| 1328 | .riscv32, .riscv64 => asm volatile ( |
| 1329 | \\ li a7, 215 # SYS_munmap |
| 1330 | \\ ecall |
| 1331 | \\ li a7, 93 # SYS_exit |
| 1332 | \\ mv a0, zero |
| 1333 | \\ ecall |
| 1334 | : |
| 1335 | : [ptr] "{a0}" (@intFromPtr(self.mapped.ptr)), |
| 1336 | [len] "{a1}" (self.mapped.len), |
| 1337 | ), |
| 1338 | .s390x => asm volatile ( |
| 1339 | \\ svc 91 # SYS_munmap |
| 1340 | \\ lghi %%r2, 0 |
| 1341 | \\ svc 1 # SYS_exit |
| 1342 | : |
| 1343 | : [ptr] "{r2}" (@intFromPtr(self.mapped.ptr)), |
| 1344 | [len] "{r3}" (self.mapped.len), |
| 1345 | ), |
| 1346 | .sh, .sheb => asm volatile ( |
| 1347 | \\ mov #91, r3 ! SYS_munmap |
| 1348 | \\ trapa #31 |
| 1349 | \\ or r0, r0 |
| 1350 | \\ or r0, r0 |
| 1351 | \\ or r0, r0 |
| 1352 | \\ or r0, r0 |
| 1353 | \\ or r0, r0 |
| 1354 | \\ mov #1, r3 ! SYS_exit |
| 1355 | \\ mov #0, r4 |
| 1356 | \\ trapa #31 |
| 1357 | \\ or r0, r0 |
| 1358 | \\ or r0, r0 |
| 1359 | \\ or r0, r0 |
| 1360 | \\ or r0, r0 |
| 1361 | \\ or r0, r0 |
| 1362 | : |
| 1363 | : [ptr] "{r4}" (@intFromPtr(self.mapped.ptr)), |
| 1364 | [len] "{r5}" (self.mapped.len), |
| 1365 | ), |
| 1366 | .sparc => asm volatile ( |
| 1367 | \\ // See sparc64 comments below. |
| 1368 | \\ t 0x3 // ST_FLUSH_WINDOWS |
| 1369 | \\ mov %%g3, %%sp |
| 1370 | \\ mov %%g1, %%o0 |
| 1371 | \\ mov %%g2, %%o1 |
| 1372 | \\ mov 73, %%g1 // SYS_munmap |
| 1373 | \\ t 0x10 |
| 1374 | \\ mov 1, %%g1 // SYS_exit |
| 1375 | \\ mov 0, %%o0 |
| 1376 | \\ t 0x10 |
| 1377 | : |
| 1378 | : [ptr] "{g1}" (@intFromPtr(self.mapped.ptr)), |
| 1379 | [len] "{g2}" (self.mapped.len), |
| 1380 | [stack] "{g3}" (&sparc_exit_stack), |
| 1381 | ), |
| 1382 | .sparc64 => asm volatile ( |
| 1383 | \\ // Ensure that the kernel only has to flush the current register window. |
| 1384 | \\ flushw |
| 1385 | \\ // Set up a fake stack for the syscall to restore l/i registers from. Local |
| 1386 | \\ // and incoming registers must be treated as effectively garbage past this |
| 1387 | \\ // instruction! |
| 1388 | \\ sub %%g3, 2047, %%sp |
| 1389 | \\ mov %%g1, %%o0 |
| 1390 | \\ mov %%g2, %%o1 |
| 1391 | \\ mov 73, %%g1 // SYS_munmap |
| 1392 | \\ t 0x6d |
| 1393 | \\ mov 1, %%g1 // SYS_exit |
| 1394 | \\ mov 0, %%o0 |
| 1395 | \\ t 0x6d |
| 1396 | : |
| 1397 | : [ptr] "{g1}" (@intFromPtr(self.mapped.ptr)), |
| 1398 | [len] "{g2}" (self.mapped.len), |
| 1399 | [stack] "{g3}" (&sparc_exit_stack), |
| 1400 | ), |
| 1401 | .loongarch32, .loongarch64 => asm volatile ( |
| 1402 | \\ ori $a7, $zero, 215 # SYS_munmap |
| 1403 | \\ syscall 0 # call munmap |
| 1404 | \\ ori $a0, $zero, 0 |
| 1405 | \\ ori $a7, $zero, 93 # SYS_exit |
| 1406 | \\ syscall 0 # call exit |
| 1407 | : |
| 1408 | : [ptr] "{r4}" (@intFromPtr(self.mapped.ptr)), |
| 1409 | [len] "{r5}" (self.mapped.len), |
| 1410 | ), |
| 1411 | .csky => asm volatile ( |
| 1412 | \\ movi r7, 215 # SYS_munmap |
| 1413 | \\ trap 0 |
| 1414 | \\ movi r7, 93 # SYS_exit |
| 1415 | \\ movi r0, 0 |
| 1416 | \\ trap 0 |
| 1417 | : |
| 1418 | : [ptr] "{r0}" (@intFromPtr(self.mapped.ptr)), |
| 1419 | [len] "{r1}" (self.mapped.len), |
| 1420 | ), |
| 1421 | .xtensa, .xtensaeb => asm volatile ( |
| 1422 | \\ movi a2, 81 // SYS_munmap |
| 1423 | \\ syscall |
| 1424 | \\ movi a6, 0 |
| 1425 | \\ movi a2, 118 // SYS_exit |
| 1426 | \\ syscall |
| 1427 | : |
| 1428 | : [ptr] "{a6}" (@intFromPtr(self.mapped.ptr)), |
| 1429 | [len] "{a3}" (self.mapped.len), |
| 1430 | ), |
| 1431 | else => |cpu_arch| @compileError("Unsupported linux arch: " ++ @tagName(cpu_arch)), |
| 1432 | } |
| 1433 | unreachable; |
| 1434 | } |
| 1435 | }; |
| 1436 | |
| 1437 | fn spawn(config: SpawnConfig, comptime f: anytype, args: anytype) !Impl { |
| 1438 | const page_size = std.heap.pageSize(); |
| 1439 | const Args = @TypeOf(args); |
| 1440 | const Instance = struct { |
| 1441 | fn_args: Args, |
| 1442 | thread: ThreadCompletion, |
| 1443 | |
| 1444 | fn entryFn(raw_arg: usize) callconv(.c) u8 { |
| 1445 | const self = @as(*@This(), @ptrFromInt(raw_arg)); |
| 1446 | defer switch (self.thread.completion.swap(.completed, .seq_cst)) { |
| 1447 | .running => {}, |
| 1448 | .completed => unreachable, |
| 1449 | .detached => self.thread.freeAndExit(), |
| 1450 | }; |
| 1451 | return callFn(f, self.fn_args); |
| 1452 | } |
| 1453 | }; |
| 1454 | |
| 1455 | var guard_offset: usize = undefined; |
| 1456 | var stack_offset: usize = undefined; |
| 1457 | var tls_offset: usize = undefined; |
| 1458 | var instance_offset: usize = undefined; |
| 1459 | |
| 1460 | const map_bytes = blk: { |
| 1461 | var bytes: usize = page_size; |
| 1462 | guard_offset = bytes; |
| 1463 | |
| 1464 | bytes += @max(page_size, config.stack_size); |
| 1465 | bytes = std.mem.alignForward(usize, bytes, page_size); |
| 1466 | stack_offset = bytes; |
| 1467 | |
| 1468 | bytes = std.mem.alignForward(usize, bytes, linux.tls.area_desc.alignment); |
| 1469 | tls_offset = bytes; |
| 1470 | bytes += linux.tls.area_desc.size; |
| 1471 | |
| 1472 | bytes = std.mem.alignForward(usize, bytes, @alignOf(Instance)); |
| 1473 | instance_offset = bytes; |
| 1474 | bytes += @sizeOf(Instance); |
| 1475 | |
| 1476 | bytes = std.mem.alignForward(usize, bytes, page_size); |
| 1477 | break :blk bytes; |
| 1478 | }; |
| 1479 | |
| 1480 | // map all memory needed without read/write permissions |
| 1481 | // to avoid committing the whole region right away |
| 1482 | // anonymous mapping ensures file descriptor limits are not exceeded |
| 1483 | const mapped = posix.mmap( |
| 1484 | null, |
| 1485 | map_bytes, |
| 1486 | .{}, |
| 1487 | .{ .TYPE = .PRIVATE, .ANONYMOUS = true }, |
| 1488 | -1, |
| 1489 | 0, |
| 1490 | ) catch |err| switch (err) { |
| 1491 | error.MemoryMappingNotSupported => unreachable, |
| 1492 | error.AccessDenied => unreachable, |
| 1493 | error.PermissionDenied => unreachable, |
| 1494 | error.ProcessFdQuotaExceeded => unreachable, |
| 1495 | error.SystemFdQuotaExceeded => unreachable, |
| 1496 | error.MappingAlreadyExists => unreachable, |
| 1497 | else => |e| return e, |
| 1498 | }; |
| 1499 | assert(mapped.len >= map_bytes); |
| 1500 | errdefer posix.munmap(mapped); |
| 1501 | |
| 1502 | // Map everything but the guard page as read/write. |
| 1503 | const guarded: []align(std.heap.page_size_min) u8 = @alignCast(mapped[guard_offset..]); |
| 1504 | const protection: posix.PROT = .{ .READ = true, .WRITE = true }; |
| 1505 | switch (posix.errno(posix.system.mprotect(guarded.ptr, guarded.len, protection))) { |
| 1506 | .SUCCESS => {}, |
| 1507 | .NOMEM => return error.OutOfMemory, |
| 1508 | else => |err| return posix.unexpectedErrno(err), |
| 1509 | } |
| 1510 | |
| 1511 | // Prepare the TLS segment and prepare a user_desc struct when needed on x86 |
| 1512 | var tls_ptr = linux.tls.prepareArea(mapped[tls_offset..][0..linux.tls.area_desc.size]); |
| 1513 | var user_desc: if (target.cpu.arch == .x86) linux.user_desc else void = undefined; |
| 1514 | if (target.cpu.arch == .x86) { |
| 1515 | defer tls_ptr = @intFromPtr(&user_desc); |
| 1516 | user_desc = .{ |
| 1517 | .entry_number = linux.tls.area_desc.gdt_entry_number, |
| 1518 | .base_addr = tls_ptr, |
| 1519 | .limit = 0xfffff, |
| 1520 | .flags = .{ |
| 1521 | .seg_32bit = 1, |
| 1522 | .contents = 0, // Data |
| 1523 | .read_exec_only = 0, |
| 1524 | .limit_in_pages = 1, |
| 1525 | .seg_not_present = 0, |
| 1526 | .useable = 1, |
| 1527 | }, |
| 1528 | }; |
| 1529 | } |
| 1530 | |
| 1531 | const instance: *Instance = @ptrCast(@alignCast(&mapped[instance_offset])); |
| 1532 | instance.* = .{ |
| 1533 | .fn_args = args, |
| 1534 | .thread = .{ .mapped = mapped }, |
| 1535 | }; |
| 1536 | |
| 1537 | const flags: u32 = linux.CLONE.THREAD | linux.CLONE.DETACHED | |
| 1538 | linux.CLONE.VM | linux.CLONE.FS | linux.CLONE.FILES | |
| 1539 | linux.CLONE.PARENT_SETTID | linux.CLONE.CHILD_CLEARTID | |
| 1540 | linux.CLONE.SIGHAND | linux.CLONE.SYSVSEM | linux.CLONE.SETTLS; |
| 1541 | |
| 1542 | switch (linux.errno(linux.clone( |
| 1543 | Instance.entryFn, |
| 1544 | @intFromPtr(&mapped[stack_offset]), |
| 1545 | flags, |
| 1546 | @intFromPtr(instance), |
| 1547 | &instance.thread.parent_tid, |
| 1548 | tls_ptr, |
| 1549 | &instance.thread.child_tid.raw, |
| 1550 | ))) { |
| 1551 | .SUCCESS => return Impl{ .thread = &instance.thread }, |
| 1552 | .AGAIN => return error.ThreadQuotaExceeded, |
| 1553 | .INVAL => unreachable, |
| 1554 | .NOMEM => return error.SystemResources, |
| 1555 | .NOSPC => unreachable, |
| 1556 | .PERM => unreachable, |
| 1557 | .USERS => unreachable, |
| 1558 | else => |err| return posix.unexpectedErrno(err), |
| 1559 | } |
| 1560 | } |
| 1561 | |
| 1562 | fn getHandle(self: Impl) ThreadHandle { |
| 1563 | return self.thread.parent_tid; |
| 1564 | } |
| 1565 | |
| 1566 | fn detach(self: Impl) void { |
| 1567 | switch (self.thread.completion.swap(.detached, .seq_cst)) { |
| 1568 | .running => {}, |
| 1569 | .completed => self.join(), |
| 1570 | .detached => unreachable, |
| 1571 | } |
| 1572 | } |
| 1573 | |
| 1574 | fn join(self: Impl) void { |
| 1575 | defer posix.munmap(self.thread.mapped); |
| 1576 | |
| 1577 | while (true) { |
| 1578 | const tid = self.thread.child_tid.load(.seq_cst); |
| 1579 | if (tid == 0) break; |
| 1580 | |
| 1581 | switch (linux.errno(linux.futex_4arg( |
| 1582 | &self.thread.child_tid.raw, |
| 1583 | .{ .cmd = .WAIT, .private = false }, |
| 1584 | @bitCast(tid), |
| 1585 | null, |
| 1586 | ))) { |
| 1587 | .SUCCESS => continue, |
| 1588 | .INTR => continue, |
| 1589 | .AGAIN => continue, |
| 1590 | else => unreachable, |
| 1591 | } |
| 1592 | } |
| 1593 | } |
| 1594 | }; |
| 1595 | |
| 1596 | fn testThreadName(io: Io, thread: *Thread) !void { |
| 1597 | const testCases: []const []const u8 = &.{ |
| 1598 | "mythread", |
| 1599 | &@as([max_name_len]u8, @splat('b')), |
| 1600 | }; |
| 1601 | |
| 1602 | inline for (testCases) |tc| { |
| 1603 | try thread.setName(io, tc); |
| 1604 | |
| 1605 | var name_buffer: [max_name_len:0]u8 = undefined; |
| 1606 | |
| 1607 | const name = try thread.getName(&name_buffer); |
| 1608 | if (name) |value| { |
| 1609 | try std.testing.expectEqual(tc.len, value.len); |
| 1610 | try std.testing.expectEqualStrings(tc, value); |
| 1611 | } |
| 1612 | } |
| 1613 | } |
| 1614 | |
| 1615 | test "setName, getName" { |
| 1616 | if (builtin.single_threaded) return error.SkipZigTest; |
| 1617 | |
| 1618 | const io = testing.io; |
| 1619 | |
| 1620 | const Context = struct { |
| 1621 | start_wait_event: Io.Event = .unset, |
| 1622 | test_done_event: Io.Event = .unset, |
| 1623 | thread_done_event: Io.Event = .unset, |
| 1624 | |
| 1625 | done: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), |
| 1626 | thread: Thread = undefined, |
| 1627 | |
| 1628 | pub fn run(ctx: *@This()) !void { |
| 1629 | // Wait for the main thread to have set the thread field in the context. |
| 1630 | try ctx.start_wait_event.wait(io); |
| 1631 | |
| 1632 | switch (native_os) { |
| 1633 | .windows => testThreadName(io, &ctx.thread) catch |err| switch (err) { |
| 1634 | error.Unsupported => return error.SkipZigTest, |
| 1635 | else => return err, |
| 1636 | }, |
| 1637 | else => try testThreadName(io, &ctx.thread), |
| 1638 | } |
| 1639 | |
| 1640 | // Signal our test is done |
| 1641 | ctx.test_done_event.set(io); |
| 1642 | |
| 1643 | // wait for the thread to property exit |
| 1644 | try ctx.thread_done_event.wait(io); |
| 1645 | } |
| 1646 | }; |
| 1647 | |
| 1648 | var context = Context{}; |
| 1649 | var thread = try spawn(.{}, Context.run, .{&context}); |
| 1650 | |
| 1651 | context.thread = thread; |
| 1652 | context.start_wait_event.set(io); |
| 1653 | try context.test_done_event.wait(io); |
| 1654 | |
| 1655 | switch (native_os) { |
| 1656 | .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => { |
| 1657 | const res = thread.setName(io, "foobar"); |
| 1658 | try std.testing.expectError(error.Unsupported, res); |
| 1659 | }, |
| 1660 | .windows => testThreadName(io, &thread) catch |err| switch (err) { |
| 1661 | error.Unsupported => return error.SkipZigTest, |
| 1662 | else => return err, |
| 1663 | }, |
| 1664 | else => try testThreadName(io, &thread), |
| 1665 | } |
| 1666 | |
| 1667 | context.thread_done_event.set(io); |
| 1668 | thread.join(); |
| 1669 | } |
| 1670 | |
| 1671 | fn testIncrementNotify(io: Io, value: *usize, event: *Io.Event) void { |
| 1672 | value.* += 1; |
| 1673 | event.set(io); |
| 1674 | } |
| 1675 | |
| 1676 | test join { |
| 1677 | if (builtin.single_threaded) return error.SkipZigTest; |
| 1678 | |
| 1679 | const io = testing.io; |
| 1680 | |
| 1681 | var value: usize = 0; |
| 1682 | var event: Io.Event = .unset; |
| 1683 | |
| 1684 | const thread = try Thread.spawn(.{}, testIncrementNotify, .{ io, &value, &event }); |
| 1685 | thread.join(); |
| 1686 | |
| 1687 | try std.testing.expectEqual(value, 1); |
| 1688 | } |
| 1689 | |
| 1690 | test detach { |
| 1691 | if (builtin.single_threaded) return error.SkipZigTest; |
| 1692 | |
| 1693 | const io = testing.io; |
| 1694 | |
| 1695 | var value: usize = 0; |
| 1696 | var event: Io.Event = .unset; |
| 1697 | |
| 1698 | const thread = try Thread.spawn(.{}, testIncrementNotify, .{ io, &value, &event }); |
| 1699 | thread.detach(); |
| 1700 | |
| 1701 | try event.wait(io); |
| 1702 | try std.testing.expectEqual(value, 1); |
| 1703 | } |
| 1704 | |
| 1705 | test "Thread.getCpuCount" { |
| 1706 | if (native_os == .wasi) return error.SkipZigTest; |
| 1707 | |
| 1708 | const cpu_count = try Thread.getCpuCount(); |
| 1709 | try std.testing.expect(cpu_count >= 1); |
| 1710 | } |
| 1711 | |
| 1712 | fn testThreadIdFn(thread_id: *Thread.Id) void { |
| 1713 | thread_id.* = Thread.getCurrentId(); |
| 1714 | } |
| 1715 | |
| 1716 | test "Thread.getCurrentId" { |
| 1717 | if (builtin.single_threaded) return error.SkipZigTest; |
| 1718 | |
| 1719 | var thread_current_id: Thread.Id = undefined; |
| 1720 | const thread = try Thread.spawn(.{}, testThreadIdFn, .{&thread_current_id}); |
| 1721 | thread.join(); |
| 1722 | try std.testing.expect(Thread.getCurrentId() != thread_current_id); |
| 1723 | } |
| 1724 | |
| 1725 | test "thread local storage" { |
| 1726 | if (builtin.single_threaded) return error.SkipZigTest; |
| 1727 | if (@sizeOf(usize) == 4) return error.SkipZigTest; // https://github.com/ziglang/zig/issues/25498 |
| 1728 | |
| 1729 | const thread1 = try Thread.spawn(.{}, testTls, .{}); |
| 1730 | const thread2 = try Thread.spawn(.{}, testTls, .{}); |
| 1731 | try testTls(); |
| 1732 | thread1.join(); |
| 1733 | thread2.join(); |
| 1734 | } |
| 1735 | |
| 1736 | threadlocal var x: i32 = 1234; |
| 1737 | fn testTls() !void { |
| 1738 | if (x != 1234) return error.TlsBadStartValue; |
| 1739 | x += 1; |
| 1740 | if (x != 1235) return error.TlsBadEndValue; |
| 1741 | } |
| 1742 | |
| 1743 | /// Configures the per-thread alternative signal stack requested by `std.options.signal_stack_size`. |
| 1744 | pub fn maybeAttachSignalStack() void { |
| 1745 | const size = std.options.signal_stack_size orelse return; |
| 1746 | switch (builtin.target.os.tag) { |
| 1747 | // TODO: Windows vectored exception handlers always run on the main stack, but we could use |
| 1748 | // some target-specific inline assembly to swap the stack pointer. |
| 1749 | .windows => return, |
| 1750 | .wasi => return, |
| 1751 | else => {}, |
| 1752 | } |
| 1753 | const global = struct { |
| 1754 | threadlocal var signal_stack: [size]u8 = undefined; |
| 1755 | }; |
| 1756 | std.posix.sigaltstack(&.{ |
| 1757 | .sp = &global.signal_stack, |
| 1758 | .flags = 0, |
| 1759 | .size = size, |
| 1760 | }, null) catch |err| switch (err) { |
| 1761 | error.SizeTooSmall => unreachable, // `std.options.signal_stack_size` must be sufficient for the target |
| 1762 | error.PermissionDenied => unreachable, // called `maybeAttachSignalStack` from a signal handler |
| 1763 | error.Unexpected => unreachable, |
| 1764 | }; |
| 1765 | } |