zig-skills/references/std-debug.md

12 KiB

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. When debug output writes to stderr/stdout directly, use std.Io.File.stderr().writer(io, &buf) / std.Io.File.stdout().writer(io, &buf) rather than old std.io or std.fs.File APIs.

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

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: {[arg]:[fill][alignment][width][.precision][specifier]}

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

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

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

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

// Positional
std.debug.print("{0} {1} {0}\n", .{"a", "b"});  // "a b a"

// Named (with struct)
std.debug.print("{name}: {value}\n", .{ .name = "x", .value = 42 });

// Runtime width/precision
std.debug.print("{d:[width]}\n", .{ .width = 5, 42 });
std.debug.print("{d:.[precision]}\n", .{ .precision = 2, 3.14159 });

Escape Braces

std.debug.print("{{literal braces}}\n", .{});  // "{literal braces}"

Custom Format Method

Types can implement a format method for {f}:

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)

const data = .{ .x = 1, .list = &[_]u8{ 1, 2, 3 } };
std.debug.print("{any}\n", .{data});
// Prints struct with depth-limited recursion

Assertions

// 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

// Assert slice is readable (checks memory mapping)
std.debug.assertReadable(slice);

// Assert pointer alignment
std.debug.assertAligned(ptr, .@"16");  // 16-byte alignment

Panic

// 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

// Print current stack trace to stderr
std.debug.dumpCurrentStackTrace(null);

// Skip frames until this address
std.debug.dumpCurrentStackTrace(@returnAddress());

Dump to Writer

var buf: [4096]u8 = undefined;
var stderr = std.Io.File.stderr().writer(io, &buf);
try std.debug.dumpCurrentStackTraceToWriter(null, &stderr.interface);

Capture Stack Trace

var addrs: [32]usize = undefined;
var trace: std.builtin.StackTrace = .{
    .instruction_addresses = &addrs,
    .index = 0,
};
std.debug.captureStackTrace(@returnAddress(), &trace);

// Later: print captured trace
std.debug.dumpStackTrace(trace);

StackIterator

Walk the stack manually:

var it = std.debug.StackIterator.init(@returnAddress(), null);
defer it.deinit();

while (it.next()) |return_address| {
    const addr = return_address -| 1;
    std.debug.print("0x{x}\n", .{addr});
}

Hex Dump

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!...

// Dump to writer
var buf: [256]u8 = undefined;
var aw: std.io.Writer.Allocating = .init(allocator);
defer aw.deinit();
try std.debug.dumpHexFallible(&aw.writer, .no_color, data);

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:

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

// 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});

In release builds (enabled = false), all trace operations are no-ops with zero size.

SafetyLock

Debug helper to detect concurrent access violations:

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
}
  • In Debug/ReleaseSafe: actively tracks lock state
  • In ReleaseFast/ReleaseSmall: all methods are no-ops

Source Location

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

const Symbol = std.debug.Symbol;

// Symbol with resolved source location
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 = .{};  // name = "???", compile_unit_name = "???"

Segfault Handling

// 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.

Thread Context

Platform-specific CPU register state for stack unwinding:

const ThreadContext = std.debug.ThreadContext;

var ctx: ThreadContext = undefined;
if (std.debug.getContext(&ctx)) {
    // ctx now contains register state
    std.debug.dumpStackTraceFromBase(&ctx, stderr);
}

// Copy context (handles internal pointers)
var ctx_copy: ThreadContext = undefined;
std.debug.copyContext(&original_ctx, &ctx_copy);

Valgrind Detection

if (std.debug.inValgrind()) {
    // Running under Valgrind - may want different behavior
    std.debug.print("Valgrind detected\n", .{});
}

Debug Info Access

// Get debug info for current executable
const info = try std.debug.getSelfDebugInfo();

// Get symbol at address
const symbol = try info.getSymbolAtAddress(allocator, address);
defer if (symbol.source_location) |sl| allocator.free(sl.file_name);

std.debug.print("{s}:{d}: {s}\n", .{
    symbol.source_location.?.file_name,
    symbol.source_location.?.line,
    symbol.name,
});

Constants

// Whether runtime safety checks are enabled
std.debug.runtime_safety  // true in Debug/ReleaseSafe

// Whether platform can produce stack traces
std.debug.sys_can_stack_trace  // false on WASM, MIPS, etc.

// Whether platform has ucontext_t
std.debug.have_ucontext

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.MemoryAccessor Safe memory access for unwinding
std.debug.Coverage Code coverage support

FullPanic

Create custom panic handler with formatted safety messages:

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:

// Lock stderr and clear any progress indicators
std.debug.lockStdErr();
defer std.debug.unlockStdErr();

// Safe to write multiple lines
var buf: [256]u8 = undefined;
var stderr = std.Io.File.stderr().writer(io, &buf);
try stderr.interface.writeAll("Line 1\n");
try stderr.interface.writeAll("Line 2\n");
try stderr.interface.flush();

Or with a writer:

var buf: [256]u8 = undefined;
const writer = std.debug.lockStderrWriter(&buf);
defer std.debug.unlockStderrWriter();

try writer.print("Complex output: {}\n", .{value});