Derive Macro Architecture
A custom derive macro—declared with #[proc_macro_derive(TraitName, attributes(helper_attr))]—is a procedural macro invoked via #[derive(TraitName)] on a struct, enum, or union.
Core Rule of Derive Macros: Additive Output Only
Unlike attribute macros (#[my_attr]), which replace the annotated item with the macro's output token stream, a #[proc_macro_derive] macro is purely additive:
- The original
struct,enum, orunionremains in the AST completely unmodified. - The
TokenStreamreturned by the derive macro is appended directly after the item (containing the generatedimpl TraitName for Type { ... }block and any private helper items).
Input to `#[proc_macro_derive(Describe, attributes(desc))]`:
#[derive(Describe)]
struct Server {
#[desc(label = "listen_port")]
port: u16,
}
Compiler Keeps Original Struct AND Appends Derive Output:
struct Server {
port: u16, // Helper attribute `#[desc(...)]` is stripped after derive runs!
}
impl Describe for Server {
fn describe(&self) -> String { ... }
}
Helper Attributes (attributes(...))
A derive macro can declare inert helper attributes inside #[proc_macro_derive(MyDerive, attributes(field_opt, rename))]. Users can then attach #[field_opt(...)] or #[rename(...)] to the struct, enum variants, or individual fields without defining separate attribute macros—the compiler passes those attributes inside DeriveInput.attrs and Field.attrs so the derive macro can inspect them!
Structure of a Production Derive Macro
A clean derive macro separates its entry point into two functions:
#[proc_macro_derive(...)] pub fn my_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream: ParsesDeriveInputand converts anysyn::Errorinto.to_compile_error().fn expand_my_derive(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>: Pureproc_macro2logic that inspectsinput.ident,input.generics, andinput.data(Data::Struct,Data::Enum,Data::Union) and emitsquote! { ... }.
// Complete architecture of a `#[proc_macro_derive(FieldNames)]` implementation:
/*
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Fields, parse_macro_input};
#[proc_macro_derive(FieldNames, attributes(skip_field))]
pub fn derive_field_names(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match expand_field_names(&input) {
Ok(tokens) => tokens.into(),
Err(err) => err.to_compile_error().into(),
}
}
fn expand_field_names(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let Data::Struct(data_struct) = &input.data else {
return Err(syn::Error::new_spanned(
input,
"`FieldNames` can only be derived on structs",
));
};
let Fields::Named(named_fields) = &data_struct.fields else {
return Err(syn::Error::new_spanned(
&data_struct.fields,
"`FieldNames` requires named struct fields",
));
};
let field_strs = named_fields
.named
.iter()
.filter(|f| !f.attrs.iter().any(|a| a.path().is_ident("skip_field")))
.filter_map(|f| f.ident.as_ref().map(|id| id.to_string()));
Ok(quote! {
impl #impl_generics #name #ty_generics #where_clause {
pub const FIELD_NAMES: &'static [&'static str] = &[ #( #field_strs ),* ];
}
})
}
*/
// Demonstrating the exact generated output produced by `derive_field_names` above:
struct UserRecord {
pub id: u64,
pub username: String,
pub password_hash: String,
}
// Output appended by `#[derive(FieldNames)]` with `#[skip_field]` on `password_hash`:
impl UserRecord {
pub const FIELD_NAMES: &'static [&'static str] = &["id", "username"];
}
fn main() {
let user = UserRecord {
id: 1,
username: String::from("prime"),
password_hash: String::from("***"),
};
println!(
"user #{}: exported FIELD_NAMES = {:?}",
user.id,
UserRecord::FIELD_NAMES
);
}
user #1: exported FIELD_NAMES = ["id", "username"]