RustArena

macro_rules! in Rust: writing your own macros

How to write declarative macros in Rust with macro_rules!: matchers, fragment specifiers like expr and ident, repetition with $(...),*, and when to use them.

You've been calling macros since your first program: println!, vec!, format!, assert_eq!. A macro is code that writes code at compile time. macro_rules! lets you write your own declarative macros: you describe input patterns, and what each one expands to.

A first macro

macro_rules! square {
    ($x:expr) => {
        $x * $x
    };
}

fn main() {
    println!("{}", square!(7));
    println!("{}", square!(2 + 3));
}
49
25

Fragment specifiers

Specifier Matches
expr an expression: 1 + 2, foo(x), vec![1]
ident an identifier: count, Player
ty a type: u32, Vec<String>
literal a literal: 42, "hi", true
pat a pattern: Some(x), 1..=5
block a { ... } block
tt a single token tree (the most flexible)

Repetition

$( ... ),* matches zero or more comma-separated items; + means one or more; ? means optional. Repeat the same group in the expansion to emit code per item:

macro_rules! hashmap {
    ($($key:expr => $value:expr),* $(,)?) => {{
        let mut map = std::collections::HashMap::new();
        $( map.insert($key, $value); )*
        map
    }};
}

let ages = hashmap! { "Ferris" => 7, "Corro" => 3 };

$(,)? accepts an optional trailing comma. The double braces {{ ... }} make the expansion a block expression, so the macro can be used where a value is expected.

Hygiene

Variables created inside a macro (like map above) can't clash with the caller's variables of the same name: macro_rules! is hygienic for local variables. Items (functions, types) are not, so refer to them with full paths ($crate::helper, ::std::collections::HashMap) in macros you export.

When to write a macro

Reach for a function or a generic first: they're easier to read, type-check and debug. Macros earn their place when you need something functions can't do:

#[macro_export] makes a macro available to other crates. cargo expand (an installable tool) shows what macros expand to, which is the best way to debug them.

Errors

Calling a macro with input no rule matches gives errors like no rules expected `,` or unexpected end of macro invocation. Errors inside the expansion point into the macro's body.

Procedural macros

#[derive(Serialize)], #[tokio::main] and #[test]-like attributes are procedural macros: Rust functions, in a separate crate, that receive a token stream and return a new one. They're far more powerful (and more work) than macro_rules!; most developers use them every day but rarely write one.

Example

use std::collections::BTreeMap;

macro_rules! square {
    ($x:expr) => {
        $x * $x
    };
}

// Variadic: zero or more key => value pairs, optional trailing comma.
macro_rules! btreemap {
    ($($key:expr => $value:expr),* $(,)?) => {{
        let mut map = BTreeMap::new();
        $( map.insert($key, $value); )*
        map
    }};
}

// Several rules, tried in order.
macro_rules! max_of {
    ($x:expr) => { $x };
    ($x:expr, $($rest:expr),+) => {{
        let a = $x;
        let b = max_of!($($rest),+);
        if a > b { a } else { b }
    }};
}

// Generating items: one function per identifier.
macro_rules! unit_fns {
    ($($name:ident => $factor:expr),* $(,)?) => {
        $( fn $name(v: f64) -> f64 { v * $factor } )*
    };
}

unit_fns! {
    km_to_miles => 0.621371,
    kg_to_pounds => 2.20462,
}

// stringify! turns the caller's tokens into text.
macro_rules! check {
    ($cond:expr) => {
        println!("{:<22} {}", stringify!($cond), if $cond { "ok" } else { "FAILED" });
    };
}

fn main() {
    println!("{} {}", square!(7), square!(2 + 3));
    let stock = btreemap! { "potion" => 5, "sword" => 1, "arrow" => 40, };
    println!("{stock:?}");
    println!("max {}", max_of!(3, 17, 9, 12));
    println!("{:.2} miles, {:.1} lb", km_to_miles(10.0), kg_to_pounds(5.0));
    check!(square!(4) == 16);
    check!(stock.len() > 5);
}

Output:

49 25
{"arrow": 40, "potion": 5, "sword": 1}
max 17
6.21 miles, 11.0 lb
square!(4) == 16       ok
stock.len() > 5        FAILED

Practice it