Expand description
Make your Rust types readable in GDB and LLDB with one derive.
use debuggable::Debuggable;
#[derive(Debuggable)]
#[debuggable(summary = "{len}/{cap}")]
pub struct Buffer {
#[debuggable(items, len = "len")]
data: Vec<u32>,
len: usize,
cap: usize,
}In a debugger, a Buffer then reads 3/8 = {len = 3, cap = 8, [0] = 1, [1] = 2, [2] = 3}
instead of its raw representation.
To see it, set up your debugger once with
cargo-debuggable:
cargo install cargo-debuggable && cargo debuggable setup. Nothing runs inside your
program: the derive only embeds a description of the type for the debugger to read.
§Attributes
All attributes are optional. A bare #[derive(Debuggable)] hides PhantomData fields
and, for enums, shows just the variant name when the variant has no fields.
§summary = "...": one-line summary
On a struct, or on an enum variant. {name} inserts a field, {0} a tuple field, {#}
the number of elements (on types with items), and {{ and }} are literal braces.
Summaries may use hidden fields. Each inserted field
shows the debugger’s own one-line rendering of it, so a field that is itself
Debuggable shows its summary.
#[derive(Debuggable)]
#[debuggable(summary = "({x}, {y})")]
pub struct Point { pub x: i32, pub y: i32 }
#[derive(Debuggable)]
#[debuggable(summary = "{0} m")]
pub struct Meters(pub f64);
#[derive(Debuggable)]
pub enum Shape {
#[debuggable(summary = "circle r={radius} at {center}")]
Circle { center: Point, radius: Meters },
Empty,
}A circle then shows as circle r=2.5 m at (1, 2). On an enum, put summary on each
variant; variants without one show their name.
§hide: leave a field out
#[derive(Debuggable)]
pub struct Cache {
pub hits: u64,
#[debuggable(hide)]
generation: u32,
_marker: std::marker::PhantomData<*const ()>, // hidden automatically
}§rename = "...": show a field under another name
#[derive(Debuggable)]
pub struct Account {
#[debuggable(rename = "balance_pence")]
balance: i64,
}§items and len = "field": show a collection’s elements
On one field per struct. The field’s elements are shown as [0], [1], … after the
other fields, in place of the field itself.
- On a
Vec<T>: shows its elements. Withlen, at most that many. - On an array
[T; N]stored in the struct (an inline buffer): shows its elements. Withlen, at most that many. - On a
*const T,*mut TorNonNull<T>:lenis required and gives the count.
Elements of type MaybeUninit<T> or ManuallyDrop<T> are shown as T. Only the first
len of them are read, so give len whenever some slots may be uninitialized.
#[derive(Debuggable)]
#[debuggable(summary = "{len} items")]
pub struct Stack<T> {
#[debuggable(items, len = "len")]
slots: Vec<Option<T>>, // only the first `len` slots are in use
#[debuggable(hide)]
len: usize,
}
#[derive(Debuggable)]
pub struct RawParts {
#[debuggable(items, len = "len")]
ptr: NonNull<u16>,
len: usize,
}
#[derive(Debuggable)]
pub struct Inline<T, const N: usize> {
#[debuggable(items, len = "len")]
buf: [std::mem::MaybeUninit<T>; N],
len: usize,
}At most 10,000 elements are shown.
§only and value: slot collections
Options for items on collections whose slots may be vacant, such as slab, arena or slot
map types. only keeps the elements that match, and value shows a field of each kept
element instead of the whole slot. Kept elements keep their index as their label, so
[3] is still slot 3.
only = "Variant": elements that are enums, in that variant.only = "field"oronly = "field & mask": elements whose integer orboolfield is non-zero, or has a bit ofmaskset. The path may go through fields:"meta.flags & 0x4".value = "path": a field of the element, or of the variant chosen byonly; through unions too.MaybeUninitandManuallyDropare unwrapped.
enum Entry<T> { Vacant(usize), Occupied(T) }
#[derive(Debuggable)]
#[debuggable(summary = "{len} items")]
pub struct Slab<T> {
#[debuggable(items, only = "Occupied", value = "0")]
entries: Vec<Entry<T>>,
#[debuggable(hide)]
len: usize,
}
union SlotUnion<T> { value: ManuallyDrop<T>, next_free: u32 }
struct Slot<T> { u: SlotUnion<T>, version: u32 } // odd version = occupied
#[derive(Debuggable)]
#[debuggable(summary = "{num_elems} items")]
pub struct SlotMap<V> {
#[debuggable(items, only = "version & 1", value = "u.value")]
slots: Vec<Slot<V>>,
#[debuggable(hide)]
num_elems: u32,
}A slab holding “a” in slot 1 and “d” in slot 3 then shows as
2 items = {[1] = "a", [3] = "d"}. Names inside the element type can’t be checked at
compile time; a misspelled variant shows <only: no variant ...> in the debugger.
§items = "path" on the type: inline-or-heap collections
For types whose elements live in one of several places, such as small-vector types that
move from an inline buffer to the heap. On the struct or enum, each
#[debuggable(items = "path", len = "path")] names one place the elements can be. The
first one whose paths exist in the current value is shown. A path segment can name an enum
variant, and then only leads somewhere while that variant is active. Paths start at the
type: on an enum, at a variant.
enum SmallVecData<A: Array> {
Inline(MaybeUninit<A>),
Heap { ptr: NonNull<A::Item>, len: usize },
}
#[derive(Debuggable)]
#[debuggable(summary = "{#} items")]
#[debuggable(items = "data.Inline.0", len = "capacity")] // inline: `capacity` is the length
#[debuggable(items = "data.Heap.ptr", len = "data.Heap.len")]
pub struct SmallVec<A: Array> {
#[debuggable(hide)]
capacity: usize,
#[debuggable(hide)]
data: SmallVecData<A>,
_marker: PhantomData<A::Item>,
}
#[derive(Debuggable)]
#[debuggable(items = "Inline.0", len = "Inline.1")]
#[debuggable(items = "Heap.0")]
pub enum TinyVec<T> {
#[debuggable(summary = "{#} items (inline)")]
Inline(#[debuggable(hide)] [T; 4], #[debuggable(hide)] u16),
#[debuggable(summary = "{#} items")]
Heap(#[debuggable(hide)] Vec<T>),
}Either way, three elements show as 3 items = {[0] = 1, [1] = 2, [2] = 3}. The first
segment of each path is checked at compile time; deeper ones can’t be, and if no place
matches, the debugger shows <items: no alternative matched>.
§text: show bytes as a string
On any number of fields. The field’s bytes are shown as a string literal, as UTF-8:
"hello". Escapes follow Rust’s {:?} for strings (\n, \", \0), and bytes that
aren’t valid UTF-8 show as \xNN, so nothing is lost. Takes the same sources as items,
with elements of one byte: [u8; N], [MaybeUninit<u8>; N], Vec<u8>, or a pointer with
len. With hide, the field gets no row of its own but a summary can still show it.
#[derive(Debuggable)]
#[debuggable(summary = "{buf}")]
pub struct InlineString<const N: usize> {
#[debuggable(text, len = "len", hide)]
buf: [MaybeUninit<u8>; N],
#[debuggable(hide)]
len: u8,
}An InlineString holding “hello” then shows as just "hello", like a String. At most
1,024 bytes are read; LLDB leaves out trailing NUL bytes.
§Compile errors
Every mistake is a compile error pointing at the attribute, with suggestions for typos:
#[derive(Debuggable)]
#[debuggable(summary = "{lenght} items")] // did you mean `length`?
pub struct Stack { length: usize }§Generics, enums, and dependencies
Generic types work for every instantiation. Enums work with any layout, including
niche-optimised ones (for example, an enum whose data variant holds a String). Types from
dependency crates work in the final binary like your own.
§Cost, and opting out
About 300 bytes per derived type in the final binary, plus a 6 KB GDB runtime once per
Linux binary: data in a section that only debuggers read. On Windows nothing is emitted
(not supported yet). Build with RUSTFLAGS="--cfg debuggable_disable" to emit nothing
anywhere.
§Limitations
- Types defined inside function bodies are not matched by the debugger
(
cargo debuggable doctor <binary>points them out). - Summaries refer to fields by name only: no expressions or format specs.
- Windows (Natvis) is not supported yet.
See the repository for the compatibility table and troubleshooting.
Derive Macros§
- Debuggable
- Derives debugger visualizers for a struct or an enum. See the crate docs.
Derives debugger visualizers for a struct or enum. See the
debuggablecrate docs.