Skip to content

Step 1 of 4 · Reading · ~4 min

Learn

Macros and Unsafe

Declarative Macros (macro_rules!)

You will write macro_rules! macros that take lists, recurse on their own input, and generate whole items such as structs and impl blocks. You have called println! and vec! since the first lessons; this lesson writes macros of that kind.

Rules, fragments and repetition

A macro is a list of rules, (matcher) => { transcriber };. The compiler tries the rules top to bottom and expands the first matcher that fits. Inside a matcher, $name:fragment captures a piece of syntax, and the fragment says what kind:

FragmentMatches
expran expression, kept as one unit
identan identifier (a name)
tya type
literala literal such as 3 or "a"
visan optional pub or pub(crate)
lifetimea lifetime such as 'a
ttone token tree (a token or a bracketed group); the catch-all

$( ... ) sep rep repeats: * is zero or more, + one or more, ? zero or one. The transcriber repeats the same variables the same way. An optional trailing comma is written $(,)?:

use std::collections::BTreeMap;

macro_rules! map {
    ($($k:expr => $v:expr),* $(,)?) => {{
        let mut m = BTreeMap::new();
        $( m.insert($k, $v); )*
        m
    }};
}

fn main() {
    let m = map!{"b" => 2, "a" => 1,};
    println!("{:?}", m);
}

Output: {"a": 1, "b": 2}. Delete the $(,)? and the same call stops compiling: error: unexpected end of macro invocation ... missing tokens in macro arguments. The doubled {{ }} is deliberate: the inner braces make the expansion a block expression.

Recursion and rule order

A macro may call itself on part of its input. Put the base case first, because order decides which rule wins:

macro_rules! max {
    ($x:expr) => { $x };
    ($x:expr, $($rest:expr),+) => {{
        let a = $x;
        let b = max!($($rest),+);
        if a > b { a } else { b }
    }};
}
// max!(3, 9, 4) expands to max!(3, max!(9, max!(4)))  ->  9

The trap is a catch-all rule above a specific one. With ($($t:tt)*) => { "catch-all" }; first, kind!(1 + 2) prints catch-all; with ($x:expr) => { "one expr" }; first it prints one expr. Peeling a token off the front and recursing on the rest is called tt-munching. Helper rules get a private marker such as (@step ...) so callers cannot hit them by accident.

Hygiene and textual scope

Names created inside a macro do not collide with the caller's names:

macro_rules! twice {
    ($e:expr) => {{ let a = 2; $e * a }};
}
let a = 10;
println!("{}", twice!(a + 1));   // 22

The a in $e is the caller's a (10), the a in the macro is its own, so the result is (10 + 1) * 2 = 22. An expr capture keeps its grouping; a C preprocessor would give 10 + 1 * 2 = 12. Hygiene also hides a macro's let hidden = 5; from the caller; to hand the caller a name, pass it in as an ident.

A macro must be defined above its first use in the file: calling later!() first gives error: cannot find macro 'later' in this scope. To use one across modules and crates, mark it #[macro_export] and write paths inside it as $crate::path.

Generating items

An ident capture lets a macro declare things, and stringify! turns tokens into a string literal:

macro_rules! unit_struct {
    ($name:ident) => {
        struct $name;
        impl $name {
            fn label() -> &'static str { stringify!($name) }
        }
    };
}
unit_struct!(Meter);   // Meter::label() returns "Meter"

stringify! is also accepted where a pattern is expected, so a generated match can compare against it.

Common mistakes

  • A catch-all tt rule above a specific one: it matches everything, so the specific rule never runs.
  • Counting commas or a fixed number of items instead of the items: a trailing comma then changes the count by one.
  • Forgetting $(,)?: an invocation with a trailing comma fails to compile.
  • Using a macro where a generic function would do. Reach for one only when the arity, an identifier or a whole item must be generated. Procedural macros (#[derive(...)]) are functions over token streams in a separate crate and are not written here.

Your exercise

Write make_enum! so one invocation generates the enum, its ALL slice, name, from_name and COUNT. The starter's main only uses that generated API. Names match exactly, case included, and the second enum is written with a trailing comma.

Up nextUnsafe Rust: Raw Pointers and Safe AbstractionsMacros and Unsafe

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…