Macros
What’s with the bangs?
Every Rust program you’ve written in this book started with println!. You’ve
seen vec!, format!, assert_eq!, dbg!. Every one of them ends in !,
and that bang is the giveaway: these aren’t functions. They’re macros.
A macro is code that writes code. You feed it some syntax; it expands into a chunk of Rust at compile time, which is then compiled normally. There are two big reasons macros exist:
- Variable numbers of arguments. Functions have a fixed signature.
println!("hi {} {} {}", a, b, c)takes three args; the next call might take none. Only a macro can pull that off. - Compile-time format-string checking.
println!reads the format string at compile time and ensures the placeholders match the arguments. A function couldn’t see the format string until runtime.
Rust actually has two kinds of macros:
- Declarative macros —
macro_rules!. Pattern-match input syntax, emit output syntax. The everyday workhorse. - Procedural macros — full Rust functions that run at compile time and
rewrite syntax. Includes
#[derive(...)], attribute macros like#[tokio::main], and function-like macros likesqlx::query!.
We’ll spend most of this chapter on declarative macros because that’s what you’ll likely write. Procedural macros get a glance at the end.
Your first macro
The classic small one: a say_hi! macro that takes any number of names and
prints a greeting for each.
Let’s pick that apart, because the syntax is weird the first time.
macro_rules! say_hi— declares a macro namedsay_hi.( $( $name:expr ),* $(,)? )— the matcher. It matches the input.$name:exprsays “capture an expression, call it$name.”$( ... ),*says “repeat the inside, comma-separated, zero or more times.”$(,)?allows an optional trailing comma. (Quality-of-life touch.)
=>separates the pattern from the body.$( println!("hi, {}!", $name); )*— the expansion. The$( ... )*repeats the body once per captured$name, substituting$nameeach time.
That’s the whole declarative-macro mental model: pattern → expansion, with metavariables and repetition.
Designators — what kind of thing am I capturing?
The bit after the : in $name:expr is called a designator. It tells
the macro engine what kind of syntax to expect. The common ones:
| Designator | Captures |
|---|---|
expr | an expression (2 + 2, "hi", f(x)) |
ident | an identifier (foo, MyType) |
ty | a type (i32, Vec<String>) |
pat | a pattern (the kind that goes in match) |
block | a block ({ ... }) |
item | a top-level item (a function, a struct, a use) |
path | a path (std::collections::HashMap) |
tt | a token tree — the catch-all, matches anything balanced |
literal | a literal (42, "hi", true) |
You pick the most specific designator that fits, because it gives the best error messages when callers mess up.
A more useful macro — min! for any number of args
Rust’s standard library has std::cmp::min(a, b) for two arguments. What if
we want the minimum of an arbitrary number?
A couple of new things in here:
- The double braces
{{ ... }}— the outer pair is part of the macro expansion syntax (every macro arm body is wrapped in some kind of bracket), the inner pair is an actual Rust block. The whole macro invocation becomes an expression that returnsm. - The matcher requires at least one argument:
$first:exprthen zero-or-more$rest. If you callmin!()it won’t compile.
Try it: change one argument to a different type than the others (min!(1, "x"))
and see how the compiler localises the error. Macros aren’t typed — they’re
just syntax substitution — but the expanded code is typechecked normally.
A tiny DSL — hash_map!
Macros really shine when you want literal-syntax for things the language
doesn’t have built-in. Rust ships vec![1, 2, 3] but not hash_map!. Let’s
write it.
Notice we used {...} braces around the invocation instead of (...). Macros
accept any matching bracket pair — (), [], or {} — by convention
vec![...] uses [], println!(...) uses (), hash_map!{...} uses {}.
Pick whichever reads best for the call site.
Hygiene — the small thing that saves your life
C macros are notoriously dangerous because they’re textual substitution. If
the macro uses a variable name x internally, and the caller also has an
x, kaboom.
Rust macros are hygienic. Identifiers introduced inside the macro live in their own namespace; they can’t accidentally collide with identifiers at the call site. Watch:
The macro’s internal x and the caller’s x don’t collide. There’s an
escape hatch ($crate, $tt-tricks, paste!) for the rare cases you do
want to share, but by default hygiene saves you from a whole category of
bugs.
Where to write your macros
A macro defined in module foo is, by default, visible only within foo.
To export it from your crate so callers can use it, you put
#[macro_export] on it and they import it as crate::my_macro!. Most
real-world macros live next to the type they’re paired with.
#[macro_export]macro_rules! my_macro { /* ... */ }Procedural macros — a glimpse
The other half of the macro world is procedural macros, which are actual Rust functions that run at compile time. You don’t write them often; you consume them constantly.
Three flavours:
- Derive macros —
#[derive(Debug, Clone, Serialize)]. Generateimplblocks for your type based on its fields. You used#[derive(Debug, Clone, PartialEq)]back in the types chapter. - Attribute macros —
#[tokio::main],#[test],#[route("/users")]. They wrap or transform the thing they’re attached to. - Function-like macros —
sqlx::query!("SELECT …"),html!{...}. Look like declarative macros at the call site, but the body is real Rust running at compile time. This is howsqlxcan typecheck SQL queries against your actual database schema at compile time.
Writing your own procedural macro requires a separate crate with
proc-macro = true, the syn and quote crates, and a Saturday afternoon
you’ll never get back. It’s worth doing once to understand the model, but
for most application code, declarative macros are plenty.
What you take away
- Macros end with
!and are syntactic substitution at compile time. macro_rules!is pattern-match-on-syntax → emit-syntax. Designators likeexpr,ident,tyconstrain what you capture.$(...)*and$(...)?handle repetition.- Rust macros are hygienic — internal names can’t collide with the caller’s.
- Procedural macros (derives, attributes, function-like) are full Rust code running at compile time. You’ll use them constantly, write them rarely.
And that’s the book
You’ve now seen every major concept in Rust: ownership, borrowing, types,
traits, enums, pattern matching, error handling, iterators, collections,
lifetimes, smart pointers, concurrency, and macros. From here, the path
forward is to build something. The standard library docs are excellent;
docs.rs has hand-rolled docs for every crate on crates.io. The official
Rust Book covers some patches we glossed over (the module system, testing
conventions, generics syntax) in much more depth.
Mostly, though: write Rust. Make small things. Make a TODO app. Make a tiny CLI. Make a parser for some little file format you care about. The compiler will keep teaching you long after this book stops.
Thanks for reading. Go forth, oxidize.