Rust Macro Metaprogramming: 5 Production Patterns from Declarative to Procedural Macros

编程语言

Why Do You Always Forget How Your Rust Macros Work

You wrote a macro_rules! and three days later you can't understand the matching rules; you wanted to auto-implement Serialize with a derive macro, but the proc-macro crate configuration is full of pitfalls; you used an attribute macro for logging decorators and got a pile of TokenStream mismatch errors; you tried generating code with procedural macros and found that syn parsing and quote splicing debugging relies entirely on cargo expand. In 2026, Rust's macro system provides declarative macros, three types of procedural macros (derive/attribute/function-like), and a mature syn+quote ecosystem — but macro programming is never just about writing code that compiles.

This article starts from declarative macros and walks you through macro_rules! DSL → Derive macro auto-implementation → Attribute macro decorators → Function-like procedural macros → syn/quote production code generation — 5 production patterns to take Rust macros from "it compiles" to "it's maintainable".


Core Concepts

Concept Description
macro_rules! Declarative macro, pattern-matching code template, expanded at compile time
Declarative Macro Another name for declarative macros, defined with macro_rules!
Procedural Macro Procedural macro, receives TokenStream input, returns TokenStream output
Derive Macro Derive macro, auto-implements traits for structs/enums
Attribute Macro Attribute macro, annotated on items, can modify or replace the annotated item
Function-like Proc Macro Function-like procedural macro, called like a function mac!()
TokenStream Token stream, input/output type for procedural macros
syn Rust source code parsing library, converts TokenStream to AST
quote Code generation library, converts Rust code templates to TokenStream
cargo expand Tool to expand macros and view generated code
Span Source code location info, used for error report positioning

Macro Expansion Flow

Macro Expansion Flow:
1. Compiler encounters macro invocation mac!(...)
2. Declarative macro: match pattern arm, replace with template code
   Procedural macro: convert input to TokenStream
3. Procedural macro parses TokenStream into AST (syn::parse)
4. Transform AST / generate new code
5. Convert generated code to TokenStream (quote::quote!)
6. Compiler compiles expansion result as normal Rust code
7. Expanded code participates in type checking and borrow checking

Problem Analysis: 5 Major Challenges in Rust Macro Development

  1. Declarative macro pattern matching is hard to debug: macro_rules! matching rules are obscure, $($x:tt),* repetition patterns are easy to forget, error messages are unhelpful
  2. Procedural macro crate configuration is cumbersome: derive/attribute/function-like macros must be in a separate proc-macro = true crate, isolated from the main crate
  3. TokenStream debugging relies entirely on cargo expand: println! inside procedural macros produces no output; you can only use cargo expand or eprintln! to stderr
  4. syn parsing of complex syntax is error-prone: Generic parameters, lifetimes, and where clauses require handling many edge cases
  5. Macro-generated code lacks IDE support: No autocompletion, go-to-definition, or refactoring for expanded code, high maintenance cost

Step-by-Step: 5 Production Macro Patterns

Pattern 1: Declarative Macros with macro_rules! and DSL Creation

