Learning Rust · Part III · Chapter 17

Macros, code that writes code

A function takes values and gives back a value. A macro takes syntax and gives back syntax, before your program ever runs. That one shift, from values to code, is what lets println! accept any number of arguments and what lets #[derive] write a whole trait impl you never typed.

01

A call that bends the rules

You have used a macro on the very first page of this book and on nearly every page since: println!. Look closely at it and something is off. You can call it with one argument, or with a format string and four more, or with named fields, and it accepts them all. No Rust function can do that. A function fixes its arity (the number of parameters it takes) the moment you write its signature, and the compiler holds you to it. So println! is not a function. The exclamation mark is the tell: it marks a macro, and a macro plays by different rules.

Here is the gap, side by side. A small greet function takes exactly one name. Next to it, a greet_all! macro takes as many names as you hand it and writes one greet call per name. Read it once for shape, then press Run:

why_a_macro.rs
// A function fixes its arity: greet takes exactly one name, no more.
fn greet(name: &str) {
    println!("hello, {name}");
}

// A macro runs at compile time and works on syntax, not values, so it can
// take any number of arguments and write the calls for you. The matcher
// $($name:expr),* says "zero or more expressions separated by commas".
macro_rules! greet_all {
    ($($name:expr),*) => {
        $(
            greet($name);
        )*
    };
}

fn main() {
    greet("Ada");
    greet_all!("Grace", "Alan", "Edsger");
}

The function call greet("Ada") is the Rust you already know. The macro call greet_all!("Grace", "Alan", "Edsger") is the new thing. By the time the program runs, that one line has already become three ordinary greet calls; the macro did the writing at compile time, and the running program never knew a macro was involved. That is the whole idea of this chapter, and the rest of it is just learning to write the macro yourself.

The ! means: expands before it runs

A name followed by ! is a macro invocation, not a function call. The compiler expands it (replaces the call with the code it generates) early in compilation, and only that generated code is type-checked and run. So a macro can do things a function cannot: take a variable number of arguments, accept a trailing comma, even write whole new functions and types.

02

A macro is not a function, and the difference is everything

It is tempting to file macros under “functions with a weird exclamation mark”, but that mental model will mislead you. The difference is not cosmetic; it is a difference in what they operate on and when.

A function operates on values, at run time. You pass it an i32 or a String, it computes, it returns a value. By the time the function runs, every type is fixed and every argument is a real value sitting in memory. A macro operates on syntax, at compile time. You pass it a fragment of source code (an expression, a type, a name, a comma-separated list), and it produces more source code, which the compiler then goes on to parse and check as if you had typed it by hand. The macro’s output is not a value; it is program text.

That is why println! can take any number of arguments and a function cannot. println! never sees your arguments as values. It sees them as a chunk of syntax at compile time, counts the placeholders in the format string, matches them against the arguments, and writes out the formatting code. By the time the program runs, there is no println! left, only the plain code it wrote.

If you come from C, you may be bracing for the horrors of textual #definemacros, which paste raw text around and routinely produce nonsense. Rust’s macros are not that. They operate on parsed syntax, not loose text, so a macro cannot tear an expression in half or smuggle in an unbalanced bracket. What it captures is always a complete, well-formed piece of the language. The power is the same; the foot-guns are mostly gone.

Why the exclamation mark, and why "macro"

The !is not punctuation for emphasis; it is part of the syntax that tells the compiler “expand this, do not call it”. The word macrois old (it predates Rust by decades) and just means “something that expands into more of something”. A Rust macro expands one invocation into a block of real Rust code. Nothing more mysterious than that.

03

The matcher and the transcriber

You write your own macros with macro_rules!, the declarative macro system. The name is apt: you give it rules, and each rule has two halves separated by =>. On the left is the matcher: a pattern describing the syntax this rule accepts. On the right is the transcriber: the code to write out when the matcher matches. A macro invocation is checked against each rule in order, top to bottom, and the first matcher that fits wins.

