Lesson 22 of 22ยท17 min

Macros

You have used macros since the first lesson: println!, format!, vec!, assert_eq!, and the #[derive(...)] attributes on your structs. A macro is code that writes code. The compiler expands each macro call into ordinary Rust before it type-checks the program, so a macro can do things a function can't: take any number of arguments, accept syntax that isn't a value (a type, a name, a =>), and generate whole functions, impl blocks and types.

This lesson shows how to write your own declarative macros with macro_rules!, and what the other kind, procedural macros, are and how one is built.

A first macro

macro_rules! defines a macro. Its body is a list of rules; each rule has a matcher on the left of => and a transcriber on the right:

macro_rules! shout {
    ($text:expr) => {
        println!("{}!", $text.to_uppercase())
    };
}
 
fn main() {
    shout!("hello");
 
    let name = "ferris";
    shout!(name);
}
HELLO!
FERRIS!

The matcher ($text:expr) says: expect one expression, and call it $text. The transcriber is the code that replaces the call, with $text substituted. So shout!("hello") becomes println!("{}!", "hello".to_uppercase()), and only then does the compiler check types. A macro works on syntax, not on values.

A call can use (), [] or {}: shout!("hi"), shout!["hi"] and shout! { "hi" } are the same. By convention you pick the one that reads best, like vec![...] for a list.

Expressions stay whole

A $x:expr fragment is substituted as one unit, so operator precedence can't break it apart the way it can with text-replacing macros in C:

macro_rules! square {
    ($x:expr) => {
        $x * $x
    };
}
 
fn main() {
    println!("{}", square!(2 + 3));
}
25

It is still substituted twice, though. If the expression has a side effect, the side effect happens twice:

macro_rules! square {
    ($x:expr) => {
        $x * $x
    };
}
 
fn main() {
    let mut calls = 0;
    let mut next_reading = || {
        calls += 1;
        4
    };
 
    println!("{}", square!(next_reading()));
    println!("called {calls} times");
}
16
called 2 times

The fix is to evaluate the argument once into a variable. A transcriber wrapped in double braces, {{ ... }}, expands to a block expression, so it can hold let statements and still produce a value:

macro_rules! square {
    ($x:expr) => {{
        let value = $x;
        value * value
    }};
}
 
fn main() {
    let value = 3;
    println!("{}", square!(value + 1));
}
16

The caller has a value too, and the two don't clash. Macros are hygienic: a variable the macro declares lives in the macro's own naming context, so it can't capture or overwrite the caller's value. (The outer braces are the rule's delimiters; the inner pair is the block in the generated code.)

Fragment specifiers

expr is one of several fragment specifiers. Each tells the macro what kind of syntax to match:

SpecifierMatchesExample
expran expression2 + 3, f(x)
identa nametotal, Book
tya typeVec<u32>
literala literal value42, "hi"
pata patternSome(x), _
blocka block{ a + b }
patha pathstd::io::Read
stmta statementlet x = 1
iteman item: a function, struct, impl, ...fn f() {}
tta single token tree, anything in brackets+, (a, b)

Pick the narrowest one that fits. It gives callers better errors, and the compiler knows how the fragment can be used in the expansion.

Several rules

A macro can have several rules. The compiler tries them from top to bottom and uses the first one that matches. Anything in a matcher that is not a $ fragment has to appear literally in the call, which lets a macro accept small bits of custom syntax:

macro_rules! seconds {
    ($n:literal h) => {
        $n * 3600
    };
    ($n:literal min) => {
        $n * 60
    };
    ($n:literal s) => {
        $n
    };
}
 
fn main() {
    let total = seconds!(1 h) + seconds!(30 min) + seconds!(15 s);
    println!("{total} seconds");
}
5415 seconds

h, min and s are not variables: they are tokens the caller has to write. A call that matches no rule is a compile error at the call site.

Repetition

The reason vec![1, 2, 3] can take any number of elements is repetition. In a matcher, $( ... ),* matches the part in parentheses zero or more times, separated by commas. In the transcriber, $( ... )* repeats its contents once for each match:

use std::collections::HashMap;
 
macro_rules! hash_map {
    ($($key:expr => $value:expr),* $(,)?) => {{
        let mut map = HashMap::new();
        $(
            map.insert($key, $value);
        )*
        map
    }};
}
 
fn main() {
    let stock = hash_map! {
        "apples" => 12,
        "pears" => 0,
        "plums" => 7,
    };
 
    println!("{} kinds", stock.len());
    println!("apples: {}", stock["apples"]);
}
3 kinds
apples: 12

The call has three key => value pairs, so the expansion has three map.insert(...) lines. $key and $value are used inside the same $( )* in the transcriber, so they are repeated in step. $(,)? matches an optional trailing comma, a small courtesy that lets callers put each entry on its own line.

OperatorMeans
$( ... )*zero or more times
$( ... )+one or more times
$( ... )?zero or one time

The separator goes between the ) and the operator: , in $( ... ),*, ; in $( ... );*, or nothing for no separator.

