Commit 229f0a01b8

mlugg <mlugg@mlugg.co.uk>
2025-09-08 21:46:58
std.debug: handle ThreadContext slightly better
It's now user-overrideable, and uses `noreturn` types to neatly stop analysis.
1 parent 1392a7a
Changed files (2)
lib
std
debug
Dwarf
lib/std/debug/Dwarf/abi.zig
@@ -139,7 +139,7 @@ pub fn regBytes(
         };
     }
 
-    if (!std.debug.have_ucontext) return error.ThreadContextNotSupported;
+    if (posix.ucontext_t == void) return error.ThreadContextNotSupported;
 
     const ucontext_ptr = thread_context_ptr;
     return switch (builtin.cpu.arch) {
lib/std/debug.zig
@@ -330,19 +330,19 @@ test dumpHexFallible {
     try std.testing.expectEqualStrings(expected, aw.written());
 }
 
-pub const have_ucontext = posix.ucontext_t != void;
-
 /// Platform-specific thread state. This contains register state, and on some platforms
 /// information about the stack. This is not safe to trivially copy, because some platforms
 /// use internal pointers within this structure. After copying, call `relocateContext`.
-pub const ThreadContext = blk: {
-    if (native_os == .windows) {
-        break :blk windows.CONTEXT;
-    } else if (have_ucontext) {
-        break :blk posix.ucontext_t;
-    } else {
-        break :blk void;
+pub const ThreadContext = ThreadContext: {
+    // Allow overriding the target's `ThreadContext` by exposing `root.debug.ThreadContext`.
+    if (@hasDecl(root, "debug") and @hasDecl(root.debug, "ThreadContext")) {
+        break :ThreadContext root.debug.ThreadContext;
     }
+
+    if (native_os == .windows) break :ThreadContext windows.CONTEXT;
+    if (posix.ucontext_t != void) break :ThreadContext posix.ucontext_t;
+
+    break :ThreadContext noreturn;
 };
 /// Updates any internal pointers of a `ThreadContext` after the caller copies it.
 pub fn relocateContext(dest: *ThreadContext) void {
@@ -351,6 +351,10 @@ pub fn relocateContext(dest: *ThreadContext) void {
         else => {},
     }
 }
+/// The value which is placed on the stack to make a copy of a `ThreadContext`.
+const ThreadContextBuf = if (ThreadContext == noreturn) void else ThreadContext;
+/// The pointer through which a `ThreadContext` is received from callers of stack tracing logic.
+const ThreadContextPtr = if (ThreadContext == noreturn) noreturn else *const ThreadContext;
 
 /// Capture the current context. The register values in the context will reflect the
 /// state after the platform `getcontext` function returns.
@@ -358,7 +362,12 @@ pub fn relocateContext(dest: *ThreadContext) void {
 /// It is valid to call this if the platform doesn't have context capturing support,
 /// in that case `false` will be returned. This function is `inline` so that the `false`
 /// is comptime-known at the call site in that case.
-pub inline fn getContext(context: *ThreadContext) bool {
+pub inline fn getContext(context: *ThreadContextBuf) bool {
+    // Allow overriding the target's `getContext` by exposing `root.debug.getContext`.
+    if (@hasDecl(root, "debug") and @hasDecl(root.debug, "getContext")) {
+        return root.debug.getContext(context);
+    }
+
     if (native_os == .windows) {
         context.* = std.mem.zeroes(windows.CONTEXT);
         windows.ntdll.RtlCaptureContext(context);
@@ -608,8 +617,8 @@ pub const StackUnwindOptions = struct {
     first_address: ?usize = null,
     /// If not `null`, we will unwind from this `ThreadContext` instead of the current top of the
     /// stack. The main use case here is printing stack traces from signal handlers, where the
-    /// kernel provides a `*ThreadContext` of the state before the signal.
-    context: ?*const ThreadContext = null,
+    /// kernel provides a `*const ThreadContext` of the state before the signal.
+    context: ?ThreadContextPtr = null,
     /// If `true`, stack unwinding strategies which may cause crashes are used as a last resort.
     /// If `false`, only known-safe mechanisms will be attempted.
     allow_unsafe_unwind: bool = false,
@@ -620,7 +629,7 @@ pub const StackUnwindOptions = struct {
 ///
 /// See `writeCurrentStackTrace` to immediately print the trace instead of capturing it.
 pub fn captureCurrentStackTrace(options: StackUnwindOptions, addr_buf: []usize) std.builtin.StackTrace {
-    var context_buf: ThreadContext = undefined;
+    var context_buf: ThreadContextBuf = undefined;
     var it = StackIterator.init(options.context, &context_buf) catch {
         return .{ .index = 0, .instruction_addresses = &.{} };
     };
@@ -660,7 +669,7 @@ pub fn writeCurrentStackTrace(options: StackUnwindOptions, writer: *Writer, tty_
             return;
         },
     };
-    var context_buf: ThreadContext = undefined;
+    var context_buf: ThreadContextBuf = undefined;
     var it = StackIterator.init(options.context, &context_buf) catch |err| switch (err) {
         error.OutOfMemory => {
             tty_config.setColor(writer, .dim) catch {};
@@ -769,7 +778,7 @@ const StackIterator = union(enum) {
     /// It is important that this function is marked `inline` so that it can safely use
     /// `@frameAddress` and `getContext` as the caller's stack frame and our own are one
     /// and the same.
-    inline fn init(context_opt: ?*const ThreadContext, context_buf: *ThreadContext) error{OutOfMemory}!StackIterator {
+    inline fn init(context_opt: ?ThreadContextPtr, context_buf: *ThreadContextBuf) error{OutOfMemory}!StackIterator {
         if (builtin.cpu.arch.isSPARC()) {
             // Flush all the register windows on stack.
             if (builtin.cpu.has(.sparc, .v9)) {
@@ -1178,7 +1187,7 @@ pub const have_segfault_handling_support = switch (native_os) {
     .windows,
     => true,
 
-    .freebsd, .openbsd => have_ucontext,
+    .freebsd, .openbsd => ThreadContext != noreturn,
     else => false,
 };
 
@@ -1289,10 +1298,10 @@ fn handleSegfaultPosix(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopa
         => true,
         else => false,
     };
-    if (!have_ucontext or !use_context) return handleSegfault(addr, name, null);
+    if (ThreadContext == noreturn or !use_context) return handleSegfault(addr, name, null);
 
     // Some kernels don't align `ctx_ptr` properly, so we'll copy it into a local buffer.
-    var copied_ctx: ThreadContext = undefined;
+    var copied_ctx: ThreadContextBuf = undefined;
     const orig_ctx: *align(1) posix.ucontext_t = @ptrCast(ctx_ptr);
     copied_ctx = orig_ctx.*;
     if (builtin.os.tag.isDarwin() and builtin.cpu.arch == .aarch64) {
@@ -1329,7 +1338,7 @@ fn handleSegfaultWindows(info: *windows.EXCEPTION_POINTERS) callconv(.winapi) c_
     handleSegfault(addr, name, info.ContextRecord);
 }
 
-fn handleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?*ThreadContext) noreturn {
+fn handleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?ThreadContextPtr) noreturn {
     // Allow overriding the target-agnostic segfault handler by exposing `root.debug.handleSegfault`.
     if (@hasDecl(root, "debug") and @hasDecl(root.debug, "handleSegfault")) {
         return root.debug.handleSegfault(addr, name, opt_ctx);
@@ -1337,7 +1346,7 @@ fn handleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?*ThreadContext) nore
     return defaultHandleSegfault(addr, name, opt_ctx);
 }
 
-pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?*ThreadContext) noreturn {
+pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?ThreadContextPtr) noreturn {
     // There is very similar logic to the following in `defaultPanic`.
     switch (panic_stage) {
         0 => {
@@ -1355,7 +1364,6 @@ pub fn defaultHandleSegfault(addr: ?usize, name: []const u8, opt_ctx: ?*ThreadCo
                 } else {
                     stderr.print("{s} (no address available)\n", .{name}) catch break :trace;
                 }
-                // MLUGG TODO: for this to work neatly, `ThreadContext` needs to be `noreturn` when not supported!
                 if (opt_ctx) |context| {
                     writeCurrentStackTrace(.{
                         .context = context,