macro_rules! define_enum_with_display {
    (
        $(#[$meta:meta])*
        $vis:vis enum $name:ident {
            $(
                $(#[$variant_meta:meta])*
                $variant:ident = $value:expr
            ),* $(,)?
        }
    ) => {
        $(#[$meta])*
        $vis enum $name {
            $(
                $(#[$variant_meta])*
                $variant = $value,
            )*
        }

        impl std::fmt::Display for $name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                match self {
                    $(
                        $name::$variant => write!(f, stringify!($variant)),
                    )*
                }
            }
        }

        impl $name {
            pub fn values() -> Vec<Self> {
                vec![$($name::$variant),*]
            }

            pub fn from_str(s: &str) -> Option<Self> {
                match s {
                    $(
                        stringify!($variant) => Some($name::$variant),
                    )*
                    _ => None,
                }
            }
        }
    };
}

define_enum_with_display! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum HttpStatus {
        Ok = 200,
        Created = 201,
        BadRequest = 400,
        Unauthorized = 401,
        NotFound = 404,
        InternalServerError = 500,
    }
}
macro_rules! builder_pattern {
    (
        $(#[$struct_meta:meta])*
        $vis:vis struct $name:ident {
            $(
                $(#[$field_meta:meta])*
                $field:ident : $ty:ty
            ),* $(,)?
        }
    ) => {
        $(#[$struct_meta])*
        $vis struct $name {
            $(
                $(#[$field_meta])*
                $field: Option<$ty>,
            )*
        }

        impl $name {
            pub fn builder() -> ${concat($name, Builder)} {
                ${concat($name, Builder)} {
                    $(
                        $field: None,
                    )*
                }
            }
        }

        pub struct ${concat($name, Builder)} {
            $(
                $field: Option<$ty>,
            )*
        }

        impl ${concat($name, Builder)} {
            $(
                pub fn $field(mut self, value: $ty) -> Self {
                    self.$field = Some(value);
                    self
                }
            )*

            pub fn build(self) -> Result<$name, String> {
                let missing: Vec<&str> = vec![
                    $(
                        if self.$field.is_none() { stringify!($field) } else { "" },
                    )*
                ].into_iter().filter(|s| !s.is_empty()).collect();

                if !missing.is_empty() {
                    return Err(format!("Missing required fields: {}", missing.join(", ")));
                }

                Ok($name {
                    $(
                        $field: self.$field,
                    )*
                })
            }
        }
    };
}

builder_pattern! {
    pub struct User {
        name: String,
        email: String,
        age: u32,
    }
}

Pattern 2: Derive Macros for Auto-Implementing Traits

# Cargo.toml (proc-macro crate)
[package]
name = "my_derive"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
syn = "2.0"
quote = "1.0"
proc-macro2 = "1.0"
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields, GenericParam, Ident};

#[proc_macro_derive(CustomDebug, attributes(debug_name))]
pub fn derive_custom_debug(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    let generics = &input.generics;
    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

    let debug_impl = match &input.data {
        Data::Struct(data) => {
            match &data.fields {
                Fields::Named(fields) => {
                    let field_debug: Vec<_> = fields.named.iter().map(|f| {
                        let field_name = &f.ident;
                        let field_name_str = field_name.as_ref().unwrap().to_string();
                        quote! {
                            .field(#field_name_str, &self.#field_name)
                        }
                    }).collect();

                    let struct_name_str = name.to_string();
                    quote! {
                        impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
                            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                                f.debug_struct(#struct_name_str)
                                    #(#field_debug)*
                                    .finish()
                            }
                        }
                    }
                }
                Fields::Unnamed(fields) => {
                    let field_debug: Vec<_> = fields.unnamed.iter().enumerate().map(|(i, _)| {
                        let idx = syn::Index::from(i);
                        quote! {
                            .field(&self.#idx)
                        }
                    }).collect();

                    let struct_name_str = name.to_string();
                    quote! {
                        impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
                            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                                f.debug_tuple(#struct_name_str)
                                    #(#field_debug)*
                                    .finish()
                            }
                        }
                    }
                }
                Fields::Unit => {
                    let struct_name_str = name.to_string();
                    quote! {
                        impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
                            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                                write!(f, #struct_name_str)
                            }
                        }
                    }
                }
            }
        }
        Data::Enum(data) => {
            let variant_debug: Vec<_> = data.variants.iter().map(|v| {
                let variant_name = &v.ident;
                let variant_name_str = variant_name.to_string();
                match &v.fields {
                    Fields::Named(fields) => {
                        let field_names: Vec<_> = fields.named.iter().map(|f| &f.ident).collect();
                        let field_strs: Vec<_> = field_names.iter().map(|f| f.as_ref().unwrap().to_string()).collect();
                        quote! {
                            #name::#variant_name { #(#field_names),* } => {
                                f.debug_struct(#variant_name_str)
                                    #( .field(#field_strs, #field_names) )*
                                    .finish()
                            }
                        }
                    }
                    Fields::Unnamed(fields) => {
                        let field_names: Vec<_> = fields.unnamed.iter().enumerate().map(|(i, _)| {
                            Ident::new(&format!("f{}", i), v.ident.span())
                        }).collect();
                        quote! {
                            #name::#variant_name(#(#field_names),*) => {
                                f.debug_tuple(#variant_name_str)
                                    #( .field(#field_names) )*
                                    .finish()
                            }
                        }
                    }
                    Fields::Unit => {
                        quote! {
                            #name::#variant_name => write!(f, #variant_name_str)
                        }
                    }
                }
            }).collect();

            quote! {
                impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause {
                    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                        match self {
                            #(#variant_debug),*
                        }
                    }
                }
            }
        }
        Data::Union(_) => {
            syn::Error::new_spanned(name, "CustomDebug does not support unions")
                .to_compile_error()
                .into()
        }
    };

    debug_impl.into()
}
use my_derive::CustomDebug;

#[derive(CustomDebug)]
pub struct ApiResponse {
    pub status: u16,
    pub message: String,
    pub data: Option<serde_json::Value>,
}

#[derive(CustomDebug)]
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Pattern 3: Attribute Macros for Logging, Validation, and Caching Decorators

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, FnArg, Pat, Signature, Visibility};

#[proc_macro_attribute]
pub fn traced(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);
    let ItemFn { attrs, vis, sig, block, .. } = input;

    let fn_name = &sig.ident.to_string();
    let fn_name_lit = syn::LitStr::new(fn_name, sig.ident.span());

    let expanded = quote! {
        #(#attrs)*
        #vis #sig {
            let __start = std::time::Instant::now();
            tracing::info!(function = #fn_name_lit, "→ entering");
            let __result = #block;
            let __elapsed = __start.elapsed();
            tracing::info!(
                function = #fn_name_lit,
                elapsed_ms = __elapsed.as_millis() as u64,
                "← exiting"
            );
            __result
        }
    };

    expanded.into()
}