Recursion

A macro can call itself, which is how you handle a list one element at a time. This one finds the largest of any number of values:

macro_rules! max_of {
    ($x:expr) => {
        $x
    };
    ($x:expr, $($rest:expr),+) => {{
        let first = $x;
        let rest = max_of!($($rest),+);
        if first > rest { first } else { rest }
    }};
}
 
fn main() {
    println!("{}", max_of!(3, 9, 4));
    println!("{}", max_of!(2.5, -1.0));
    println!("{}", max_of!("pear", "apple", "fig"));
}
9
2.5
pear

The second rule takes the first value, and hands the rest ($($rest),+) back to max_of!, until one value is left and the first rule ends the recursion. Because the expansion is plain code, it works for any type that supports >: integers, floats and strings alike.

Generating items

Macros can expand to items, not only expressions: functions, structs, enums and impl blocks. That is where they save the most typing. Here an ident fragment names an enum and its variants, and the macro writes the enum along with a method and a constant that would otherwise have to be kept in sync by hand:

macro_rules! named_enum {
    ($name:ident { $($variant:ident),* $(,)? }) => {
        #[derive(Clone, Copy)]
        enum $name {
            $($variant),*
        }
 
        impl $name {
            const ALL: &[$name] = &[$($name::$variant),*];
 
            fn name(self) -> &'static str {
                match self {
                    $($name::$variant => stringify!($variant)),*
                }
            }
        }
    };
}
 
named_enum! { Planet { Mercury, Venus, Earth, Mars } }
 
fn main() {
    for planet in Planet::ALL {
        println!("{}", planet.name());
    }
}
Mercury
Venus
Earth
Mars

$variant is used twice in the transcriber: once to declare the variants and once in the match, and each use repeats over the same list. stringify! turns tokens into a string literal at compile time, so Mercury becomes "Mercury". Adding a planet is now one word in one place.

Generating the same impl for several types works the same way, with a ty fragment for the type. It is the main reason macros appear in the standard library's own source: most of the arithmetic traits for the dozen integer types are written once, in a macro.

Where a macro can be used

Macros are defined textually: a macro_rules! macro can only be used below its definition in the file (or in a module declared after it). This is different from functions, which can be called before they are defined.

To use a macro from another crate, the defining crate marks it #[macro_export]. That puts it at the root of the crate, and other crates import it like anything else:

// in a library crate called `units`
#[macro_export]
macro_rules! seconds {
    ($n:literal min) => {
        $n * 60
    };
}
 
// in a crate that depends on `units`:
// use units::seconds;
// let timeout = seconds!(5 min);

When a macro doesn't expand to what you expect, cargo expand (installed with cargo install cargo-expand) prints your code with every macro expanded.

Procedural macros

Declarative macros match patterns. Procedural macros are Rust functions that run at compile time: they receive the tokens of your code as a TokenStream, and return a new TokenStream that the compiler uses instead. There are three kinds:

KindLooks likeExamples
Derive#[derive(Serialize)]serde's Serialize, thiserror
Attribute#[route(GET, "/")] on an item#[tokio::main], #[async_trait]
Function-likesql!(SELECT ...)sqlx::query!, html!

A derive macro only adds code after the item it is on. An attribute macro replaces the item with whatever it returns. A function-like macro is called like a macro_rules! macro but can run any code to produce its output.

A procedural macro has to live in its own crate, marked proc-macro = true, because the compiler builds it first and runs it while compiling the crates that use it. Here is a small derive macro, TypeName, that gives a struct a type_name() function. The project has the macro crate inside it:

invoices/
โ”œโ”€โ”€ Cargo.toml
โ”œโ”€โ”€ src/main.rs
โ””โ”€โ”€ type_name_derive/
    โ”œโ”€โ”€ Cargo.toml
    โ””โ”€โ”€ src/lib.rs

