Declarative macros (macro_rules!)

Rust · Advanced · 16 min

Macros remove boilerplate that functions cannot, such as variadic arguments or generating impls for many types.

A declarative macro matches its input tokens against patterns and expands into code, at compile time. vec!, println! and matches! are examples. Use one when a function cannot do the job: a variable number of arguments, generating items (functions, impls) for many types, or taking code fragments as input.

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

Each rule is (matcher) => { transcriber }, tried top to bottom. Metavariables carry a fragment specifier: expr, ident, ty, pat, path, block, stmt, literal, lifetime, vis, item, meta, and tt (a single token tree, the most flexible).

Repetitions $( ... ),* match zero or more comma-separated items (+ for one or more, ? for optional) and are expanded with the same $( ... )* syntax in the output. Rules can call the macro recursively to process input piece by piece.

Macros are partly hygienic: local variables created inside the expansion cannot clash with the caller's variables. Item names (functions, types) are not hygienic, and paths are resolved at the call site, so exported macros should refer to their own crate as $crate::path::item.

Two classic gotchas: an expr argument is substituted as one unit, so square!(1 + 2) gives 9, but it is also evaluated as many times as it appears; bind it with let v = $x; to evaluate once. And macros must be defined before use in textual order inside a crate.

To make a macro usable from other crates, add #[macro_export]; it then lives at the crate root and can be imported with use my_crate::my_macro;. Debug expansions with cargo expand (a separate tool).

Example

use std::collections::HashMap;

macro_rules! hashmap {
    ($($k:expr => $v:expr),+ $(,)?) => {{ // one or more pairs, optional trailing comma
        let mut m = HashMap::new();
        $( m.insert($k, $v); )+
        m
    }};
}

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

macro_rules! newtype {
    ($($name:ident($inner:ty)),* $(,)?) => { $( #[derive(Debug, Clone, Copy)] struct $name($inner); )* };
}
newtype!(Meters(f64), Seconds(f64));

fn main() {
    let ages = hashmap! { "ann" => 31, "bob" => 27, };
    println!("{} {}", ages["ann"], max!(3, 9, 4));          // 31 9
    println!("{} m, {} s", Meters(1.5).0, Seconds(2.0).0);  // 1.5 m, 2 s
}

Remember

  • Rules match token patterns with fragment specifiers like expr, ident, ty, tt
  • $( ... ),* repeats in both matcher and expansion
  • Bind expr arguments once; use $crate:: and #[macro_export] for exported macros

Watch out for

  • Writing a macro where a generic function would work; macros are harder to read, debug and document.

Check yourself

  1. Given `macro_rules! double { ($x:expr) => { $x + $x }; }`, how many times does `double!(next())` call next()?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: doc.rust-lang.org/reference/macros-by-example.html