#[proc_macro_attribute]
pub fn validate_args(attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);
    let attr_args: Vec<String> = attr.to_string()
        .split(',')
        .map(|s| s.trim().to_string())
        .collect();

    let ItemFn { attrs, vis, sig, block, .. } = input;
    let fn_name = &sig.ident;

    let validations: Vec<_> = sig.inputs.iter().filter_map(|arg| {
        if let FnArg::Typed(pat_type) = arg {
            if let Pat::Ident(pat_ident) = &*pat_type.pat {
                let param_name = pat_ident.ident.to_string();
                if attr_args.is_empty() || attr_args.contains(&param_name) {
                    let ident = &pat_ident.ident;
                    return Some(quote! {
                        if #ident.is_empty() {
                            return Err(format!("Parameter '{}' must not be empty", #param_name));
                        }
                    });
                }
            }
        }
        None
    }).collect();

    let return_type = match &sig.output {
        syn::ReturnType::Type(_, ty) => quote! { #ty },
        syn::ReturnType::Default => quote! { () },
    };

    let inputs = &sig.inputs;
    let output = &sig.output;

    let expanded = quote! {
        #(#attrs)*
        #vis fn #fn_name(#inputs) #output {
            #(#validations)*
            #block
        }
    };

    expanded.into()
}

#[proc_macro_attribute]
pub fn cached(attr: TokenStream, item: TokenStream) -> TokenStream {
    let ttl_secs: u64 = if attr.is_empty() {
        60
    } else {
        attr.to_string().parse().unwrap_or(60)
    };

    let input = parse_macro_input!(item as ItemFn);
    let ItemFn { attrs, vis, sig, block, .. } = input;
    let fn_name = &sig.ident;
    let fn_name_str = fn_name.to_string();

    let expanded = quote! {
        #(#attrs)*
        #vis #sig {
            static CACHE: std::sync::OnceLock<std::sync::Mutex<(std::time::Instant, Option<_>)>> = std::sync::OnceLock::new();
            let cache = CACHE.get_or_init(|| std::sync::Mutex::new((std::time::Instant::now(), None)));
            let mut guard = cache.lock().unwrap();
            let now = std::time::Instant::now();
            if let (created, Some(ref val)) = guard.0 {
                if now.duration_since(created).as_secs() < #ttl_secs {
                    return val.clone();
                }
            }
            drop(guard);
            let result = #block;
            let mut guard = cache.lock().unwrap();
            *guard = (std::time::Instant::now(), Some(result.clone()));
            result
        }
    };

    expanded.into()
}
use my_macros::{traced, validate_args, cached};