The macro crate's Cargo.toml turns on proc-macro and pulls in the two crates almost every procedural macro uses: syn parses the token stream into a syntax tree, and quote turns Rust-like code back into tokens:

[package]
name = "type_name_derive"
version = "0.1.0"
edition = "2024"
 
[lib]
proc-macro = true
 
[dependencies]
quote = "1"
syn = "2"

The macro itself, in type_name_derive/src/lib.rs:

use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
 
#[proc_macro_derive(TypeName)]
pub fn derive_type_name(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let label = name.to_string();
 
    let expanded = quote! {
        impl #name {
            pub fn type_name() -> &'static str {
                #label
            }
        }
    };
 
    expanded.into()
}

#[proc_macro_derive(TypeName)] registers the function as the derive for TypeName. parse_macro_input! parses the tokens of the struct into a DeriveInput, which has the item's name (ident), its generics, and its data: the fields of a struct or the variants of an enum, which a real derive usually walks over. Inside quote!, #name inserts a variable, as an identifier here, and #label inserts a String as a string literal.

The main crate depends on the macro crate by path, in its Cargo.toml:

[dependencies]
type_name_derive = { path = "type_name_derive" }

and derives it like any built-in trait:

use type_name_derive::TypeName;
 
#[derive(TypeName)]
struct Invoice {
    total_cents: u64,
}
 
fn main() {
    let invoice = Invoice { total_cents: 1999 };
    println!("{}: {} cents", Invoice::type_name(), invoice.total_cents);
}
Invoice: 1999 cents

Procedural macros are a large topic. The syn docs and the Rust Reference's chapter on procedural macros go further.

When to write a macro

Macros are harder to read than functions, their errors point at generated code, and editors understand them less well. Reach for a function or generics first. A macro is the right tool when:

  • the call needs a variable number of arguments, like vec! or hash_map!;
  • the input is syntax, not a value: a type, a name, a small custom notation;
  • the same items (impl blocks, functions, enums) would otherwise be written out many times with only names or types changing.

Common mistakes

A call that matches no rule

macro_rules! seconds {
    ($n:literal min) => {
        $n * 60
    };
    ($n:literal s) => {
        $n
    };
}
 
fn main() {
    let timeout = seconds!(2 hours);
    println!("{timeout}");
}

The compiler tried every rule and none of them accepts hours. Either call the macro with syntax one of its rules expects, or add a rule for it.

Using a macro above its definition

fn main() {
    let total = double!(21);
    println!("{total}");
}
 
macro_rules! double {
    ($x:expr) => {
        $x * 2
    };
}

Unlike functions, macro_rules! macros exist only from their definition downwards. Move the macro above the code that uses it, or into a module declared before that code.

An expr followed by a word

macro_rules! between {
    ($value:expr in $low:expr, $high:expr) => {
        $low <= $value && $value <= $high
    };
}
 
fn main() {
    println!("{}", between!(5 in 1, 10));
}

An expression can go on for more tokens than it seems (5 in ... might be the start of a longer expression in a future Rust), so the compiler only allows =>, , or ; right after an expr fragment. Use one of those (between!(5, 1, 10) or between!(5 => 1, 10)), or match a narrower fragment such as literal or ident when that is all you need.

A repeated variable outside its repetition

macro_rules! sum {
    ($($n:expr),*) => {
        0 + $n
    };
}
 
fn main() {
    println!("{}", sum!(1, 2, 3));
}

$n was matched inside $( ... ),*, so it stands for a whole list, and it can only be used inside a $( ... ) repetition in the transcriber too: 0 $(+ $n)* expands to 0 + 1 + 2 + 3.

Summary

  • A macro is expanded into ordinary Rust before type checking. It can take any number of arguments and any syntax, and generate items.
  • macro_rules! holds rules of (matcher) => { transcriber }, tried top to bottom. $name:fragment captures syntax: expr, ident, ty, literal, tt and others.
  • An expr argument is substituted as a whole but evaluated each time it appears. Bind it once with let inside a {{ ... }} block. Macro variables are hygienic and don't clash with the caller's.
  • $( ... ),* repeats in the matcher and the transcriber; + means one or more, ? optional. Recursion handles a list one element at a time.
  • macro_rules! macros are usable below their definition; #[macro_export] makes one available to other crates.
  • Procedural macros (derive, attribute, function-like) are compile-time functions from TokenStream to TokenStream, in a proc-macro = true crate, usually built with syn and quote.
  • Prefer functions and generics; write a macro when they can't express it.

Practise what you read

Challenges that use the ideas from this lesson.