Declarative Macros (macro_rules!)
A function takes values and returns a value. A macro_rules! macro takes syntax and produces code, before the compiler ever type-checks anything. That's the one capability functions and generics can't match: accepting a variable number of arguments, generating repetitive impl blocks, or building DSL-like syntax. It's also why macros should be a last resort, code that only exists after macro expansion is harder to read, harder to debug, and invisible to most tooling. This tutorial covers the mechanics and, just as importantly, when not to reach for one.
A Macro Matches Syntax Against Patterns
macro_rules! defines a set of rules, each pairing a pattern (a "matcher") against the code to emit when input matches it. The structure mirrors a match expression, but over token syntax rather than runtime values:
macro_rules! square {
($x:expr) => {
$x * $x
};
}
let n = square!(5); // expands to: 5 * 5
$x:expr is a metavariable: $x is the name, :expr is the fragment specifier declaring what kind of syntax it captures, here, any expression. At expansion, every $x in the body is replaced with whatever tokens were matched. The whole thing happens at compile time; by the time the code is type-checked, square!(5) is simply 5 * 5.
There's a subtle correctness trap here worth seeing early. square!(2 + 3) expands to 2 + 3 * 2 + 3 (which is 11, not 25) because the captured tokens are spliced in raw. Wrap metavariable uses in parentheses in the macro body to preserve grouping: ($x) * ($x) expands to (2 + 3) * (2 + 3).
Fragment Specifiers: What a Metavariable Can Capture
The specifier after the colon constrains what syntax a metavariable accepts, and determines how it can be used in the body. The ones you'll reach for most:
expr— an expression (5,a + b,foo())ty— a type (i32,Vec<String>)ident— an identifier (a variable, function, or type name)tt— a single "token tree", the most permissive, matches almost any single token or balanced-bracket grouppat— a pattern (the kind from amatcharm)literal— a literal value (42,"text")block— a brace-delimited block ({ ... })
macro_rules! make_getter {
($name:ident, $field:ident, $ret:ty) => {
fn $name(&self) -> &$ret {
&self.$field
}
};
}
// make_getter!(get_name, name, String) generates:
// fn get_name(&self) -> &String { &self.name }
Picking the right specifier matters: ident lets you use the captured token as a name to declare things (a function, a field access), which expr wouldn't allow. Reach for tt only when nothing more specific fits, its permissiveness means the macro accepts malformed input further before failing, with worse error messages.
Repetition: Handling a Variable Number of Arguments
The feature that genuinely can't be replicated with a function is matching a variable-length sequence. $( ... )* repeats a pattern zero or more times (+ for one-or-more), with an optional separator token between the ) and the *:
macro_rules! hashmap {
($($key:expr => $value:expr),* $(,)?) => {{
let mut map = std::collections::HashMap::new();
$(
map.insert($key, $value);
)*
map
}};
}
let scores = hashmap! {
"alice" => 85,
"bob" => 92,
};
The $($key:expr => $value:expr),* matcher captures a comma-separated list of key => value pairs. The body's $( map.insert($key, $value); )* then replays that repetition once per captured pair, emitting one insert call for each. This is exactly how vec![1, 2, 3] works internally, and it's the canonical reason to write a macro: a literal-collection constructor that takes any number of elements, which no function signature can express.
The trailing $(,)? is a small but important touch, it optionally matches a trailing comma (? means zero-or-one), so hashmap!{ a => 1, } with the dangling comma is accepted rather than rejected.
Hygiene: Macro-Introduced Variables Don't Leak
A real hazard in C-style textual macros is name capture: a variable the macro introduces silently clashing with one at the call site. Rust's macros are hygienic, identifiers a macro introduces live in their own syntactic context and can't collide with names in the caller's scope.
macro_rules! using_temp {
($e:expr) => {{
let result = $e; // this `result` is the macro's own
result * 2
}};
}
let result = 10; // the caller's `result`
let doubled = using_temp!(result); // expands using the caller's `result` (10), not the macro's
// doubled == 20; the two `result`s never interfere
The macro's internal let result and the caller's let result are genuinely different bindings despite the identical spelling, the macro can't accidentally clobber the caller's variable, and the caller can't accidentally reference the macro's internal one. This is a guarantee C preprocessor macros simply don't provide, and it removes an entire category of subtle bug.
When a Macro Is the Wrong Tool
Macros pay for their power with real costs: expanded code doesn't show up in source, IDE features (go-to-definition, autocomplete) work poorly inside them, error messages point at generated code, and a complex matcher is hard to follow. Before writing one, check whether something simpler does the job:
- Variable behavior over a known type? A generic function with a trait bound, from Generics and Trait Bounds Deep Dive, is clearer and fully visible to tooling.
- Repeated trait impls across types? A blanket impl often covers it without any macro at all.
- A variable number of arguments of the same type? Take a slice (
&[T]) or animpl IntoIteratorinstead, callers pass an array literal, no macro needed.
A macro earns its place when you genuinely need new syntax (a collection literal, a DSL), repetitive code that varies structurally in a way generics can't express (different field names, different type-level shapes), or true variadics over heterogeneous types. When the need is just "run this logic for any T," that's a function's job, reach for the macro only when no abstraction over values or types can express what you need.
Key Takeaways
- A
macro_rules!macro transforms syntax into code at compile time, matching input tokens against patterns the waymatchmatches values, which is what lets it do things functions can't (variadics, generating impls, new syntax). - Metavariables (
$x:expr) capture syntax fragments by specifier (expr,ty,ident,tt,pat, ...); pick the most specific specifier that fits, and parenthesizeexprcaptures in the body to avoid precedence surprises. $( ... ),*repetition matches and replays variable-length sequences, this is the genuinely unique capability (e.g.vec![...], ahashmap!literal). Add$(,)?to accept a trailing comma.- Rust macros are hygienic: identifiers a macro introduces can't collide with the caller's variables, eliminating the name-capture bugs C macros are prone to.
- Prefer a generic function, blanket impl, or slice/iterator parameter whenever one of them fits, reach for a macro only when you truly need new syntax or structural repetition no value/type abstraction can express.