Rust 76 🦀 Procedural Macros: Derive Macros (#[derive])
Derive macros are the most common kind of procedural macro in Rust. When you write #[derive(Debug, Clone, Serialize)], the compiler invokes a procedural macro for each of those names, passing the token stream of the annotated item, and expects a token stream of new items to append to the surrounding module . The macro is not a function that runs at runtime; it is a compiler plugin that generates code during compilation. This is how serde produces Serialize implementations without reflection, and how the standard library provides Debug and PartialEq without hand-written impl blocks .
The workflow has a strict structure. A derive macro must live in its own crate with proc-macro = true in Cargo.toml . The crate depends on syn to parse the input token stream into a syntax tree and quote to turn that syntax tree back into Rust code . The entry point is a public function annotated with #[proc_macro_derive(Name)], which receives a TokenStream and returns a TokenStream. The function parses the input into a DeriveInput, extracts the relevant parts (the type name, the generics, the fields), and uses quote! to build the implementation.
Key point: A derive macro is a function in a proc-macro crate annotated with #[proc_macro_derive(TraitName)]. It receives the token stream of the annotated item, parses it with syn::parse_macro_input! into a DeriveInput, and returns a TokenStream built with quote!. The generated code must use absolute paths (::std::...), thread the item’s generics through split_for_impl(), and emit syn::Error with the correct span instead of panicking.
Why derive macros exist
The boilerplate problem. Implementing Debug for a struct with ten fields means writing a fmt method that matches on every field. Implementing PartialEq means comparing every field. Implementing Serialize means emitting a key and a value for every field. Each of these is mechanical, repetitive, and error-prone when written by hand. A derive macro generates the implementation from the struct’s definition, so the developer writes the struct once and the impl is derived from it .
The reflection problem. Rust has no runtime reflection. A generic function cannot inspect a type’s fields and produce behavior based on their names or types at runtime. The derive macro operates at compile time, when the field names and types are still available as syntax. It can generate code that references each field explicitly, which is what serde does when it generates a serialize method that calls serializer.serialize_field("name", &self.name) for every field .
The orphan rule problem. A trait implementation must be in the crate that defines either the trait or the type. serde defines the Serialize trait but does not define the user’s struct. The user’s crate cannot implement Serialize manually without writing the entire impl block. The derive macro is invoked in the user’s crate, so the generated impl Serialize for User is in the user’s crate, which satisfies the orphan rule. This is why serde is a derive macro rather than a trait with a blanket implementation.
The error reporting problem. A derive macro that panics produces a compiler error with no span information—the error points at the macro invocation, not the offending field. The syn::Error type carries a Span, and calling .to_compile_error() produces a token stream that the compiler attributes to the correct location . This is the difference between a usable macro and a frustrating one.
a. The crate structure and the entry point
The derive macro crate must be separate from the crate that defines the trait. The convention is foo for the trait crate and foo_derive for the macro crate . The Cargo.toml declares the crate type and the dependencies.
# The macro crate's Cargo.toml
[package]
name = "hello_macro_derive"
version = "0.1.0"
edition = "2021"
# The lib section marks this crate as a procedural macro crate
[lib]
proc-macro = true
# syn parses the input tokens into a syntax tree
# quote turns the syntax tree back into tokens
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
The proc-macro = true flag is what allows the crate to link against the compiler’s proc_macro library and export the macro functions. Without it, the #[proc_macro_derive] attribute is rejected by the compiler .
The entry point is a public function. The name in the attribute is the name the user writes in #[derive(...)]. The function name itself is irrelevant to the user; it is only the attribute argument that matters.
// Import the compiler's token stream type
use proc_macro::TokenStream;
// Import the parser entry point and the syntax tree type
use syn::{parse_macro_input, DeriveInput};
// Import the quote macro for building output tokens
use quote::quote;
// The attribute declares the derive macro named "Hello"
// The user writes #[derive(Hello)] to invoke it
#[proc_macro_derive(Hello)]
pub fn derive_hello(input: TokenStream) -> TokenStream {
// Parse the input token stream into a DeriveInput syntax tree
// If the input is not a struct, enum, or union, this macro fails
let ast = parse_macro_input!(input as DeriveInput);
// Extract the type name from the parsed syntax tree
let name = &ast.ident;
// Build the output token stream with quote!
// The #name interpolation inserts the type name
let expanded = quote! {
// Generate the trait implementation
// The absolute path ::hello_macro::Hello avoids import issues
impl ::hello_macro::Hello for #name {
// Generate the method body
fn hello(&self) -> ::std::string::String {
// Use stringify! to get the type name as a string literal
// The macro has no runtime reflection, so this is compile-time
::std::format!("Hello, {}!", stringify!(#name))
}
}
};
// Convert the proc_macro2 token stream back to the compiler's token stream
expanded.into()
}
The parse_macro_input! macro is the standard entry point. It parses the TokenStream into the specified type and returns a compile_error! token stream if the parse fails, so the error appears at the macro invocation . The ast.ident is the name of the struct or enum. The quote! macro interpolates #name into the generated code, and the .into() converts the proc_macro2::TokenStream that quote! produces into the proc_macro::TokenStream that the compiler expects.
b. Handling generics and where clauses
A derive macro that ignores generics produces an impl that fails to compile for generic types. The DeriveInput contains a generics field, and split_for_impl() returns the three pieces needed to write a correct impl header: the impl generics, the type generics, and the where clause .
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
use quote::quote;
#[proc_macro_derive(Hello)]
pub fn derive_hello(input: TokenStream) -> TokenStream {
// Parse the input into the syntax tree
let ast = parse_macro_input!(input as DeriveInput);
// Extract the type name
let name = &ast.ident;
// Split the generics into the three parts needed for the impl
// impl_generics: the <T: Bound> part after "impl"
// ty_generics: the <T> part after the type name
// where_clause: the where clause, if any
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
// Build the impl with the generics threaded through
let expanded = quote! {
// The impl header includes the generics and the where clause
impl #impl_generics ::hello_macro::Hello for #name #ty_generics #where_clause {
fn hello(&self) -> ::std::string::String {
::std::format!("Hello, {}!", stringify!(#name))
}
}
};
expanded.into()
}
The split_for_impl() method is the mechanism that makes the derive work for struct Wrapper<T>(T). Without it, the generated impl would be impl Hello for Wrapper instead of impl<T> Hello for Wrapper<T>, and the compiler would reject the impl as having undeclared type parameters. The quote! interpolation places the three pieces in the correct positions: #impl_generics after impl, #ty_generics after the type name, and #where_clause at the end of the header.
c. Helper attributes and error reporting
A derive macro can declare helper attributes that the user writes on fields. The #[proc_macro_derive(MyTrait, attributes(my_attr))] syntax registers my_attr as an attribute that the macro can read. The Default derive uses this mechanism for #[default] on enum variants .
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
use quote::quote;
// The attributes(...) argument declares the helper attribute "skip"
// The user can write #[derive(Describe, skip)] on a field
#[proc_macro_derive(Describe, attributes(skip))]
pub fn derive_describe(input: TokenStream) -> TokenStream {
// Parse the input
let ast = parse_macro_input!(input as DeriveInput);
// Extract the type name
let name = &ast.ident;
// Handle only named structs for this example
// A real macro would handle enums and tuples too
let fields = match &ast.data {
// The Data::Struct variant contains the fields
Data::Struct(data) => match &data.fields {
// The Fields::Named variant contains the named fields
Fields::Named(fields) => &fields.named,
// Reject tuple structs with a compile error
_ => return syn::Error::new(ast.ident.span(), "expected named fields")
.to_compile_error()
.into(),
},
// Reject enums and unions
_ => return syn::Error::new(ast.ident.span(), "expected a struct")
.to_compile_error()
.into(),
};
// Collect the field names that are not marked with #[skip]
let field_names: Vec<_> = fields
.iter()
// Filter out fields that have the skip attribute
.filter(|f| !f.attrs.iter().any(|a| a.path().is_ident("skip")))
// Extract the identifier of each remaining field
.map(|f| f.ident.as_ref().unwrap())
.collect();
// Generate a method that returns the field names as a string
let expanded = quote! {
impl #name {
// The method joins the field names with commas
fn describe(&self) -> ::std::string::String {
::std::format!(
"{} has fields: {}",
// The type name as a string literal
stringify!(#name),
// The field names as string literals, joined
::std::vec![#(stringify!(#field_names)),*].join(", ")
)
}
}
};
expanded.into()
}
The syn::Error::new(span, message).to_compile_error() call is the correct way to report an error. It creates a token stream that the compiler interprets as a compile_error! invocation with the given span, so the error underlines the offending code rather than the macro invocation . A panic! in a derive macro produces an error with no span, which is nearly useless to the user.
The helper attribute registration is what allows the #[skip] attribute to appear on a field without the compiler rejecting it as an unknown attribute. Without the attributes(skip) declaration, the user’s #[skip] would be an error before the macro even runs.
Complete Example Session
The following session builds a complete derive macro that generates a Describe trait implementation, handling generics, helper attributes, and proper error reporting.
// ============================================
// PART 1: THE TRAIT CRATE
// ============================================
// The trait crate defines the trait that the derive macro implements
// It has no dependency on the macro crate
pub trait Describe {
fn describe(&self) -> String;
}
// ============================================
// PART 2: THE MACRO CRATE (Cargo.toml)
// ============================================
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = { version = "2", features = ["full"] }
// quote = "1"
// ============================================
// PART 3: THE MACRO IMPLEMENTATION
// ============================================
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};
// Declare the derive macro and its helper attribute
#[proc_macro_derive(Describe, attributes(skip))]
pub fn derive_describe(input: TokenStream) -> TokenStream {
// Parse the input into a DeriveInput
let ast = parse_macro_input!(input as DeriveInput);
// The type name
let name = &ast.ident;
// Split the generics for the impl header
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
// Extract the field names from the struct
let field_names: Vec<_> = match &ast.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => fields
.named
.iter()
// Skip fields marked with #[skip]
.filter(|f| !f.attrs.iter().any(|a| a.path().is_ident("skip")))
// Get the identifier of each field
.map(|f| f.ident.as_ref().unwrap())
.collect(),
// Reject tuple structs
_ => return syn::Error::new(ast.ident.span(), "expected named fields")
.to_compile_error()
.into(),
},
// Reject enums and unions
_ => return syn::Error::new(ast.ident.span(), "expected a struct")
.to_compile_error()
.into(),
};
// Build the impl
let expanded = quote! {
// The impl header threads the generics through
impl #impl_generics ::describe::Describe for #name #ty_generics #where_clause {
fn describe(&self) -> ::std::string::String {
::std::format!(
"{}: {}",
stringify!(#name),
// Build a comma-separated list of field names
::std::vec![#(stringify!(#field_names)),*].join(", ")
)
}
}
};
expanded.into()
}
// ============================================
// PART 4: THE USAGE
// ============================================
use describe::Describe;
use describe_derive::Describe;
// The derive generates the impl
// The #[skip] attribute is registered by the macro
#[derive(Describe)]
struct User {
name: String,
age: u32,
#[skip]
password: String,
}
fn main() {
let user = User {
name: "Alice".into(),
age: 30,
password: "secret".into(),
};
// The generated method returns "User: name, age"
println!("{}", user.describe());
}
The four parts cover the trait crate, the macro crate configuration, the macro implementation with generics and error handling, and the usage. What the session demonstrates is that the derive macro is a compile-time function that parses the annotated type and generates the trait impl, and that the helper attribute and the generics handling are what make it usable in real code.
Quick Reference
Crate Setup
| Item | Value |
|---|---|
| Crate type | proc-macro = true |
| Entry attribute | #[proc_macro_derive(Name)] |
| Parser | syn |
| Generator | quote |
| Entry function | fn(TokenStream) -> TokenStream |
Key Types and Functions
| Item | Purpose |
|---|---|
DeriveInput | Parsed struct, enum, or union |
parse_macro_input! | Parse with automatic error |
ast.ident | The type name |
ast.generics.split_for_impl() | Generics for the impl |
quote! | Build output tokens |
syn::Error::new(span, msg) | Span-aware error |
Common Pitfalls
| Pitfall | Fix |
|---|---|
| Generics missing | split_for_impl() |
| Path not found | Use ::std:: absolute paths |
| Unknown attribute | Declare in attributes(...) |
| Useless error | syn::Error not panic! |
Best Practices
✅ Do This:
// Use absolute paths in generated code
::std::string::String
// Thread generics through the impl
let (impl_g, ty_g, where_c) = ast.generics.split_for_impl();
// Report errors with spans
syn::Error::new(ast.ident.span(), "expected a struct").to_compile_error()
❌ Don’t Do This:
// Ignore generics
impl Hello for #name // fails for generic types
// Use bare paths
String::from("x") // may not be imported at the call site
// Panic on bad input
panic!("not a struct") // no span, useless error
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Generic impl fails | Generics not threaded | split_for_impl() |
| Path not found | Bare path not imported | ::std:: absolute |
| Unknown attribute | Helper not declared | attributes(name) |
| Error has no span | Used panic! | syn::Error |
| Macro not found | Wrong crate type | proc-macro = true |
Real-World Examples
1. Basic derive
#[proc_macro_derive(Hello)]
pub fn hello(input: TokenStream) -> TokenStream { ... }
2. Parse input
let ast = parse_macro_input!(input as DeriveInput);
3. Extract name
let name = &ast.ident;
4. Build impl
quote! { impl Hello for #name { ... } }
5. Generics
let (impl_g, ty_g, where_c) = ast.generics.split_for_impl();
6. Helper attribute
#[proc_macro_derive(Describe, attributes(skip))]
7. Error with span
syn::Error::new(ast.ident.span(), "error").to_compile_error()
8. Return tokens
expanded.into()
9. Named fields
match &ast.data { Data::Struct(d) => match &d.fields { Fields::Named(f) => ... } }
10. Filter fields
fields.named.iter().filter(|f| !f.attrs.iter().any(|a| a.path().is_ident("skip")))
Visual
Derive Macro Flow
┌──────────────────────────────────────────────┐
│ User writes: │
│ #[derive(Hello)] │
│ struct User { name: String } │
│ │ │
│ ▼ │
│ Compiler passes the struct's tokens │
│ to derive_hello() │
│ │ │
│ ▼ │
│ syn::parse_macro_input! → DeriveInput │
│ │ │
│ ▼ │
│ quote! builds the impl tokens │
│ │ │
│ ▼ │
│ TokenStream returned to compiler │
│ │ │
│ ▼ │
│ impl Hello for User { ... } compiled │
└──────────────────────────────────────────────┘
Generics Handling
┌──────────────────────────────────────────────┐
│ ast.generics.split_for_impl() │
│ │ │
│ ▼ │
│ impl_generics: <T: Bound> │
│ ty_generics: <T> │
│ where_clause: where T: Other │
│ │ │
│ ▼ │
│ impl #impl_g Hello for #name #ty_g #where_c │
└──────────────────────────────────────────────┘
Error Reporting
┌──────────────────────────────────────────────┐
│ BAD: panic!("not a struct") │
│ → error points at the macro invocation │
│ │
│ GOOD: syn::Error::new(span, "not a struct") │
│ .to_compile_error() │
│ → error points at the offending type │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Crate type | proc-macro = true |
| Entry attribute | #[proc_macro_derive(Name)] |
| Parser | syn::parse_macro_input! |
| Input type | DeriveInput |
| Generator | quote! |
| Generics | split_for_impl() |
| Helper attribute | attributes(name) |
| Error type | syn::Error |
| Output conversion | .into() |
Key takeaways:
- A derive macro is a function in a
proc-macrocrate. The#[proc_macro_derive(Name)]attribute declares it, and the function receives the annotated item’s token stream and returns the generated tokens . synparses,quotegenerates.syn::parse_macro_input!converts the token stream into aDeriveInputsyntax tree, andquote!builds the output tokens with#nameinterpolation .- Generics must be threaded through.
ast.generics.split_for_impl()returns the three pieces needed for a correct impl header, and ignoring them produces code that fails for generic types . - Helper attributes must be declared. The
attributes(name)argument toproc_macro_deriveregisters the attribute so the compiler does not reject it before the macro runs . - Errors must carry spans.
syn::Error::new(span, message).to_compile_error()produces a compile error at the correct location, whilepanic!produces an error with no useful span . - Absolute paths prevent hygiene problems. Procedural macros are unhygienic; the generated code is inserted as if written inline, so bare paths like
Stringmay resolve to the wrong item .
Remember: A derive macro is a compile-time function that turns a type definition into an impl block. The proc-macro crate is the boundary; syn and quote are the tools inside it. The DeriveInput gives you the name, the generics, and the fields, and quote! turns your logic into tokens the compiler can compile. The generics must be threaded through, the helper attributes must be declared, and the errors must carry spans. The result is code that the user writes once and the macro expands into the boilerplate they would otherwise write by hand.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!