Rust Writing a Derive Macro
A derive macro is a procedural macro that adds an impl to a struct or enum. The user asks for it with a #[derive(...)] line.
Crate one: just the trait
hello_macro is a normal library crate. It holds only the trait, and nothing else:
// hello_macro/src/lib.rs
pub trait HelloMacro {
fn hello_macro();
}Crate two: the generator function
#[proc_macro_derive(HelloMacro)] marks the generator. The name in brackets is the name users write in #[derive(...)]. The generator receives the annotated item as a TokenStream:
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_hello_macro(&ast)
}Read the name, build the impl
ast.ident holds the type's name — ident is short for identifier. quote! builds ordinary Rust with #name where the parsed name belongs, and stringify!(#name) turns the name into text at compile time. Rust has no reflection. So the name could not be found at run time:
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}Carry the generics over
The impl above forgets the generics. A generic struct then fails to build. The error is E0107: missing generics for the struct. split_for_impl cuts the generics into three pieces. Each piece lands where an impl line needs it:
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let gen = quote! {
impl #impl_generics HelloMacro for #name #ty_generics #where_clause {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};The generator crate's Cargo.toml
The generator crate is named after the trait crate, with _derive on the end. Users depend on both crates and import both. Its Cargo.toml sets proc-macro = true and depends on syn and quote:
# hello_macro_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"The user side: two imports, one derive
The derive line runs the generator while compiling. The closing example below is this struct in a full program:
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;Example
// This program needs two companion crates (see the points above):
// hello_macro holds the trait, hello_macro_derive holds the generator.
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}Key points
- syn is the parser crate
- It reads Rust code into data you can inspect
- proc_macro itself ships with Rust
- It needs no Cargo.toml entry
- Hand the built code back with generated.into()
- The compiler adds it beside the user's struct
- A derive can add code
- It can never change or remove the item itself
- The generator unwraps the parse on purpose
- A derive function must return a TokenStream, not a Result
- So real crates panic with a clear message when the input cannot be parsed
- Use split_for_impl() on ast.generics for generic types
- It returns the impl generics, the type generics, and the where clause
- Each piece is already shaped for its slot in the impl line
- The derive built here adds a hello_macro() method that prints the struct's own name.
In this example, Pancakes is an empty struct — it has no methods at all. The line #[derive(HelloMacro)] runs the generator while compiling. The generator parses the struct, reads its name, Pancakes, from ast.ident, and builds an impl of the HelloMacro trait with that name filled in. The impl's method prints the greeting with stringify!, so main can call Pancakes::hello_macro() and see: Hello, Macro! My name is Pancakes!. The pancakes crate never wrote that impl — the derive did.
Note
serde's #[derive(Serialize)] is this same skeleton with a much bigger middle
Parse with syn
Inspect every field
Build the impl with quote!
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
In the generator, where does the type's name come from?