#[traced]
async fn fetch_user(user_id: &str) -> Result<User, AppError> {
    let client = reqwest::Client::new();
    let resp = client.get(&format!("/api/users/{}", user_id)).send().await?;
    Ok(resp.json().await?)
}

#[validate_args(name, email)]
fn create_user(name: String, email: String, age: u32) -> Result<User, String> {
    Ok(User { name, email, age })
}

#[cached(300)]
fn get_config() -> AppConfig {
    load_config_from_file()
}

Pattern 4: Function-like Procedural Macros for SQL Query Builder

use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{Ident, LitStr, Token, Result as SynResult};

struct SqlQuery {
    table: Ident,
    columns: Vec<Ident>,
    where_clause: Option<LitStr>,
}

impl Parse for SqlQuery {
    fn parse(input: ParseStream) -> SynResult<Self> {
        let table: Ident = input.parse()?;
        let mut columns = vec![table.clone()];

        if input.peek(Token![.]) {
            input.parse::<Token![.]>()?;
            let method: Ident = input.parse()?;
            if method != "select" {
                return Err(syn::Error::new(method.span(), "Expected .select()"));
            }
            input.parse::<Token![()]>();
            let content;
            syn::parenthesized!(content in input);
            columns.clear();
            loop {
                let col: Ident = content.parse()?;
                columns.push(col);
                if content.peek(Token![,]) {
                    content.parse::<Token![,]>()?;
                } else {
                    break;
                }
            }
        }

        let where_clause = if input.peek(Token![.]) {
            input.parse::<Token![.]>()?;
            let method: Ident = input.parse()?;
            if method == "where_clause" {
                input.parse::<Token![()]>();
                let content;
                syn::parenthesized!(content in input);
                Some(content.parse()?)
            } else {
                None
            }
        } else {
            None
        };

        Ok(SqlQuery { table, columns, where_clause })
    }
}

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    let query = match syn::parse::<SqlQuery>(input) {
        Ok(q) => q,
        Err(e) => return e.to_compile_error().into(),
    };

    let table_name = query.table.to_string();
    let column_names: Vec<String> = query.columns.iter().map(|c| c.to_string()).collect();
    let column_refs: Vec<_> = column_names.iter().map(|c| c.as_str()).collect();

    let struct_name = Ident::new(
        &format!("{}Row", to_pascal_case(&table_name)),
        query.table.span(),
    );

    let field_names: Vec<Ident> = column_names.iter()
        .map(|c| Ident::new(&to_snake_case(c), query.table.span()))
        .collect();

    let field_types: Vec<_> = column_names.iter().map(|_| quote! { String }).collect();

    let select_clause = if column_names.len() == 1 && column_names[0] == table_name {
        "*".to_string()
    } else {
        column_names.join(", ")
    };

    let where_part = match &query.where_clause {
        Some(w) => format!(" WHERE {}", w.value()),
        None => String::new(),
    };

    let sql_string = format!("SELECT {} FROM {}{}", select_clause, table_name, where_part);

    let expanded = quote! {
        struct #struct_name {
            #(pub #field_names: #field_types,)*
        }

        impl #struct_name {
            pub fn sql() -> &'static str {
                #sql_string
            }

            pub fn from_row(row: &sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
                Ok(Self {
                    #(
                        #field_names: row.try_get(stringify!(#field_names))?,
                    )*
                })
            }
        }
    };

    expanded.into()
}

fn to_pascal_case(s: &str) -> String {
    s.split('_')
        .map(|word| {
            let mut chars = word.chars();
            match chars.next() {
                None => String::new(),
                Some(c) => c.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
            }
        })
        .collect()
}

fn to_snake_case(s: &str) -> String {
    s.to_lowercase()
}
use my_macros::sql;

sql!(users.select(id, name, email).where_clause("active = true"));

sql!(orders.select(id, user_id, total));

sql!(products);

Pattern 5: Production Code Generation with syn/quote

use proc_macro::TokenStream;
use quote::quote;
use syn::{
    parse_macro_input, DeriveInput, Data, Fields, Attribute, Meta, Lit, NestedMeta,
    GenericParam, LifetimeParam, TypeParam, ConstParam,
};

