zig-skills/references/std-debug.md

433 lines
12 KiB
Markdown

# std.debug (Zig 0.16.0)
Debugging utilities: panic handling, assertions, stack traces, hex dumps, and value tracing.
Primary Zig 0.16 release-note source: https://ziglang.org/download/0.16.0/release-notes.html
Zig 0.16 reworked debug information and expanded target support for segfault handling/unwinding. For application-owned output, create a writer from the caller's `std.Io` (for example `std.Io.File.stderr().writer(io, &buf)`). Low-level `std.debug.print` and stack-dump helpers intentionally use the configured debug I/O path instead.
## Quick Reference
| Function | Purpose |
|----------|---------|
| `print(fmt, args)` | Printf-style debug output to stderr |
| `panic(fmt, args)` | Format message and abort |
| `assert(bool)` | Crash if false (optimized out in ReleaseFast) |
| `dumpCurrentStackTrace(addr)` | Print stack trace to stderr |
| `dumpHex(bytes)` | Print hexdump to stderr |
## Debug Printing
```zig
const std = @import("std");
// Quick debug output (64-byte buffer, auto-flush)
std.debug.print("value: {}\n", .{x});
std.debug.print("name: {s}, count: {d}\n", .{name, count});
// Print without newline
std.debug.print("loading...", .{});
```
**Note:** `std.debug.print` silently ignores errors. For production logging, use `std.log`.
## Format Specifiers
Format string syntax: `{[argument][specifier]:[fill][alignment][width].[precision]}`. Named arguments use square brackets, for example `{[name]s}`.
### Type Specifiers
| Specifier | Types | Output |
|-----------|-------|--------|
| `{}` | any | Default formatting |
| `{s}` | `[]const u8`, `[*:0]const u8` | String |
| `{d}` | int, float, enum | Decimal |
| `{b}` | int, enum | Binary |
| `{o}` | int, enum | Octal |
| `{x}` | int, float, `[]u8`, enum | Lowercase hex |
| `{X}` | int, float, `[]u8`, enum | Uppercase hex |
| `{c}` | u8, u21 | ASCII character |
| `{u}` | u21 | Unicode codepoint |
| `{e}` | float | Scientific notation |
| `{*}` | pointer | Address (`Type@0x...`) |
| `{f}` | has `format` method | Custom formatter |
| `{any}` | any | Debug representation with depth limit |
### Examples
```zig
std.debug.print("{d}\n", .{42}); // "42"
std.debug.print("{x}\n", .{255}); // "ff"
std.debug.print("{X}\n", .{255}); // "FF"
std.debug.print("{b}\n", .{5}); // "101"
std.debug.print("{o}\n", .{64}); // "100"
std.debug.print("{s}\n", .{"hello"}); // "hello"
std.debug.print("{c}\n", .{'A'}); // "A"
std.debug.print("{*}\n", .{&value}); // "i32@7fff5fbff8a0"
// Floats
std.debug.print("{d}\n", .{3.14159}); // "3.14159"
std.debug.print("{e}\n", .{1234.5}); // "1.2345e+03"
std.debug.print("{x}\n", .{@as(f32, 1.0)}); // "0x1.0p0"
// Hex dump of bytes
std.debug.print("{x}\n", .{"hello"}); // "68656c6c6f"
```
### Width and Alignment
```zig
std.debug.print("{d:5}\n", .{42}); // " 42" (right-aligned, width 5)
std.debug.print("{d:<5}\n", .{42}); // "42 " (left-aligned)
std.debug.print("{d:^5}\n", .{42}); // " 42 " (center-aligned)
std.debug.print("{d:0>5}\n", .{42}); // "00042" (zero-padded)
std.debug.print("{s:_<10}\n", .{"hi"}); // "hi________" (custom fill)
```
### Precision
```zig
std.debug.print("{d:.2}\n", .{3.14159}); // "3.14"
std.debug.print("{e:.3}\n", .{1234.5}); // "1.234e+03"
std.debug.print("{x:.4}\n", .{@as(f32, 1.0)}); // "0x1.0000p0"
```
### Named and Positional Arguments
```zig
// Positional
std.debug.print("{0} {1} {0}\n", .{"a", "b"}); // "a b a"
// Named (with struct)
std.debug.print("{[name]s}: {[value]d}\n", .{ .name = "x", .value = 42 });
// Runtime width/precision
std.debug.print("{[value]d:[width]}\n", .{ .value = 42, .width = 5 });
std.debug.print("{[value]d:.[precision]}\n", .{ .value = 3.14159, .precision = 2 });
```
### Escape Braces
```zig
std.debug.print("{{literal braces}}\n", .{}); // "{literal braces}"
```
### Custom Format Method
Types can implement a `format` method for `{f}`:
```zig
const Point = struct {
x: f32,
y: f32,
pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void {
try writer.print("({d:.2}, {d:.2})", .{ self.x, self.y });
}
};
const p = Point{ .x = 1.5, .y = 2.5 };
std.debug.print("{f}\n", .{p}); // "(1.50, 2.50)"
```
### Any Format (Debug Representation)
```zig
const data = .{ .x = 1, .list = &[_]u8{ 1, 2, 3 } };
std.debug.print("{any}\n", .{data});
// Prints struct with depth-limited recursion
```
## Assertions
```zig
// Runtime assertion (triggers illegal instruction on failure)
std.debug.assert(x > 0);
std.debug.assert(ptr != null);
// Debug/ReleaseSafe: generates check
// ReleaseFast/ReleaseSmall: optimized away (undefined behavior if false)
```
### Specialized Assertions
```zig
// Assert slice is readable (checks memory mapping)
std.debug.assertReadable(slice);
// Assert pointer alignment
std.debug.assertAligned(ptr, .@"16"); // 16-byte alignment
```
## Panic
```zig
// Formatted panic message
std.debug.panic("invalid state: {}", .{state});
// With explicit return address
std.debug.panicExtra(@returnAddress(), "error: {s}", .{msg});
```
Panic prints message + stack trace to stderr, then aborts.
## Stack Traces
### Dump Current Stack
```zig
// Print current stack trace to stderr
std.debug.dumpCurrentStackTrace(.{});
// Skip frames until this address
std.debug.dumpCurrentStackTrace(.{ .first_address = @returnAddress() });
```
### Dump to Writer
```zig
var buf: [4096]u8 = undefined;
var locked = try io.lockStderr(&buf, null);
defer io.unlockStderr();
try std.debug.writeCurrentStackTrace(.{}, locked.terminal());
```
### Capture Stack Trace
```zig
var addrs: [32]usize = undefined;
const trace = std.debug.captureCurrentStackTrace(.{}, &addrs);
// Later: print captured trace
std.debug.dumpStackTrace(&trace);
```
`StackUnwindOptions` can also carry a target-specific `cpu_context.Native` pointer and opt into unsafe fallback unwinding. The stack iterator itself is an implementation detail; use `captureCurrentStackTrace`, `writeCurrentStackTrace`, and `dumpCurrentStackTrace` as the public interface.
## Hex Dump
```zig
const data = "Hello, World!\x00\x01\x02";
// Quick dump to stderr
std.debug.dumpHex(data);
// Output:
// 7fff5fbff8a0 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 00 01 02 Hello, World!...
// Fallible dump to an existing terminal abstraction
try std.debug.dumpHexFallible(terminal, data);
```
`dumpHexFallible` accepts `std.Io.Terminal`, not a bare writer, because it uses terminal color/mode operations. Use `.no_color` when constructing a terminal for a non-terminal sink.
Output format:
- Address (lowercase hex)
- 16 bytes per line (uppercase hex)
- ASCII representation (`.` for non-printable, special chars for `\n`, `\r`, `\t`)
## Value Tracing
Track where values originate and mutate during debugging:
```zig
const Trace = std.debug.Trace; // Pre-configured: 2 traces, 4 stack frames
const MyStruct = struct {
value: u32,
trace: Trace = .init,
fn setValue(self: *@This(), v: u32) void {
self.value = v;
self.trace.add("setValue called");
}
};
var s = MyStruct{ .value = 0 };
s.setValue(42);
s.trace.dump(); // Prints stack traces with notes
```
### Configurable Trace
```zig
// Custom configuration: 4 trace slots, 8 stack frames per trace
const MyTrace = std.debug.ConfigurableTrace(4, 8, true);
var trace: MyTrace = .init;
trace.add("first mutation");
trace.addAddr(@returnAddress(), "with explicit address");
// Check if tracing is enabled
if (MyTrace.enabled) {
trace.dump();
}
// Use in format strings
std.debug.print("trace: {}", .{trace});
```
The predefined `std.debug.Trace` is enabled only in Debug mode. A custom `ConfigurableTrace` follows its explicit `is_enabled` argument; when disabled, its operations are no-ops and its storage is zero-sized.
## SafetyLock
Debug helper to detect concurrent access violations:
```zig
const SafetyLock = std.debug.SafetyLock;
var lock: SafetyLock = .{};
fn criticalSection() void {
lock.lock();
defer lock.unlock();
// ... protected code
}
fn checkNotLocked() void {
lock.assertUnlocked(); // Panics if locked
}
```
- `SafetyLock` follows runtime-safety mode: active in Debug and ReleaseSafe, and a no-op in ReleaseFast and ReleaseSmall.
- `Trace` has the separate Debug-only default described above.
## Source Location
```zig
const SourceLocation = std.debug.SourceLocation;
const loc: SourceLocation = .{
.line = 42,
.column = 10,
.file_name = "src/main.zig",
};
// Invalid/unknown location
const unknown = SourceLocation.invalid;
```
## Symbol Information
```zig
const Symbol = std.debug.Symbol;
// Resolved fields are optional because debug information may be incomplete.
const sym: Symbol = .{
.name = "myFunction",
.compile_unit_name = "main.zig",
.source_location = .{ .line = 100, .column = 1, .file_name = "src/main.zig" },
};
// Unknown symbol
const unknown: Symbol = .unknown; // all three fields are null
```
## Segfault Handling
```zig
// Check if platform supports segfault handling
if (std.debug.have_segfault_handling_support) {
// Attach handler (prints stack trace on SIGSEGV/SIGBUS/etc)
std.debug.attachSegfaultHandler();
// Later: reset to default handler
std.debug.resetSegfaultHandler();
}
// Check if handler is enabled by default
const enabled = std.debug.default_enable_segfault_handler;
```
**Note:** `maybeEnableSegfaultHandler()` is called automatically by the runtime if `std.options.enable_segfault_handler` is true.
## CPU Context Unwinding
Signal handlers that already receive a native CPU context can pass it through `std.debug.StackUnwindOptions.context` to `captureCurrentStackTrace`, `writeCurrentStackTrace`, or `dumpCurrentStackTrace`. There are no public `std.debug.ThreadContext`, `getContext`, `copyContext`, or `dumpStackTraceFromBase` helpers in Zig 0.16; native context capture is target- and signal-handler-specific under `std.debug.cpu_context`.
## Valgrind Detection
```zig
if (std.debug.inValgrind()) {
// Running under Valgrind - may want different behavior
std.debug.print("Valgrind detected\n", .{});
}
```
## Debug Info Access
```zig
// Get debug info for current executable
const info = try std.debug.getSelfDebugInfo();
// Public methods are target-specific through SelfInfo. A broadly available
// operation is resolving the owning module name; returned storage is owned by
// SelfInfo rather than by the caller.
const module_name = try info.getModuleName(io, address);
std.debug.print("module: {s}\n", .{module_name});
```
## Constants
```zig
// Whether runtime safety checks are enabled
std.debug.runtime_safety // deprecated; reflects stdlib mode, not necessarily the caller's module
// Whether platform can produce stack traces
std.debug.sys_can_stack_trace // false on WASM, MIPS, etc.
```
## Submodules
| Module | Purpose |
|--------|---------|
| `std.debug.Dwarf` | DWARF debug info parser |
| `std.debug.Pdb` | Windows PDB debug info parser |
| `std.debug.SelfInfo` | Debug info for current executable |
| `std.debug.Coverage` | Code coverage support |
| `std.debug.cpu_context` | Target-specific native CPU context definitions |
## FullPanic
Create custom panic handler with formatted safety messages:
```zig
pub const panic = std.debug.FullPanic(myPanicFn);
fn myPanicFn(msg: []const u8, ret_addr: ?usize) noreturn {
// Custom panic handling (log to file, send telemetry, etc.)
std.posix.abort();
}
// Now safety checks use myPanicFn with descriptive messages:
// - "sentinel mismatch: expected X, found Y"
// - "index out of bounds: index N, len M"
// - "attempt to unwrap error: ErrorName"
// etc.
```
## Locking stderr
For multi-line debug output without interleaving:
```zig
// Lock stderr and clear any progress indicators. The returned object exposes
// a file writer and terminal; unlock performs the final flush automatically.
var lock_buf: [256]u8 = undefined;
const locked = std.debug.lockStderr(&lock_buf);
defer std.debug.unlockStderr();
// Safe to write multiple lines
try locked.file_writer.interface.writeAll("Line 1\n");
try locked.file_writer.interface.writeAll("Line 2\n");
```
The matching function is spelled `unlockStderr`:
```zig
var buf: [256]u8 = undefined;
const locked = std.debug.lockStderr(&buf);
defer std.debug.unlockStderr();
try locked.file_writer.interface.print("Complex output: {}\n", .{value});
```