Introduction
Custom derive macros are the most common type of procedural macro. They let you write #[derive(MyTrait)] on a struct or enum and have the compiler generate a trait implementation automatically. This section walks through creating a derive macro step by step, from the simplest case to handling generics correctly.
Key Concepts
#[proc_macro_derive(Name)]: The attribute that marks a function as a custom derive macro. TheNamebecomes the identifier used in#[derive(Name)].split_for_impl(): A method onsyn::Genericsthat splits generic parameters into the three parts needed for animplblock:impl<T> Name<T> where T: ....- Additive only: Derive macros can only add new items (impl blocks, functions). They cannot modify the original struct or enum.
Real World Context
Almost every Rust project uses custom derive macros. #[derive(Debug, Clone, Serialize, Deserialize)] is ubiquitous. Understanding how to create your own means you can eliminate boilerplate trait implementations specific to your domain — for example, deriving a Validate trait that checks struct fields against business rules.
Deep Dive
Step 1: Basic Implementation
Start with the simplest possible derive macro that generates a method:
rustuse proc_macro::TokenStream; use syn::{parse_macro_input, DeriveInput}; use quote::quote; #[proc_macro_derive(Hello)] pub fn hello_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; let name_str = name.to_string(); let expanded = quote! { impl #name { pub fn hello(&self) { println!("Hello from {}!", #name_str); } } }; expanded.into() }
This generates an impl block with a hello() method. The #name interpolation inserts the struct's name as an identifier, and #name_str inserts it as a string literal.
Step 2: Handle Generics
The basic version breaks for generic types like Person<T>. Use split_for_impl() to handle generics correctly:
rust#[proc_macro_derive(Hello)] pub fn hello_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; // Split generics into the three parts of an impl block let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let expanded = quote! { impl #impl_generics #name #ty_generics #where_clause { pub fn hello(&self) { println!("Hello from {}!", stringify!(#name)); } } }; expanded.into() }
The split_for_impl() method returns three parts: impl_generics goes after impl, ty_generics goes after the type name, and where_clause goes at the end. For struct Person<T: Display>, these expand to <T: Display>, <T>, and `` respectively.
Step 3: Use It
rustuse my_derive::Hello; #[derive(Hello)] struct Person<T> { name: String, data: T, } fn main() { let person = Person { name: "Alice".into(), data: 42 }; person.hello(); // Output: "Hello from Person!" }
The derive macro correctly generates impl<T> Person<T> { ... }, handling the generic parameter transparently.
Step 3: Process Fields
Most real derive macros need to iterate over the struct's fields to generate field-specific code:
rustlet field_prints = match &input.data { Data::Struct(data) => match &data.fields { Fields::Named(fields) => { fields.named.iter().map(|f| { let field_name = &f.ident; quote! { println!(" {}: {:?}", stringify!(#field_name), &self.#field_name); } }).collect::<Vec<_>>() } _ => return Error::new_spanned(name, "expected named fields") .to_compile_error().into(), }, _ => return Error::new_spanned(name, "Hello only works on structs") .to_compile_error().into(), };
This maps each named field to a println! statement that prints the field's name and value using Debug formatting.
Common Pitfalls
- Ignoring
split_for_impl()— Writingimpl #name { ... }without generics works for concrete types but fails for any generic struct. Always usesplit_for_impl()even if you think generics are unlikely. - Returning the wrong tokens — Derive macros must return new code to add, not a modified version of the input. The compiler appends your output after the original item.
- Missing trait bounds — If your generated code calls
.clone()on a field, you need to add aT: Clonebound. Usewhere_clausefromsplit_for_impl()and extend it with additional bounds.
Best Practices
- Always handle generics — Use
split_for_impl()from the start, even in tutorials. It costs nothing for non-generic types and prevents breakage later. - Generate trait impls, not inherent impls — Prefer
impl MyTrait for #nameoverimpl #name. Trait impls are namespaced, avoiding conflicts with user-defined methods. - Write integration tests — Create a
tests/directory in your derive crate with structs that exercise named fields, tuple structs, generics, and lifetimes.
Summary
- Custom derive macros add code alongside the annotated type using
#[proc_macro_derive(Name)]. split_for_impl()splits generics into the three parts needed for an impl block.- Derive macros are additive — they cannot modify the original item.
- Always handle generics and use
syn::Errorfor unsupported cases. - Prefer trait implementations over inherent impl blocks.
Code Examples
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Error};
use quote::quote;
// Generates a Debug-like implementation
#[proc_macro_derive(SimpleDebug)]
pub fn simple_debug_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) =
input.generics.split_for_impl();
let debug_body = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => {
let field_prints = fields.named.iter().map(|f| {
let field_name = &f.ident;
quote! { .field(stringify!(#field_name), &self.#field_name) }
});
quote! {
f.debug_struct(stringify!(#name))
#(#field_prints)*
.finish()
}
}
Fields::Unnamed(fields) => {
let field_prints = (0..fields.unnamed.len()).map(|i| {
let index = syn::Index::from(i);
quote! { .field(&self.#index) }
});
quote! {
f.debug_tuple(stringify!(#name))
#(#field_prints)*
.finish()
}
}
Fields::Unit => quote! { f.write_str(stringify!(#name)) },
},
_ => return Error::new_spanned(name, "SimpleDebug only supports structs")
.to_compile_error().into(),
};
let output = quote! {
impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#debug_body
}
}
};
output.into()
}