The matcher captures pieces of your input into variables that start with $, and each capture names a fragment specifier: the category of syntax it will accept. The common ones are expr (any expression), ident (an identifier, a bare name), ty (a type), pat (a pattern), block (a { ... } block), stmt (a statement), and tt (a single token tree, the most permissive). The transcriber then drops those captures into the code it generates. Run this:

matcher_and_transcriber.rs
// A macro_rules! rule has two halves around the =>: a matcher on the left
// and a transcriber on the right. Each $name:fragment captures a piece of
// syntax by category. expr captures any expression; ident captures a name;
// ty captures a type.
macro_rules! describe {
    ($label:expr, $value:expr) => {
        println!("{} = {:?}", $label, $value);
    };
}

// ident lets the macro name a variable; ty lets it spell a type. Together
// they let one line of input declare a fully typed, zeroed binding.
macro_rules! make_zero {
    ($name:ident: $kind:ty) => {
        let $name: $kind = Default::default();
    };
}

fn main() {
    describe!("answer", 42);
    describe!("flag", true);

    make_zero!(count: u32);
    make_zero!(label: String);
    println!("count = {count}, label = {label:?}");
}

Read describe! first. Its matcher is ($label:expr, $value:expr): two expressions separated by a comma. When you call describe!("answer", 42), $label captures the expression "answer" and $value captures 42, and the transcriber writes one println! with both spliced in. The macro never evaluated anything; it just moved syntax into a template.

Now make_zero! shows why the fragment kind matters. $name:ident captures a bare name and $kind:ty captures a type, so the transcriber can write a let binding: let $name: $kind = .... Pass it count: u32 and it writes let count: u32 = Default::default(). A function could never take a type as a positional argument like that and then declare a variable with it; this is squarely macro territory.

Fragment specifiers, in one line each

expr matches an expression (something with a value), ident matches a bare name, ty matches a type, pat matches a pattern (the left side of a match arm), block matches a braced block, and tt matches a single token tree and is the catch-all when nothing more specific fits. The specifier tells the compiler how to parse that slice of your input, and it tells the reader what the slot is for.

A captured expr is one sealed unit

Once the matcher captures something as $x:expr, the transcriber treats it as a single sealed expression, not raw text. So $x * 2 in the body multiplies the whole captured expression by two; you never get the C-macro surprise where a + b pasted into $x * 2 silently becomes a + b * 2. The fragment is parsed once and stays whole wherever it lands.

04

Repetition: doing it any number of times

Single captures are useful, but the reason you reach for a macro is usually the thing a function cannot do: handle a list of unknown length. That is what repetition is for. Inside a matcher, $( ... )*means “match the pattern inside zero or more times”; $( ... )+means “one or more times”. Put a token just before the * or + and it becomes a required separator between repeats, so $($x:expr),* matches a comma-separated list of expressions.

The transcriber repeats too. Write $( ... )* on the output side and the code inside is emitted once per match the matcher found, with each $x taking its value from that repetition. The matcher counts; the transcriber stamps. Here is a min! macro that collapses any number of values down to the smallest. Run it:

repetition.rs
// $( ... )* repeats a captured fragment zero or more times. Put a separator
// (here a comma) just before the * and the macro expects that separator
// between repeats. This min! folds any number of values down to one by
// recursion: a rule for the single-value case, and a rule that peels off a
// head and recurses on the tail.
macro_rules! min {
    // One value: it is already the minimum.
    ($only:expr) => { $only };
    // A head, then one or more tail values (+ means "one or more").
    ($head:expr, $($tail:expr),+) => {{
        let head = $head;
        let rest = min!($($tail),+);
        if head < rest { head } else { rest }
    }};
}

fn main() {
    println!("{}", min!(7));
    println!("{}", min!(9, 4, 11, 2, 8));
    println!("{}", min!(3.5, 1.2, 9.9));
}

