You have been calling macros since the first line of Rust you ever wrote.
println!, vec!, assert_eq!, format!, matches!, write!, todo! — all
macros, all marked by that exclamation point, and until now you have had no
reason to ask why they need one.
This track answers that, and the answer is more interesting than “it’s a shorthand”.
Three things a function fundamentally cannot do
Not “would be awkward at”. Cannot.
1. Take a variable number of arguments of differing types.
println!("{} scored {} in {}", name, points, game);
println!("done");
Rust has no varargs. A function’s arity is part of its type, and so is the type
of every parameter. There is no signature you can write for println — not with
generics, not with trait objects, not with a slice — that accepts two arguments
here and four there, of unrelated types, and also type-checks the format string
against them at compile time. println! does the last part, which is why a
mismatched {} is a compile error rather than a runtime one.
2. Accept syntax that is not an expression.
vec![0u8; 1024]
matches!(x, Some(n) if n > 3)
[0u8; 1024] inside vec! is not an expression; it is a piece of bespoke
syntax that vec! defines for itself. Some(n) if n > 3 is a pattern with a
guard — a thing that can only otherwise appear in a match arm. A function
call cannot receive a pattern, because patterns are not values. A macro can,
because a macro receives syntax.
The same freedom is why every macro can accept a trailing comma if its author
bothers to allow it, and why vec![] with nothing inside is legal.
3. Generate items.
A function produces a value at run time. A macro produces source: functions,
structs, enums, impl blocks, modules, other macro definitions. #[derive(Debug)]
writes an entire impl Debug for YourType that nobody typed. thread_local!
expands into a static plus an accessor type plus several impls. No function call
can do this, because by the time a function exists there is nothing left to
generate.
Those three capabilities are the entire justification for the macro system. Every macro you will ever write should be doing at least one of them.
The C programmer’s trap
If you come from C, “macro” means #define, and #define is textual
substitution: the preprocessor is a separate program that runs before the
compiler, does not know what a Rust — or C — expression is, and cheerfully
produces garbage.
#define DOUBLE(x) x * 2
DOUBLE(1 + 2) /* becomes 1 + 2 * 2, which is 5 */
Rust’s macro_rules! is not textual substitution, and this is the single
most important correction to make before going further. A macro_rules! macro
operates on token trees: the source has already been lexed, and the
delimiters (), [], {} have already been paired into a tree structure.
The macro matches against that tree and emits a new tree.
Being past the lexer buys real guarantees. A macro can never split a string literal in half, never produce an unbalanced bracket, never accidentally glue two identifiers into one. And when a macro captures a fragment as an expression rather than as raw tokens, that fragment stays welded together as one unit — which is exactly the bug C cannot avoid. Item 18.4 makes you measure the difference with a number.
Being past the lexer is not the same as being past the parser, though. Macros
run before type checking and before name resolution. A macro has no idea what
type anything is, cannot ask whether a type implements a trait, and cannot see
the definition of a struct it was handed the name of. That limit is permanent for
macro_rules! and it is the reason procedural macros exist (18.16, 18.17).
If you come from Python or Java
Then you have no prior model at all, which is honestly the easier starting point.
The nearest analogy is a code generator that runs as part of compilation, whose input is the parenthesised text you wrote and whose output is spliced back in where you wrote it. Java’s annotation processors and Python’s decorators are cousins — but a decorator runs at run time and receives a live function object, while a Rust macro runs at compile time and receives syntax. That difference means a Rust macro costs nothing at run time and can produce things (new types, new impls) that a decorator cannot.
The vocabulary you need
-
Invocation — the call site:
vec![1, 2, 3]. The!is what marks it as an invocation rather than a function call, and it is there so that a human reading the code knows immediately that arbitrary syntax and arbitrary expansion are in play. -
Matcher — the pattern half of a rule, left of
=>. Matches token trees. -
Transcriber — the output half, right of
=>. Emits token trees. - Expansion — the act of replacing the invocation with the transcriber’s tokens, and the resulting code.
-
Fragment specifier — the
:expr,:ident,:tysuffix that says what kind of syntax a captured piece must be (18.3). - Hygiene — the machinery that keeps a macro’s local variables from colliding with the caller’s (18.11).
! therefore marks a compile-time syntactic transformation, not a runtime
call. Nothing is pushed on a stack; there is no println function to step
into. When a debugger drops you inside println!, you are inside the expansion.
Macros are not a first resort
This needs saying now, before you learn how to write them, because the failure mode of a macros chapter is a learner who reaches for a macro every time something repeats.
The Rust community’s ordering is: traits and generics first, macro_rules!
second, a procedural macro only when you must inspect structure. A generic
function with a trait bound gets you type checking, IDE support, go-to-definition,
inlay hints, doc links, and error messages that point at your code. A macro
degrades every one of those. rust-analyzer in particular is noticeably worse
inside macro expansions, and that cost lands on every future reader of the code,
not just on you.
Reach for a macro when you hit one of the three walls above — varargs, non-expression syntax, item generation — and not before. “This is repetitive” is usually a sign that a trait with a blanket implementation was the better answer.
Item 18.20 revisits the ladder once you know enough to disagree with it.
Where this track goes
Items 18.2 to 18.15 build macro_rules! from the first rule up to a
compile-time expression evaluator, meeting hygiene, follow-set rules and the
recursion limit along the way. Items 18.16 to 18.20 are the honest inventory of
what macro_rules! cannot do and what procedural macros do about it — articles,
because a procedural macro physically cannot live in the same file as the code it
transforms, for a reason worth understanding.
Items 18.21 to 18.27 cross the C boundary, and they are far more hands-on than
you would expect: because the Rust standard library links the platform C runtime,
a plain single-file build can declare and call strlen, qsort and snprintf
with no crates, no build script and no linker flags. Every FFI rule in this track
is one you can execute.
Item 18.28 is the capstone, and it is not about either topic. It is about using the type system so that the compiler enforces your design instead of your documentation.