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
- 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 - Procedural macro crate configuration is cumbersome: derive/attribute/function-like macros must be in a separate
proc-macro = truecrate, isolated from the main crate - TokenStream debugging relies entirely on cargo expand:
println!inside procedural macros produces no output; you can only usecargo expandoreprintln!to stderr - syn parsing of complex syntax is error-prone: Generic parameters, lifetimes, and where clauses require handling many edge cases
- 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(¶m_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+quotefor 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.
Recommended Online Tools
- JSON Formatter: /en/json/format — Format macro-generated JSON configuration code
- Code Formatter: /en/dev/code-formatter — Format
cargo expandoutput 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 →