#[derive(Debug)] saves you from writing formatting code for a type. To
understand a derive, start with the implementation it provides, then ask what
requirements that implementation places on the fields.
Compare derived and manual formatting
This complete program prints the same field values using two implementations:
use std::fmt;
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
struct ManualPoint {
x: i32,
y: i32,
}
impl fmt::Debug for ManualPoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Point")
.field("x", &self.x)
.field("y", &self.y)
.finish()
}
}
fn main() {
let derived = Point { x: 2, y: 5 };
let manual = ManualPoint { x: 2, y: 5 };
assert_eq!(derived.x + derived.y, 7);
println!("{derived:?}");
println!("{manual:?}");
println!("{manual:#?}");
}
The manual implementation is a readable model, not a promise about rustc's
exact expansion. debug_struct, field and finish are public formatting
helpers. They also support the alternate {:#?} layout. Debug formatting is
intended for inspection; its exact output is not a stable serialization format.
See the Debug documentation.
Built-in derives and custom derives
Rust implements built-in derives such as Debug inside the compiler. They do
not run a user crate's syn parser. Custom derives operate on token streams
from a struct, enum or union and emit additional items. A custom derive often
uses syn to parse and quote to generate tokens, but neither library is
required by the language.
A derive adds items alongside the original definition. It cannot replace that definition to add a field or change a field type. Its output is often a trait implementation, but custom derives can emit other items too.
Follow the bounds
For struct Wrapper<T> { inner: T }, formatting inner requires T: Debug.
An implementation that prints that field therefore needs the bound.
Built-in derive can also add a bound that the fields do not require. For
example, PhantomData<T> implements Debug even when T does not. A manual
implementation can preserve that flexibility:
use std::fmt;
use std::marker::PhantomData;
struct Marker<T> {
data: u32,
marker: PhantomData<T>,
}
impl<T> fmt::Debug for Marker<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Marker")
.field("data", &self.data)
.field("marker", &self.marker)
.finish()
}
}
struct NotDebug;
fn main() {
let value = Marker::<NotDebug> { data: 7, marker: PhantomData };
println!("{value:?}");
}
Replacing this manual implementation with #[derive(Debug)] makes this example
require NotDebug: Debug. When a generic type fails to compile, inspect the
implementation's bounds instead of assuming its fields need all of them.
The derive-bounds lesson
explores this distinction further.
Attribute macros can replace an item
An attribute macro receives its arguments and the annotated item as separate token streams. Its output replaces the item. It can preserve, transform or remove that item, or generate several items in its place. The resulting Rust still has to compile. Arguments have macro-specific syntax; their meaning is defined by that macro.
For example, #[tokio::main] transforms an async function into a synchronous
wrapper that constructs a runtime and drives an async block with block_on.
The block produces a future; it is not a closure. The default runtime flavor
is multithreaded, while flavor = "current_thread" selects a runtime driven by
the current thread. Availability depends on Tokio's enabled features. See
Tokio's examples and feature requirements.
This attribute can also annotate functions other than main. It creates a
runtime when the transformed function is called, so applying it broadly is
different from sharing one existing runtime.
Identify the kind of attribute
Not every #[...] is a procedural macro. #[test], #[cfg] and #[repr]
are built-in attributes. Test functions participate in the compiler's test
harness when compiled in test mode, for example with rustc --test.
When stacking attributes, follow the macro's documented placement rules. An attribute above a derive can see that derive in its item input and may preserve or remove it. Do not assume that swapping attributes always changes the program, or that unrelated macros compose safely in either order. The attribute reference describes expansion ordering; the procedural macro reference defines what each macro receives and emits.
Inspect an unfamiliar expansion
Start with the macro's documentation and the compiler diagnostic. If the
generated implementation is still unclear, an expansion tool such as
cargo expand can help. Treat expanded text as an inspection aid rather than
source you can always paste back unchanged.
Look for generated methods, added bounds and changed signatures. Custom derives and attribute macros live in a separate proc-macro crate from their callers; the ordinary formatting examples above need no such crate.