Chapter 14: Macros and Metaprogramming
Rust’s macro system lets you extend the language at the syntactic level. Declarative macros match patterns and expand into new code; procedural macros let you inspect and transform syntax trees. Macros allow expressive libraries and safe abstractions while preserving compile time guarantees. This chapter explores macro_rules patterns, hygiene, procedural macros, derive systems, and guidance for choosing when macros are the right tool.
Declarative macros with macro_rules!
Declarative macros match token patterns and rewrite them using rules. The system resembles powerful pattern substitution with strict syntax matching. Each rule is a match arm. When a call fits a pattern the corresponding rule expands into its replacement. Declarative macros excel at small domain specific expressions, forwarding APIs, or repetitive boilerplate.
Basic pattern matching
A macro accepts input as tokens. You describe the structure with fragment specifiers such as $e:expr for expressions or $t:ty for types. The expansion can repeat fragments using * or +. The following macro computes a vector literal by capturing expressions and pushing them into a generated Vec.
macro_rules! collect_vec {
( $( $x:expr ),* ) => {
{
let mut v = Vec::new();
$( v.push($x); )*
v
}
};
}
fn main() {
let nums = collect_vec!(1, 2, 3, 4);
println!("{:?}", nums);
}
Patterns must match exactly. If a user writes input that does not match a rule the compiler reports an error at the call site.
Using repetition and separators
Repetition allows flexible input. You can specify separators inside the pattern to make syntax cleaner. The following example demonstrates a simple map literal macro that collects key value pairs.
macro_rules! simple_map {
( $( $k:expr => $v:expr ),* $(,)? ) => {
{
let mut m = std::collections::HashMap::new();
$( m.insert($k, $v); )*
m
}
};
}
fn build() {
let m = simple_map!(
"alice" => 1,
"bob" => 2,
);
println!("{:?}", m);
}
The optional trailing comma pattern $(,)? improves ergonomics and mirrors Rust’s built in collection syntax.
Hygiene and pattern design
Macro hygiene ensures that names inside a macro do not accidentally collide with names in the caller’s scope. Rust assigns each macro expansion its own namespace for generated identifiers. This prevents accidental capture of variables or shadowing unless you explicitly opt in. Hygienic expansion is crucial for reliability and maintainability.
Identifier hygiene
When you write a macro that introduces new bindings such as let temp = …, the binding receives a fresh hygienic name. The user cannot clash with it by naming their variable temp. Conversely, a macro can refer to identifiers from its definition scope when needed. This lets you reuse helper functions without exposing them directly.
macro_rules! safe_add {
( $a:expr, $b:expr ) => {{
// `sum` is hygienic and cannot collide with caller variables.
let sum = $a + $b;
sum
}};
}
fn main() {
let sum = 5; // no conflict with the macro's internal `sum`
let v = safe_add!(2, 3);
println!("{}", v);
}
Hygiene prevents a common class of errors found in older macro systems. You can assume that macro internals remain isolated unless you explicitly expose names.
Designing robust macro patterns
Good macro patterns anticipate common user input. Match the simplest shapes first. Handle trailing commas. Avoid ambiguous forms that confuse parsing. When building a mini language with macros, treat patterns as grammar rules. Ensure each fragment has a clear role. Prefer explicit tokens rather than overloading punctuation with multiple meanings.
Procedural macros
Procedural macros operate on Rust’s syntax tree. They receive a TokenStream as input and produce another TokenStream as output. This permits arbitrary code generation, inspection, and transformation. Procedural macros live in their own crate type with proc-macro = true in Cargo.toml. They are compiled as compiler plugins and linked by crates that use them.
Structure of a procedural macro crate
A procedural macro crate usually depends on syn for parsing and quote for emitting Rust code. The following example demonstrates a tiny attribute macro that prints the function name at runtime. The attribute wraps the function body in generated code.
// my_macro/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn trace(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let name = &input.sig.ident;
let block = &input.block;
let gen = quote! {
fn #name() {
println!("entering {}", stringify!(#name));
#block
}
};
gen.into()
}
Consumers enable the macro by adding the proc macro crate as a dependency, then using the attribute in their code. The macro runs during compilation and emits the modified function.
Parsing and generating structures
The syn crate maps Rust syntax to rich types. You can pattern match on ItemFn, Expr, Type, and more. At expansion time you create a new syntax tree with quote! and return it. This gives you expressive metaprogramming capabilities. Keep expansions predictable so that generated code remains readable when developers inspect it with compiler output tools.
Derive, attribute, and function-like macros
Procedural macros come in three flavours. Derive macros attach to items such as structs. Attribute macros attach to items and wrap or replace them. Function-like macros resemble calls like sql!( … ) and expand to code fragments. Each flavour serves a different role.
Derive macros
Derive macros generate implementations for traits. The standard library provides several such as Debug, Clone, and PartialEq. Custom derive macros let libraries expand boilerplate such as serializers, builders, or database models. A typical derive macro accepts a struct or enum and emits an impl block with generated methods.
use serde::Serialize;
#[derive(Serialize)]
struct Point {
x: i32,
y: i32,
}
Derive macros shine when multiple types need similar implementations. They centralise logic and reduce repetition in user code.
Attribute macros
Attribute macros attach to items such as functions, modules, or impl blocks. They can wrap the item, modify signatures, or expand decorators. For example you might write #[trace] to instrument functions or #[route] to bind a function to a web handler. Attribute macros are flexible but require careful design to keep generated code explicit and understandable.
#[timeit]
fn heavy_work() {
// body …
}
The macro behind #[timeit] might measure execution time by expanding to code that records timestamps and prints results.
Function-like macros
Function-like macros look like regular function calls but operate at compile time. They often provide small DSLs for expressing patterns such as SQL queries, embedded resources, or bitfield definitions. They receive their input tokens and emit new Rust code.
let query = sql!(SELECT * FROM users WHERE id = 1);
The macro expands to a struct or function call that encodes the query safely. Function-like macros behave like textual invocations but benefit from Rust’s type system because they operate on structured tokens rather than plain strings.
When to use macros
Macros are powerful. Use them when regular functions or generics cannot solve the problem with equal clarity. If you want to eliminate boilerplate, enforce patterns, embed declarative syntax, or perform compile time transformations, macros may help. If simple functions or traits suffice, prefer those instead because they are easier to reason about and often compile faster.
Guidelines for choosing macros
Reach for macros only when you need one of these capabilities. Each point reflects a situation where ordinary Rust constructs reach their limits.
Boilerplate removal: When multiple types need identical implementations and generics cannot express the pattern.
Domain specific syntax: When you want mini languages that map cleanly to Rust code such as parsers or schema definitions.
Compile time validation: When you want to detect errors early by inspecting tokens at build time; for example validating format strings or structured queries.
Code generation for external data: When you want to embed files or generate bindings rather than performing the work at runtime.
Extensibility: When you expose an API that benefits from declarative patterns for users; for example web frameworks or serialisation crates.
Understanding both declarative and procedural macros gives you a broad foundation for expressive metaprogramming in Rust. The best macros clarify code instead of obscuring it. When used thoughtfully they elevate libraries without sacrificing safety or readability.