Rust's Macro System
Rust's macro system lets you write code that writes code, expanding at compile time into ordinary Rust before the compiler ever checks types.
This page is the conceptual map for the whole macros section. It explains what a macro fundamentally is, how the two macro families differ, and why hygiene is the property that makes macros safe to share across crates instead of a source of mysterious bugs. The rest of this section's pages, from macro_rules! mechanics to writing full procedural macro crates with syn and quote, all build on the model described here.
Summary
- A Rust macro is a compile-time program that consumes source tokens and emits new source tokens before the normal compiler pipeline runs.
- Insight: Macros let you eliminate repetitive boilerplate and build small domain-specific syntax that a plain function, which only operates on values, cannot express.
- Key Concepts: token stream, macro expansion, declarative macro, procedural macro, hygiene, call site versus definition site.
- When to Use: Repetitive trait implementations, embedding a small DSL, generating code from a struct's shape (like
derive), or wrapping a repeated syntactic pattern that functions cannot abstract over. - Limitations/Trade-offs: Macros trade readability and tooling support (debuggability, error clarity, IDE completion) for expressive power, and should be a fallback after functions and generics are ruled out.
- Related Topics: abstract syntax trees, compiler passes, trait derivation, domain-specific languages.
Foundations
A macro in Rust is fundamentally different from a function.
A function takes values and returns a value at runtime.
A macro takes source code, represented as a stream of tokens, and produces more source code, and it does this entirely during compilation.
By the time your program actually runs, every macro call has already been replaced by whatever Rust code it expanded into.
This is why macros can do things functions cannot, such as accept a variable number of arguments of different types, generate new function or struct names, or implement a trait for a struct just by inspecting its field list.
Rust has two macro families, and understanding which one you are looking at is the first skill this section builds.
Declarative macros, written with macro_rules!, work by pattern-matching the tokens passed to the macro against one or more "arms," similar to a match expression but operating on syntax instead of values.
Each arm has a pattern and a template, and the first pattern that matches determines what code gets substituted in.
Procedural macros are different in kind, not just degree: they are ordinary Rust functions, compiled into their own special crate, that receive a TokenStream as input and return a TokenStream as output.
Because a procedural macro is just Rust code with a TokenStream in and a TokenStream out, it can do anything a Rust program can do, including parsing the input into a structured syntax tree and reasoning about it before generating output.
A useful analogy is the difference between a fill-in-the-blank template and a compiler plugin.
macro_rules! is the template: fast to write, limited to matching shapes it was told to expect, and unable to truly analyze what it is given.
A procedural macro is the compiler plugin: far more work to write, but capable of arbitrary analysis and code generation because it has the full power of a Rust program available to inspect its input.
// A minimal declarative macro: matches one shape, expands to one expression.
macro_rules! square {
($x:expr) => {
$x * $x // substituted verbatim at the call site
};
}That snippet illustrates the core mechanic: $x:expr captures whatever expression the caller passes, and the template on the right is what the compiler sees after expansion, nothing more exotic than that.
Mechanics & Interactions
Macro expansion is a distinct phase in the compiler pipeline, and knowing where it sits explains a lot about how macros behave.
Rust's front end first tokenizes your source file, then macro expansion runs, rewriting macro invocations into their expanded token streams, and only after expansion is complete does the compiler build the abstract syntax tree it will type-check, borrow-check, and eventually lower to machine code.
This ordering has a direct consequence: a macro cannot see types, cannot see whether a variable is mutable, and cannot see anything about program semantics, because none of that information exists yet at expansion time.
A macro operates purely on syntax shape.
This is also why macro error messages can feel indirect: the compiler reports errors against the expanded code, so a type mismatch inside a macro's output often points at the macro definition or the call site in a way that takes a moment to map back to what you actually wrote.
Expansion itself proceeds outside-in and can recurse: a macro invocation can expand into code that contains further macro invocations, and the compiler keeps re-expanding until no macro calls remain, bounded by a configurable recursion limit to catch infinite expansion.
Hygiene is the mechanism that makes this recursive, textual-feeling substitution actually safe.
Without hygiene, a macro that internally declares a variable named x could silently shadow a variable also named x in the caller's code, because naive text substitution has no concept of scope.
Rust's macro_rules! macros are hygienic by default: identifiers introduced inside the macro body live in a separate "syntax context" from identifiers the caller passes in, so a macro-internal x and a caller's x never collide even though they share a spelling.
This is what makes it safe to call a macro without reading its implementation first, the same trust a function call gives you.
Procedural macros do not get this guarantee for free.
Because a proc macro builds its output token by token using libraries like quote, hygiene depends on how spans are assigned to generated identifiers, and a careless proc macro author can generate identifiers that do leak into caller scope.
This is one of the sharpest edges in the macro system: declarative macros are hygienic by construction, procedural macros are hygienic only if their author is careful about it.
A second mental model worth holding is call site versus definition site.
A macro's definition site is where its pattern and template are written, while the call site is where it gets invoked, and hygiene rules generally attach a macro's own generated names to its definition site while letting names the caller explicitly passed in resolve at the call site, which is exactly the split that prevents accidental capture in either direction.
Advanced Considerations & Applications
At scale, the choice between declarative and procedural macros becomes an architecture decision, not just a syntax preference.
Declarative macros live inline in the crate that uses them and compile fast, which makes them the right default for repetitive-but-simple patterns: implementing the same trait for a list of types, building a small builder-style DSL, or wrapping a repeated assertion pattern in tests.
Procedural macros require a separate proc-macro = true crate, pull in a heavier dependency chain (syn, quote, proc-macro2), and run as compiler plugins that execute on the host machine during the build of any crate that uses them, which adds real compile-time cost across a workspace.
That cost buys real capability: a procedural macro can parse an entire struct definition, inspect field names, types, and attributes, and generate a full trait implementation. This is exactly how #[derive(Serialize)] and similar ecosystem-standard derives work, and no amount of macro_rules! cleverness can replicate that level of structural analysis, because declarative macros never build a real syntax tree.
Both macro kinds interact with the broader toolchain in ways worth knowing about.
cargo expand shows you the fully expanded source for either kind, which is the single most useful debugging tool in this whole system because it turns an abstract "what did my macro produce" question into concrete Rust you can read.
Compile time is a real, measurable cost that grows with how many crates in a dependency graph use heavy procedural macros, since each one adds its own compilation unit and analysis pass to every downstream build.
Security reasoning also differs between the two: a procedural macro is arbitrary compiled Rust code that runs during your build, so adding one as a dependency means trusting that code to execute on your machine at compile time, a boundary a declarative macro, which only rewrites tokens against a fixed matcher, does not cross.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
macro_rules! | Fast to write, hygienic by default, no extra crate | Cannot inspect types or build a real AST, limited pattern matching | Repetitive syntax templates, small DSLs, test helpers |
| Procedural macro | Full Rust program over the input, can parse and analyze structure | Separate crate, slower builds, hygiene is the author's responsibility | #[derive] implementations, attribute-driven codegen, framework boilerplate |
| Plain function or generic | Best error messages, full IDE support, no expansion step | Cannot generate new syntax or vary by caller's literal token shape | Anything expressible without touching syntax itself |
The macro system has also evolved alongside the rest of the language: modern procedural macro crates lean on syn's typed parsing and quote's token-quoting to avoid hand-rolled token manipulation, which has made procedural macros dramatically more approachable than they were in Rust's earlier compiler-plugin era.
Common Misconceptions
- "Macros run at runtime like a function call" - they do not; every macro is fully expanded during compilation, and the compiled binary contains only the expanded code, with no trace of the macro invocation itself.
- "Declarative macros are just simpler procedural macros" - they are a different mechanism entirely, pattern-matching tokens against fixed shapes rather than running an arbitrary program over an input stream.
- "All macros are hygienic" -
macro_rules!macros are hygienic by default, but procedural macros only get that property if their author assigns spans carefully when generating identifiers. - "Macros are always the powerful choice, so prefer them" - macros sacrifice error clarity, IDE support, and debuggability, and idiomatic Rust reaches for functions and generics first, macros only when those genuinely cannot express the pattern.
- "A macro can see the types of what it's given" - macro expansion happens before type checking, so a macro can only reason about token shape, never about a value's actual type.
FAQs
What exactly is a "token stream"?
A token stream is the sequence of lexical tokens (identifiers, punctuation, literals, delimited groups) that make up a piece of source code, before it has been parsed into an AST.
- Declarative macros match token streams against declared patterns.
- Procedural macros receive a token stream as their literal function input and return one as output.
Why do macro error messages sometimes point to a confusing location?
Because the compiler type-checks and borrow-checks the expanded code, not the macro invocation you wrote, so an error can surface at a line inside the macro's template that you never directly typed. cargo expand is the standard way to see exactly what code produced the error.
Is `macro_rules!` older or simpler than procedural macros?
It is simpler in mechanism, not necessarily older in capability terms, and both remain fully supported, idiomatic parts of the language. Each targets a different problem: macro_rules! for syntactic templates, procedural macros for structural code analysis and generation.
How does hygiene actually prevent variable capture?
Each identifier a macro introduces carries a syntax context tied to where it was defined, so an identifier named x generated inside the macro body is treated as distinct from an identifier named x supplied by the caller, even though both are spelled the same way.
Can a macro call another macro?
Yes, macro expansion is recursive, and the compiler keeps re-expanding until no macro invocations remain, subject to a configurable recursion limit that guards against runaway expansion.
Do procedural macros run when I compile my code, or when my program runs?
They run on the host machine during compilation of any crate that invokes them, as part of the build itself, never at runtime in the compiled binary.
Why do procedural macros need their own crate?
The compiler needs to build and execute the macro's code before it can compile the crate that calls it, so proc macro definitions must live in a crate marked proc-macro = true that is compiled and run as a separate, earlier step.
What are `syn` and `quote` for, conceptually?
syn parses a token stream into a typed Rust syntax tree so a procedural macro can reason about structure instead of raw tokens, and quote does the reverse, turning Rust-like syntax back into a token stream to return as output.
Is it true that macros always make code harder to read?
Not inherently, but a poorly scoped macro can hide control flow or generate surprising code, so the honest trade-off is expressive power against transparency, which is why the section's best-practices guidance favors functions and generics first.
Do macros affect runtime performance?
Not directly, since a macro's output is ordinary Rust code that gets compiled and optimized like anything else, so any performance characteristics come entirely from what the expanded code does, not from the fact that a macro produced it.
When should I reach for a macro instead of a generic function?
Reach for a macro only when the pattern varies at the syntax level in a way a generic cannot express, such as a variable number of differently typed arguments, generating new identifiers, or inspecting a struct's shape to implement a trait automatically.
What is the difference between "definition site" and "call site"?
The definition site is where the macro's pattern and template are written, and the call site is where the macro is actually invoked. Hygiene rules generally resolve macro-internal names at the definition site while letting caller-supplied tokens resolve where the caller wrote them.
Can a macro fail to compile in a way a function cannot?
Yes, a macro can produce a pattern-match failure if no arm matches the input tokens, an error distinct from any type or borrow error, and this failure happens during expansion, before the compiler has even attempted to type-check the generated code.
Related
- Macros Basics - hands-on walkthrough of common macro patterns that builds on the mental model here.
- macro_rules! - deep dive on declarative macro syntax, matchers, and repetition.
- Procedural Macros Overview - the three procedural macro kinds and how they compile and run.
- syn & quote - the parsing and token-generation libraries procedural macros are built on.
- Macros Best Practices - when to reach for a macro versus a function or generic.
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.