#[proc_macro_derive(ApiEndpoint, attributes(api_route, api_method, api_auth))]
pub fn derive_api_endpoint(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    let route = get_string_attr(&input.attrs, "api_route").unwrap_or_else(|| "/".to_string());
    let method = get_string_attr(&input.attrs, "api_method").unwrap_or_else(|| "GET".to_string());
    let auth_required = get_bool_attr(&input.attrs, "api_auth").unwrap_or(false);

    let request_type = Ident::new(&format!("{}Request", name), name.span());
    let response_type = Ident::new(&format!("{}Response", name), name.span());
    let handler_name = Ident::new(&format!("handle_{}", to_snake_case(&name.to_string())), name.span());

    let (request_fields, response_fields) = match &input.data {
        Data::Struct(data) => {
            match &data.fields {
                Fields::Named(fields) => {
                    let req_fields: Vec<_> = fields.named.iter()
                        .filter(|f| !has_attr(&f.attrs, "response_only"))
                        .map(|f| {
                            let fname = f.ident.as_ref().unwrap();
                            let ftype = &f.ty;
                            quote! { pub #fname: #ftype }
                        }).collect();

                    let resp_fields: Vec<_> = fields.named.iter()
                        .filter(|f| !has_attr(&f.attrs, "request_only"))
                        .map(|f| {
                            let fname = f.ident.as_ref().unwrap();
                            let ftype = &f.ty;
                            quote! { pub #fname: #ftype }
                        }).collect();

                    (req_fields, resp_fields)
                }
                _ => (vec![], vec![]),
            }
        }
        _ => (vec![], vec![]),
    };

    let method_ident = match method.to_uppercase().as_str() {
        "POST" => quote! { axum::routing::post },
        "PUT" => quote! { axum::routing::put },
        "DELETE" => quote! { axum::routing::delete },
        "PATCH" => quote! { axum::routing::patch },
        _ => quote! { axum::routing::get },
    };

    let auth_middleware = if auth_required {
        quote! {
            .layer(axum::middleware::from_fn(auth_middleware))
        }
    } else {
        quote! {}
    };

    let route_lit = syn::LitStr::new(&route, name.span());

    let generics = &input.generics;
    let phantom_generics: Vec<_> = generics.params.iter().map(|param| {
        match param {
            GenericParam::Type(t) => {
                let ident = &t.ident;
                quote! { std::marker::PhantomData<#ident> }
            }
            GenericParam::Lifetime(l) => {
                let lifetime = &l.lifetime;
                quote! { std::marker::PhantomData<&#lifetime ()> }
            }
            GenericParam::Const(c) => {
                let ident = &c.ident;
                quote! { std::marker::PhantomData<[(); #ident]> }
            }
        }
    }).collect();

    let expanded = quote! {
        #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
        pub struct #request_type {
            #(#request_fields,)*
        }

        #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
        pub struct #response_type {
            #(#response_fields,)*
            #(pub #phantom_generics,)*
        }

        pub async fn #handler_name(
            axum::Json(req): axum::Json<#request_type>,
        ) -> Result<axum::Json<#response_type>, AppError> {
            let response = #name::handle(req).await?;
            Ok(axum::Json(response))
        }

        impl #name {
            pub fn register(router: axum::Router<SharedState>) -> axum::Router<SharedState> {
                router.route(#route_lit, #method_ident(#handler_name))
                    #auth_middleware
            }
        }
    };

    expanded.into()
}

fn get_string_attr(attrs: &[Attribute], name: &str) -> Option<String> {
    attrs.iter().find_map(|attr| {
        if attr.path.is_ident(name) {
            if let Ok(Meta::List(list)) = attr.parse_meta() {
                if let Some(NestedMeta::Lit(Lit::Str(s))) = list.nested.first() {
                    return Some(s.value());
                }
            }
        }
        None
    })
}

fn get_bool_attr(attrs: &[Attribute], name: &str) -> Option<bool> {
    attrs.iter().find_map(|attr| {
        if attr.path.is_ident(name) {
            if let Ok(Meta::Path(_)) = attr.parse_meta() {
                return Some(true);
            }
        }
        None
    })
}

fn has_attr(attrs: &[Attribute], name: &str) -> bool {
    attrs.iter().any(|attr| attr.path.is_ident(name))
}

fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    for (i, c) in s.chars().enumerate() {
        if c.is_uppercase() && i > 0 {
            result.push('_');
        }
        result.push(c.to_lowercase().next().unwrap());
    }
    result
}
use my_derive::ApiEndpoint;

#[derive(ApiEndpoint)]
#[api_route("/api/users")]
#[api_method("GET")]
#[api_auth]
pub struct ListUsers {
    pub page: u32,
    pub page_size: u32,
    #[request_only]
    pub search: Option<String>,
    #[response_only]
    pub users: Vec<User>,
    #[response_only]
    pub total: u64,
}

#[derive(ApiEndpoint)]
#[api_route("/api/users")]
#[api_method("POST")]
pub struct CreateUser {
    pub name: String,
    pub email: String,
    #[response_only]
    pub id: String,
    #[response_only]
    pub created_at: String,
}

Pitfall Guide

Pitfall 1: macro_rules! Hygiene Traps

// ❌ Wrong: Variable names in declarative macros may conflict with caller scope
macro_rules! swap {
    ($a:expr, $b:expr) => {
        let temp = $a;  // temp may conflict with caller's temp
        $a = $b;
        $b = temp;
    };
}

// ✅ Correct: Use unique prefix to avoid naming conflicts
macro_rules! swap {
    ($a:expr, $b:expr) => {
        let __swap_temp = $a;
        $a = $b;
        $b = __swap_temp;
    };
}

Pitfall 2: Procedural Macro Crate in Main Crate

# ❌ Wrong: Proc macro and main code in the same crate
[package]
name = "my_app"
[lib]
proc-macro = true  # Compile error! proc-macro crate cannot have non-macro code

# ✅ Correct: Proc macro in a separate crate
# my_derive/Cargo.toml
[package]
name = "my_derive"
[lib]
proc-macro = true

# my_app/Cargo.toml
[dependencies]
my_derive = { path = "../my_derive" }

Pitfall 3: TokenStream Debugging Shows No Output

// ❌ Wrong: println! inside proc macro doesn't output at compile time
#[proc_macro_derive(MyDebug)]
pub fn derive_debug(input: TokenStream) -> TokenStream {
    println!("Debugging: {}", input);  // Not visible at compile time!
    // ...
}

// ✅ Correct: Use eprintln! to stderr, or use cargo expand
#[proc_macro_derive(MyDebug)]
pub fn derive_debug(input: TokenStream) -> TokenStream {
    eprintln!("Debugging: {}", input);  // Outputs to stderr at compile time
    // ...
}
// Then run: RUST_BACKTRACE=1 cargo build 2>&1 | head
// Or: cargo expand

Pitfall 4: Missing where Clause When Parsing Generics with syn

// ❌ Wrong: Ignoring generic constraints, generated code may not compile
let name = &input.ident;
let generics = &input.generics;
quote! {
    impl #generics MyTrait for #name #generics { ... }
}

// ✅ Correct: Use split_for_impl to properly handle generics
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
    impl #impl_generics MyTrait for #name #ty_generics #where_clause { ... }
}

