Declarative and Procedural Macros in Depth

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, or union remains in the AST completely unmodified.
  • The TokenStream returned by the derive macro is appended directly after the item (containing the generated impl 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:

  1. #[proc_macro_derive(...)] pub fn my_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream: Parses DeriveInput and converts any syn::Error into .to_compile_error().
  2. fn expand_my_derive(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>: Pure proc_macro2 logic that inspects input.ident, input.generics, and input.data (Data::Struct, Data::Enum, Data::Union) and emits quote! { ... }.
// 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"]