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

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

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

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(.{});

// Skip frames until this address
std.debug.dumpCurrentStackTrace(.{ .first_address = @returnAddress() });

Dump to Writer

var buf: [4096]u8 = undefined;
var locked = try io.lockStderr(&buf, null);
defer io.unlockStderr();
try std.debug.writeCurrentStackTrace(.{}, locked.terminal());

Capture Stack Trace

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

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:

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

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:

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

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;

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

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

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();

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

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

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

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