This macro has two rules, and the order matters. The first, ($only:expr), handles a single value: one number is already its own minimum, so the transcriber is just $only. The second, ($head:expr, $($tail:expr),+), matches a first value followed by one or more others. It captures the head, then calls min! again on the tail (a macro may invoke itself; this is recursion), and compares. Call min!(9, 4, 11, 2, 8) and it peels off 9, recurses on 4, 11, 2, 8, and so on until one value remains.

Notice the macro is fully generic over the value type with no <T> anywhere. min!(7) works on integers and min!(3.5, 1.2, 9.9) on floats, because the macro only writes if a < b and lets the ordinary type checker handle each expansion. The macro generates code; the generated code is type-checked normally, per call.

Separator versus repetition operator

In $($tail:expr),+ the comma is the separator and the + is the repetition operator; they are two different things sitting next to each other. ,+means “one or more, comma-separated”. Swap to ,* for zero or more. Drop the comma and write $(...)+ for repeats with no separator at all, the way a sequence of statements needs none.

05

Build your own: a map literal in twelve lines

Rust ships vec![] for building a Vec, but there is no built-in literal for a HashMap. Real codebases fill that gap with a hand-written map! macro, and now you can read every line of one. The goal: write map!{ "http" => 80, "https" => 443 } and get back a populated map. Run it, then we will take the matcher apart:

build_a_map.rs
// A small map! macro, the kind real codebases write because std ships no map
// literal. The matcher captures key => value pairs separated by commas, with
// an optional trailing comma ($(,)? means "this comma, zero or one times"),
// then the transcriber inserts each captured pair into a fresh HashMap.
use std::collections::HashMap;

macro_rules! map {
    ($($key:expr => $val:expr),* $(,)?) => {{
        let mut m = HashMap::new();
        $(
            m.insert($key, $val);
        )*
        m
    }};
}

fn main() {
    let ports = map! {
        "http" => 80,
        "https" => 443,
        "ssh" => 22,
    };

    // Sort the keys so the output is the same on every run.
    let mut names: Vec<_> = ports.keys().collect();
    names.sort();
    for name in names {
        println!("{name} -> {}", ports[name]);
    }
}

The matcher is ($($key:expr => $val:expr),* $(,)?), and it reads left to right like a sentence. The inner pattern $key:expr => $val:expr captures one key, the literal arrow => (which is just a token the macro requires, not Rust syntax with meaning here), and one value. The $( ... ),* around it repeats that pair zero or more times with a comma between pairs. The final $(,)? is the trailing-comma trick: ?means “zero or one”, so an optional comma is allowed after the last pair, exactly like real Rust collection literals.

The transcriber wraps everything in a {{ ... }} block so the whole expansion is one expression that evaluates to the map. It makes a fresh HashMap, then the repeated $( m.insert($key, $val); )* emits one insert per captured pair, and the bare mon the last line is the block’s value. Three pairs in, three insertcalls out, and the call site gets back a ready map. You just built a small piece of the standard library’s ergonomics yourself.

The double braces are doing real work

The outer braces in => {{ ... }} are the transcriber braces, the body of the rule. The inner braces make a Rust block expression, which is what lets the macro declare a temporary m, run several statements, and still hand back a single value. Without the inner block, the expansion would be loose statements and could not sit where an expression is expected, such as the right of a let.

06

Reaching into the caller, the thing functions cannot do

So far macros have only saved you typing. Here is a power that no function has at all. Because a macro expands insidethe place you call it, the code it writes runs in the caller’s own scope. That means a macro can emit a return, a break, or a ? that affects the function you called it from. A function can never do that: a function body returns from itself, never from its caller.

The classic use is a guard: check a condition, and if it fails, bail out of the surrounding function right there. Many crates ship this as ensure!. Here is a small one of your own. Run it and watch one input pass and two get rejected:

ensure.rs
// A tiny early-return guard, the shape real crates ship as ensure!. It takes
// a condition (an expr) and a message (an expr). If the condition is false it
// returns an Err; otherwise control falls through. Because the macro expands
// inside the caller, its return leaves the caller's function, something no
// ordinary function call could ever do for you.
macro_rules! ensure {
    ($cond:expr, $msg:expr) => {
        if !$cond {
            return Err($msg);
        }
    };
}