Pitfall 5: Attribute Macro Consuming Original Item

// ❌ Wrong: Attribute macro doesn't preserve original function, other attributes lost
#[proc_macro_attribute]
pub fn my_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);
    // Only returns new code, original ItemFn is discarded
    quote! { fn new_func() {} }.into()
}

// ✅ Correct: Wrap around original item, preserving original code
#[proc_macro_attribute]
pub fn my_attr(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);
    let ItemFn { attrs, vis, sig, block, .. } = input;
    quote! {
        #(#attrs)*
        #vis #sig {
            // Pre-logic
            let __result = #block;
            // Post-logic
            __result
        }
    }.into()
}

Error Troubleshooting

# Error Cause Solution
1 proc-macro crate cannot export non-macro items Non-macro pub items defined in proc-macro crate Move helper types/functions to separate crate, proc-macro crate only has macro definitions
2 cannot find macro my_derive in this scope Missing derive crate dependency Add my_derive = { path = "..." } in Cargo.toml
3 expected ident, found punctuation syn expected identifier but found punctuation during TokenStream parsing Check macro invocation syntax, ensure parameter order and types match
4 recursion limit reached while expanding macro Declarative macro recursive expansion exceeded default limit Add #![recursion_limit = "256"] at top of lib.rs
5 mismatched types: expected TokenStream, found String Proc macro returns String instead of TokenStream Use quote!{...}.into() or input.to_string().parse().unwrap()
6 the trait bound X: Y is not satisfied Macro-generated code missing trait constraint Add constraints on generic parameters: where T: Trait
7 procedural macro panicked Proc macro panicked internally Use syn::Error::new_spanned() to return compile error instead of panic
8 variable 'temp' is not available in current scope Declarative macro hygiene issue, introduced variable not visible Use $crate:: prefix or pass variable as parameter
9 cargo expand shows unexpected code Macro expansion result differs from expectation Add eprintln! debugging in proc macro, check quote! output step by step
10 attribute macro must be in proc-macro crate Attribute macro defined in normal crate Move attribute macro to separate proc-macro = true crate

