R
RUSTY YELLOW PAGES

#[debugger_visualizer(...)] / #[collapse_debuginfo]Attribute


Explanation

Both attributes exist purely to improve the experience of debugging Rust code in an external debugger — neither changes what the program computes or how it runs; they only change what a debugger UI shows while stepping through or inspecting it.

#[debugger_visualizer(natvis_file = "...")] (or the GDB pretty-printer-script equivalent form) attaches a custom visualization script to a type, embedded into the compiled binary's debug information so a debugger that understands the format can find and use it automatically. Without one, a debugger's default view of a value is usually just its raw fields laid out as the type's memory layout happens to place them — readable, but not necessarily meaningful. A Natvis file (Microsoft's XML-based format, understood by Visual Studio and Visual Studio Code's C++/Rust debugging extensions) or a GDB pretty-printer script instead tells the debugger how to render a value in a form that actually reflects what it represents — a custom RingBuffer type showing its logical contents in order, say, rather than a raw head index and backing array a reader would have to interpret by hand.

#[collapse_debuginfo], placed on a macro_rules! macro definition, controls whether code generated by expanding that macro is attributed, in debug information, to the macro's own definition site or to each individual call site where the macro was invoked. This affects how a debugger single-steps through macro-generated code: attributed to the call site, stepping through an invocation feels like stepping through ordinary inline code at the point of the call; attributed to the definition site (the default for some macros), a debugger can instead jump into the macro's own source location, which is sometimes what's wanted (to inspect the macro's expansion itself) and sometimes just confusing noise for a macro whose internals nobody stepping through calling code cares about.

Both are niche, debugging-tooling-focused attributes. Most Rust code never needs either one — they matter specifically to authors of libraries whose types or macros are widely used and would benefit from a noticeably better debugging experience for everyone downstream, not to day-to-day application code.

Usage examples

Attributing a macro's expanded code to its call site

#[collapse_debuginfo(yes)] // <- attributes this macro's expanded code to each call site, not its own definition
macro_rules! double {
    ($value:expr) => {
        $value * 2
    };
}

Designing a public API

A widely-used logging macro expands to a fair amount of internal plumbing; without #[collapse_debuginfo], stepping through a call to it in a debugger can drop the user into the macro's own definition rather than staying at the call site the developer actually cares about.

#[collapse_debuginfo(yes)] // <- keeps single-stepping at the call site, not inside the macro's own expansion
macro_rules! log_event {
    ($msg:expr) => {
        println!("[event] {}", $msg)
    };
}

fn process_order(order_id: u32) {
    log_event!(format!("processing order {order_id}")); // <- debugger steps through this line, not the macro body
}

A caller stepping through process_order is almost always interested in their own control flow, not in re-deriving how log_event! itself expands — the Rust Reference documents collapse_debuginfo as the mechanism for choosing that behavior per macro, appropriate for macros meant to feel like ordinary statements rather than something a caller needs to debug into.

Explanation

Embedded support: Partial

#[collapse_debuginfo] is unaffected by target — it's pure debug-info attribution for macro-generated code, and works the same whether the eventual debugger is a hosted one or GDB attached over a hardware probe. #[debugger_visualizer(natvis_file = "...")] is where the caveat lives: an embedded debugging session almost always goes through GDB (or an equivalent) over SWD/JTAG via a probe — probe-rs, OpenOCD, or a vendor tool — not through Visual Studio. Natvis is a Visual-Studio-specific XML format; a GDB frontend doesn't load it, so a #[debugger_visualizer(natvis_file = "...")] written for a HAL type has no effect at all in the GDB-based session actually used to debug the firmware on target. The GDB-pretty-printer-script form of #[debugger_visualizer(...)] is the one that matters for this workflow instead — though in practice, most embedded debugging leans less on custom pretty-printers and more on defmt (structured, low-overhead logging formatted on the host) piped over RTT, alongside probe-rs's own tooling, a workflow that doesn't route through the debugger's variable-inspection view at all.

Usage examples

Why a Natvis visualizer doesn't help a probe-rs/GDB debugging session

// AVOID relying on this alone for embedded debugging — Natvis is Visual-Studio-specific:
#[debugger_visualizer(natvis_file = "RingBuffer.natvis")] // <- has no effect when debugging via GDB over a probe
pub struct RingBuffer {
    head: usize,
    data: [u8; 64],
}

Reaching for defmt/RTT instead of a visualizer for on-target inspection

use defmt::Format;

#[derive(Format)] // <- defmt's own derive, not debugger_visualizer: formats this type for RTT logging
pub struct RingBuffer {
    head: usize,
    data: [u8; 64],
}

fn log_state(buf: &RingBuffer) {
    defmt::info!("ring buffer state: {:?}", buf); // <- printed over RTT, read by probe-rs on the host
}