fn checked_port(n: i64) -> Result<u16, String> {
    ensure!(n >= 0, format!("{n} is negative"));
    ensure!(n <= 65535, format!("{n} is too large for a port"));
    Ok(n as u16)
}

fn main() {
    for n in [8080, -1, 70000] {
        match checked_port(n) {
            Ok(port) => println!("{n}: ok, port {port}"),
            Err(why) => println!("{n}: rejected, {why}"),
        }
    }
}

Look at what ensure!(n >= 0, ...) expands to: if !(n >= 0) { return Err(...); }. That return leaves checked_port, the function that called the macro, not some inner scope. So checked_port reads as three clean lines, two guards and a success, while the early-exit machinery is written for you by the macro. A helper function could compute the check, but it could never return from checked_port on your behalf. Only inlined code can, and a macro is inlined code.

This is exactly how the standard library’s control-flow macros work under the hood. assert! expands to a check and a panic!; matches! expands to a match that yields a bool; vec! expands to allocation and pushes. None of them could be plain functions, because each writes control flow or repetition that has to live at the call site. The full catalog of those built-in macros is a Part IV story; the mechanism behind all of them is this chapter.

With great inlining comes a sharp edge

Because a macro pastes code into the caller, a careless macro can surprise the reader: a return hidden inside ensure! is invisible at the call site unless you know the macro does it. That is the trade. The convention is to keep macros small and well-named so the surprise is a pleasant one, and to reach for a function first whenever a function would do.

07

Hygiene: the macro will not clobber your variables

Here is the question that should be nagging you. If a macro pastes a block of code into your function, and that block declares a variable, what happens when the macro’s variable has the same name as one of yours? In C, the answer is a famous class of bugs: the macro silently shadows or corrupts your variable, and you spend an afternoon finding out why. In Rust, the answer is hygiene, and it is one of the best things about the system.

Macro hygiene means that identifiers the macro introduces live in their own private world. A variable the macro names cannot see, capture, or overwrite a variable of the same name at the call site, and the reverse holds too. The two are different variables that merely share a spelling. This program proves it: the caller has a total set to 999, and the macro also makes a total. Run it and watch them stay separate:

hygiene.rs
// This macro introduces its own variable called total. Macro hygiene means
// that name lives in its own world: it cannot see, capture, or clobber a
// total that already exists at the call site. The two are different variables
// that merely share a spelling.
macro_rules! sum_into {
    ($($n:expr),+) => {{
        let mut total = 0;
        $(
            total += $n;
        )+
        total
    }};
}

fn main() {
    // The caller has its own total, set to a sentinel value.
    let total = 999;

    // The macro expands to code that also names a total, yet the caller's
    // total is untouched: hygiene keeps the two apart.
    let summed = sum_into!(1, 2, 3, 4);

    println!("caller total still {total}");
    println!("macro summed to {summed}");
}

The macro’s total sums to 10, and the caller’s total is still 999 after the macro runs. They never touched each other. The compiler keeps a hidden bit of bookkeeping on every identifier (which expansion it came from), so even though both are spelled total, they are distinct names to the compiler. You can write a macro without inventing paranoid unguessable variable names, and your callers can use whatever names they like without fear.

Hygiene, in plain English

Names a macro creates for itself are local to the macro, even after the code is pasted into your function. The macro cannot accidentally read or clobber a variable from the call site just because the spellings match, and your call-site variables cannot leak into the macro’s internals. Identifiers you pass in through the matcher, of course, do refer to your scope: that is the point of passing them. Hygiene governs only the names the macro conjures on its own.

Hygiene is not total, and that is on purpose

Variable names are hygienic, but some things the macro names deliberately are not, because they have to reach your scope to be useful: types, functions, and modules the macro refers to resolve where the macro is defined, and any identifier you pass into the macro refers to your scope. For everyday macro_rules! the rule of thumb holds: temporary variables the macro makes up are safe; names you hand it are yours.