Advanced Optimization

1. Procedural Macro Error Reporting with Span Positioning

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Error};

#[proc_macro_derive(Validate)]
pub fn derive_validate(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    let validations = match &input.data {
        Data::Struct(data) => {
            match &data.fields {
                Fields::Named(fields) => {
                    let field_checks: Vec<_> = fields.named.iter().map(|f| {
                        let field_name = f.ident.as_ref().unwrap();
                        let field_name_str = field_name.to_string();

                        let type_checks = if let Some(attr) = f.attrs.iter().find(|a| a.path.is_ident("validate")) {
                            let mut checks = Vec::new();
                            if let Ok(meta) = attr.parse_meta() {
                                if let syn::Meta::List(list) = meta {
                                    for nested in list.nested.iter() {
                                        match nested {
                                            syn::NestedMeta::Meta(syn::Meta::Path(path)) => {
                                                if path.is_ident("non_empty") {
                                                    checks.push(quote! {
                                                        if self.#field_name.is_empty() {
                                                            return Err(format!("Field '{}' must not be empty", #field_name_str));
                                                        }
                                                    });
                                                } else if path.is_ident("positive") {
                                                    checks.push(quote! {
                                                        if self.#field_name <= 0 {
                                                            return Err(format!("Field '{}' must be positive", #field_name_str));
                                                        }
                                                    });
                                                }
                                            }
                                            _ => {}
                                        }
                                    }
                                }
                            }
                            checks
                        } else {
                            vec![]
                        };

                        Ok(type_checks)
                    }).collect::<Result<Vec<_>, _>>();

                    match field_checks {
                        Ok(checks) => checks.into_iter().flatten().collect(),
                        Err(e) => return e.to_compile_error().into(),
                    }
                }
                _ => {
                    return Error::new_spanned(
                        name,
                        "Validate derive only supports structs with named fields",
                    ).to_compile_error().into();
                }
            }
        }
        _ => {
            return Error::new_spanned(
                name,
                "Validate derive only supports structs",
            ).to_compile_error().into();
        }
    };

    let expanded = quote! {
        impl #name {
            pub fn validate(&self) -> Result<(), String> {
                #(#validations)*
                Ok(())
            }
        }
    };

    expanded.into()
}

2. Declarative Macro Recursion and TT Chewing

macro_rules! impl_from_for_enum {
    (@inner $name:ident { $($variant:ident($inner:ty)),* $(,)? }) => {
        $(
            impl From<$inner> for $name {
                fn from(value: $inner) -> Self {
                    $name::$variant(value)
                }
            }
        )*
    };

    ($name:ident { $($variant:ident($inner:ty)),* $(,)?) => {
        impl_from_for_enum!(@inner $name { $($variant($inner)),* });
    };
}

impl_from_for_enum! {
    AppError {
        Io(std::io::Error),
        Serde(serde_json::Error),
        Http(reqwest::Error),
        Database(sqlx::Error),
    }
}

macro_rules! hash_map {
    (@single $($x:tt)*) => { () };
    (@count $($rest:expr),*) => { <[()]>::len(&[$(hash_map!(@single $rest)),*]) };

    ($($key:expr => $value:expr),* $(,)?) => {{
        let cap = hash_map!(@count $($key),*);
        let mut map = std::collections::HashMap::with_capacity(cap);
        $(
            map.insert($key, $value);
        )*
        map
    }};
}

let config = hash_map! {
    "host" => "localhost",
    "port" => "8080",
    "database" => "myapp",
};

3. Procedural Macro Incremental Compilation and Performance

use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;

#[proc_macro_derive(LargeDerive, attributes(large_attr))]
pub fn derive_large(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as syn::DeriveInput);

    let name = &input.ident;
    let generics = &input.generics;
    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

    let trait_impl = generate_trait_impl(&input);

    quote! {
        impl #impl_generics LargeTrait for #name #ty_generics #where_clause {
            #trait_impl
        }
    }.into()
}

