Skip to content
← All articles

Why macros exist: code that writes code

The three things a function fundamentally cannot do, why macro_rules! is not textual substitution, and why the community reaches for generics first.

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, :ty suffix 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.