08

Beyond macro_rules!: a glance at derive, and when a macro earns its place

Everything so far has been macro_rules!, the declarative macro: you describe a transformation as matcher-to-transcriber rules and the compiler applies it. There is a second, more powerful family you have already been using without writing one: procedural macros. A procedural macro is not a set of patterns; it is an actual Rust function that runs at compile time, takes your code as structured input, and returns new code it builds programmatically. The one you meet first is #[derive(...)]. Run this:

the_derive_glance.rs
// #[derive(...)] is a procedural macro: a different breed from macro_rules!,
// it reads your type's syntax and writes a trait impl for it at compile time.
// You never see the code it generates, but it is there. Here Debug, Clone,
// PartialEq, and Default are all written for Config by the derive, not by you.
#[derive(Debug, Clone, PartialEq, Default)]
struct Config {
    host: String,
    port: u16,
    tls: bool,
}

fn main() {
    // Default came from the derive: every field gets its type's default.
    let base = Config::default();

    // Clone came from the derive: a full copy with no hand-written code.
    let mut custom = base.clone();
    custom.host = String::from("api.internal");
    custom.port = 8443;
    custom.tls = true;

    // Debug came from the derive: {:?} can print the whole struct.
    println!("base   = {base:?}");
    println!("custom = {custom:?}");

    // PartialEq came from the derive: == compares field by field.
    println!("base == custom?  {}", base == custom);
    println!("base == default? {}", base == Config::default());
}

That one line, #[derive(Debug, Clone, PartialEq, Default)], invoked four procedural macros. Each one read the fields of Config and wrote a trait impl for it that you never see in your source but that is fully present by the time the program runs. Default wrote Config::default() that zeroes every field; Clone wrote a field-by-field copy; Debug wrote the :?} formatter; PartialEq wrote ==. That is dozens of lines of mechanical code, derived from the shape of your type. You have leaned on derive since the early chapters; now you know it is a macro doing the writing.

We will not write a procedural macro here, because they live in their own crate with their own toolchain and belong with the ecosystem material. The line to remember: macro_rules! is pattern matching on syntax and is plenty for most needs; derive and other procedural macros are real programs that generate code, and the crates that provide them (the ones behind serialization, command-line parsing, and async) are a Part V story.

When a macro actually earns its place

Reach for a macro only when a function or a generic cannot do the job. If you need a variable number of arguments, or to take a type or a name as input, or to generate items (structs, impls, functions), or to write control flow into the caller like ensure!, a macro is the right tool. If a generic function would do, use the generic function: it is easier to read, gives better error messages, and does not surprise anyone. Macros are a sharp tool you keep for the jobs that genuinely need them.

The cost you pay for the power

Macros expand before the type checker runs, so when a macro is misused the error often points at the generated code, not your call, and can read strangely. They are harder to read, harder to debug, and they slow compilation a little. None of this is a reason to avoid them; it is a reason to prefer a plain function whenever one fits and to save macros for the cases that earn the trade.

The whole chapter in six lines

  1. A macro takes syntax and produces syntax at compile time, which is why println! can take any number of arguments and a function cannot.
  2. macro_rules! is a list of rules, each a matcher => transcriber; the first matcher that fits wins.
  3. Fragment specifiers ($x:expr, $i:ident, $t:ty, and friends) capture pieces of input by syntactic category, and each capture stays a sealed whole.
  4. Repetition $( ... ),* and $( ... ),+ match and emit a list any number of times, with an optional separator and an optional trailing comma $(,)?.
  5. A macro can write control flow into the caller (like ensure!) and generate whole items, and hygiene keeps its own variables from clobbering yours.
  6. #[derive] is a procedural macro, a real program that writes trait impls from your type; reach for a macro only when a function or generic cannot do the job.
End-of-chapter assessmentCheck your understanding21 questions. Your first attempt is recorded on your report card.Take it →