When copy-paste stops working
You're building a configuration loader. You need to parse integers, floats, and booleans from strings. You write parse_int, then parse_float, then parse_bool. The code is identical except for the type name. You copy-paste the function body three times. Two weeks later, you find a bug in the error handling. You fix parse_int. You forget parse_float. The bug lives on. You need a way to write the logic once and generate the functions automatically. Rust gives you macro_rules! for this. It lets you write code that writes code, checked by the compiler before your program even runs.
Macros are recipes for code
Think of a function as a machine that takes inputs and produces outputs while your program runs. A macro is a machine that takes inputs and produces source code while your program compiles. The compiler runs the macro, inserts the generated code into your file, and then compiles the result. If the generated code is wrong, the compiler rejects it just like any other error.
Macros let you define patterns of code and fill in the blanks. You describe the shape of the input, and you provide the template for the output. The compiler matches the input against your pattern and generates the code. Treat macros as code generators, not runtime helpers. The compiler does the work, not your program.
A minimal macro
Here is a macro that builds a vector from a list of expressions. It mimics the standard library's vec! macro.
/// Create a Vec from a comma-separated list of expressions.
macro_rules! my_vec {
// Match a comma-separated list of expressions.
// $x captures each expression.
// $( ... ),* repeats the capture zero or more times.
( $( $x:expr ),* ) => {
// The expansion: a block that builds a Vec.
{
let mut temp = Vec::new();
// Repeat the push for each captured expression.
$(
temp.push($x);
)*
// Return the vector.
temp
}
};
}
fn main() {
// Expand to code that creates a Vec and pushes 1, 2, 3.
let v = my_vec!(1, 2, 3);
println!("{:?}", v);
}
Run this and you'll see the macro expands to valid Rust code before main ever executes. The compiler sees the expanded block, not the macro call.
Anatomy of macro_rules!
The syntax looks like a mix of regex and template literals. macro_rules! starts the definition. The name follows. Inside the braces, you write "arms". Each arm has a pattern and an expansion, separated by =>. The pattern describes what input the macro accepts. The expansion is the code that gets generated.
Patterns and fragments
The pattern ( $( $x:expr ),* ) uses fragment specifiers to match syntax. Fragment specifiers tell the macro what kind of code to capture.
exprmatches any expression, like1 + 2orfoo().identmatches a single identifier, likemy_var.tymatches a type, likei32orVec<String>.pathmatches a path, likestd::vec::Vec.blockmatches a block of code{ ... }.stmtmatches a statement.itemmatches a function or struct definition.ttmatches a single token tree, which is a raw chunk of syntax.
Use expr when you need to capture a value or computation. Use ident when you need a name. Use ty when you need a type. Use tt when you need to capture arbitrary syntax that doesn't fit other categories, but be careful because tt can match things you didn't expect. Master the fragment specifiers. They are the vocabulary of macro patterns.
Repetition
Repetition operators let you match lists. $( ... )* matches zero or more repetitions. $( ... )+ matches one or more. You can add a separator after the repetition pattern. $( $x:expr ),* matches a comma-separated list. The comma is part of the pattern, not the repetition. This means the macro expects commas between items. If you omit the separator, the macro matches a sequence of items without commas. You can also nest repetitions, though nested repetitions are complex and rarely needed.
Convention aside
When you define a macro in a library crate, add #[macro_export] to make it available to users of the crate. Without this attribute, the macro is private to the crate. Convention also suggests documenting macros with /// doc comments, just like functions. Macros can have documentation, and users will read it.
Real-world pattern matching
Real macros often have multiple arms to handle different call signatures. The compiler tries each arm in order until one matches. This lets you write a macro that accepts optional arguments.
/// Unwrap a Result, panicking with a custom message if it's an Err.
macro_rules! try_unwrap {
// Match a value and a message string.
( $val:expr, $msg:expr ) => {
match $val {
Ok(v) => v,
Err(e) => panic!("{}: {:?}", $msg, e),
}
};
// Match a value alone. Falls back to this arm if the first doesn't match.
( $val:expr ) => {
match $val {
Ok(v) => v,
Err(e) => panic!("Unexpected error: {:?}", e),
}
};
}
fn main() {
let result: Result<i32, &str> = Ok(42);
// Expands to the first arm.
let x = try_unwrap!(result, "Failed to get number");
println!("{}", x);
let bad: Result<i32, &str> = Err("oops");
// Expands to the first arm, then panics.
// let y = try_unwrap!(bad, "Bad parse");
}
Multiple arms give you flexibility. The compiler picks the first match, so order matters. Put more specific arms before general ones.
Pitfalls and hygiene
Macros expand to code. If the generated code is invalid, you get standard compiler errors. The error points to the macro call, but the message might refer to the expanded code. This can be confusing. If you pass a type where an expression is expected, the pattern fails. The compiler says "no rules expected this token in macro call". If your macro generates code that violates type rules, you get E0308 (mismatched types). The error points to the macro invocation. The compiler shows the expanded code in the error message if you use cargo expand or newer rustc diagnostics.
One of Rust's superpowers is macro hygiene. When a macro generates code, it doesn't pollute the surrounding scope. If you define a variable temp inside a macro, it doesn't clash with a temp in the calling function. The compiler treats macro-generated names as distinct. This means you can write macros without worrying about accidentally shadowing variables in the user's code. Hygiene also means captured variables refer to the call site, not the definition site. If you capture $x, it resolves to the x where the macro is called.
Macros don't evaluate arguments. They capture tokens. my_vec!(foo()) captures foo() as tokens. It doesn't call foo until the expanded code runs. This is different from functions. Functions evaluate arguments before the body runs. Macros capture the syntax and generate code that evaluates later. Expand the macro mentally. If you can't trace the expansion, the macro is too complex.
When to use macros
Use macro_rules! when you need to eliminate repetitive code patterns or create domain-specific syntax that the compiler must see. Use functions when you need to perform calculations or logic at runtime; macros cannot replace runtime behavior. Use #[derive] macros when you want to attach trait implementations to data structures automatically. Use procedural macros when macro_rules! is too restrictive and you need to manipulate the abstract syntax tree directly, though procedural macros require a separate crate and are harder to debug.
Start with functions. Reach for macros only when the compiler forces you to generate code.