Skip to content

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:

  1. 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.
  2. 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 macrosmacro_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 like sqlx::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.

🦀 main.rs

Let’s pick that apart, because the syntax is weird the first time.

  • macro_rules! say_hi — declares a macro named say_hi.
  • ( $( $name:expr ),* $(,)? ) — the matcher. It matches the input.
    • $name:expr says “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 $name each 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:

DesignatorCaptures
expran expression (2 + 2, "hi", f(x))
identan identifier (foo, MyType)
tya type (i32, Vec<String>)
pata pattern (the kind that goes in match)
blocka block ({ ... })
itema top-level item (a function, a struct, a use)
patha path (std::collections::HashMap)
tta token tree — the catch-all, matches anything balanced
literala 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?

🦀 main.rs

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 returns m.
  • The matcher requires at least one argument: $first:expr then zero-or-more $rest. If you call min!() 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.

🦀 main.rs

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:

🦀 main.rs

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:

  1. Derive macros#[derive(Debug, Clone, Serialize)]. Generate impl blocks for your type based on its fields. You used #[derive(Debug, Clone, PartialEq)] back in the types chapter.
  2. Attribute macros#[tokio::main], #[test], #[route("/users")]. They wrap or transform the thing they’re attached to.
  3. Function-like macrossqlx::query!("SELECT …"), html!{...}. Look like declarative macros at the call site, but the body is real Rust running at compile time. This is how sqlx can 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 like expr, ident, ty constrain 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.