fn generate_trait_impl(input: &syn::DeriveInput) -> proc_macro2::TokenStream {
    let name = &input.ident;

    match &input.data {
        syn::Data::Struct(data) => {
            match &data.fields {
                syn::Fields::Named(fields) => {
                    let field_count = fields.named.len();
                    if field_count > 50 {
                        return syn::Error::new_spanned(
                            name,
                            format!("Struct has {} fields, consider splitting into smaller structs", field_count),
                        ).to_compile_error();
                    }

                    let field_names: Vec<_> = fields.named.iter()
                        .filter_map(|f| f.ident.as_ref())
                        .collect();

                    let field_strs: Vec<String> = field_names.iter().map(|f| f.to_string()).collect();

                    quote! {
                        fn field_count() -> usize {
                            #field_count
                        }

                        fn field_names() -> Vec<&'static str> {
                            vec![#(#field_strs),*]
                        }
                    }
                }
                _ => quote! { fn field_count() -> usize { 0 } },
            }
        }
        _ => quote! { fn field_count() -> usize { 0 } },
    }
}

Comparison

Dimension macro_rules! Derive Macro Attribute Macro Function-like Proc Macro C++ Templates
Definition macro_rules! #[proc_macro_derive] #[proc_macro_attribute] #[proc_macro] template<>
Input type Pattern matching TokenStream TokenStream×2 TokenStream Type parameters
Output type Template replacement TokenStream TokenStream TokenStream Types/functions
AST access ❌ None ✅ syn parsing ✅ syn parsing ✅ syn parsing ❌ None
Error reporting ⚠️ Poor ✅ Span positioning ✅ Span positioning ✅ Span positioning ❌ Very poor
Debugging cargo expand cargo expand cargo expand cargo expand Compile errors
Learning curve ⭐ Medium ⭐ Steep ⭐ Steep ⭐ Steep ⭐ Very steep
Use cases DSL/templates Auto-impl traits Decorators/code injection Custom syntax Generic algorithms
Separate crate ❌ Not needed ✅ Required ✅ Required ✅ Required ❌ Not needed
Compile speed ⭐ Fast ⭐ Slow ⭐ Slow ⭐ Slow ⭐ Very slow
Type safety ❌ None ⚠️ Post-expansion ⚠️ Post-expansion ⚠️ Post-expansion ✅ Compile time

Summary: The core advantage of Rust macro metaprogramming lies in compile-time code generation — from declarative macro template replacement to procedural macro AST transformation, macros let you eliminate repetitive code without sacrificing runtime performance. 2026 production practice: Use macro_rules! for DSLs and templates → Derive macros for auto-implementing traits → Attribute macros for decorators → Function-like procedural macros for custom syntax → syn+quote for complex code generation. The key insight: understand macro boundaries — macros are code generators, not runtime abstractions; if generics can solve it, don't use macros; if traits can solve it, don't use macros; macros are the weapon of last resort.


  • JSON Formatter: /en/json/format — Format macro-generated JSON configuration code
  • Code Formatter: /en/dev/code-formatter — Format cargo expand output for macro-expanded code
  • Hash Calculator: /en/encode/hash — Calculate hashes for API keys or configuration files

Try these browser-local tools — no sign-up required →

#Rust宏#声明宏#过程宏#元编程#代码生成#2026#编程语言