Skip to content
AgileLogics
7 min read

Declarative Macros

Prerequisites: Send, Sync & concurrency patterns

You have used macros since your first println!. Declarative macros, written with macro_rules!, let you write your own: they match on the syntax you pass in and expand to code before type checking. Use them to remove repetition that functions and generics can't, such as variadic arguments or generating several similar impl blocks.

A first macro

A macro is a list of rules. Each rule is (pattern) => { expansion }, and the first matching rule wins.

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

fn main() {
    println!("{}", square!(7));
    println!("{}", square!(2 + 1)); // 9, not 2 + 1 * 2 + 1
}

Expected output:

49
9

$x:expr captures a whole expression as a single unit, so 2 + 1 behaves as if parenthesised. Note that $x is still evaluated twice; a macro that receives next_id() would call it twice.

Fragment specifiers

The part after the colon says what kind of syntax to capture:

Specifier Matches
expr an expression: 1 + 2, foo(), vec![1]
ident an identifier: x, Point
ty a type: u32, Vec<String>
pat a pattern: Some(_), 1..=5
literal a literal: 42, "hi"
block a { ... } block
path a path: std::io::Error
lifetime, vis, item, stmt, meta what their names say
tt a single token tree — the most flexible

In the 2024 edition, expr also matches _ and const { ... }; expr_2021 keeps the older behaviour if you need it.

Repetition

$( ... ),* repeats a pattern zero or more times separated by commas (+ means one or more, ? zero or one). The same syntax in the expansion repeats the output once per match. $(,)? accepts an optional trailing comma.

use std::collections::BTreeMap;

macro_rules! map {
    ($($key:expr => $value:expr),* $(,)?) => {{
        #[allow(unused_mut)] // an empty map!{} never inserts
        let mut m = ::std::collections::BTreeMap::new();
        $( m.insert($key, $value); )*
        m
    }};
}

fn main() {
    let ports = map! {
        "http" => 80,
        "https" => 443,
    };
    println!("{ports:?}");
    let empty: BTreeMap<&str, i32> = map!{};
    println!("{}", empty.len());
}

Expected output:

{"http": 80, "https": 443}
0

The doubled braces {{ ... }} make the expansion a block expression, so the macro can be used wherever an expression is expected.

Recursion and generating items

Macros can call themselves, and can generate items like structs and impls:

macro_rules! max {
    ($x:expr) => { $x };
    ($x:expr, $($rest:expr),+) => {{
        let a = $x;
        let b = max!($($rest),+);
        if a > b { a } else { b }
    }};
}

trait Named {
    fn name() -> &'static str;
}

macro_rules! impl_named {
    ($($t:ty),+) => {
        $( impl Named for $t {
            fn name() -> &'static str { stringify!($t) }
        } )+
    };
}

struct Dog;
struct Cat;
impl_named!(Dog, Cat);

fn main() {
    println!("{}", max!(3, 9, 4));
    println!("{} {}", Dog::name(), Cat::name());
}

Expected output:

9
Dog Cat

Hygiene

Variables introduced inside a macro live in the macro's own syntax context, so they cannot clash with (or accidentally capture) the caller's variables:

macro_rules! double_it {
    ($e:expr) => {{
        let x = $e; // this `x` is private to the macro
        x * 2
    }};
}

fn main() {
    let x = 10;
    println!("{}", double_it!(x + 1));
    println!("{x}");
}

Expected output:

22
10

Hygiene applies to local variables and labels. Items and paths are resolved at the call site, so inside exported macros refer to your own crate as $crate::path::to::item.

Exporting and ordering

A macro_rules! macro is only visible after its definition in the source file. Put #[macro_export] on it to make it available to other crates as your_crate::map!.

Common mistakes

  • Passing input no rule accepts. square!(1, 2) fails with the first error below; a trailing comma against a pattern of $($x:expr),* fails with the second. Add $(,)? to patterns that take lists.
error: no rules expected `,`
error: unexpected end of macro invocation
  • Evaluating an argument more than once. Bind it with let first.
  • Using a macro where a generic function would do. Functions give better error messages and are easier to read; macros are for what functions can't express.
  • Referring to helper items with bare paths inside an exported macro. Use $crate::.

Practice

Write a sum! macro that accepts one or more expressions separated by commas (with an optional trailing comma) and adds them up. sum!(1, 2, 3) should give 6 and sum!(10,) should give 10.

Show solution
macro_rules! sum {
    ($first:expr $(, $rest:expr)* $(,)?) => {
        $first $(+ $rest)*
    };
}

fn main() {
    println!("{}", sum!(1, 2, 3));
    println!("{}", sum!(10,));
    println!("{}", sum!(1.5, 2.5));
}

This prints 6, 10 and 4.