Skip to content
rusthacks.dev
Generics

Macro Rules Basics in Rust With Patterns and Repetitions

macro_rules! matches token patterns and expands them into code. Learn fragment types, repetition and when a macro beats a function.

2 min read
Summarize in AI
Flat illustration for the post “Macro Rules Basics in Rust With Patterns and Repetitions”, composed around generics.

A macro is code that writes code before the compiler checks types, which is why println! can take any number of arguments and a function cannot. You rarely need to write one, and when you do it is usually a short macro_rules! that removes repetition. This post builds one from nothing and names the pieces. For derive-style macros see writing a derive macro; both sit under traits, generics and trait objects.

Rust 1.90, edition 2024.

The smallest macro

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

fn main() {
    println!("{}", square!(7));
}

It prints 49. $x:expr captures any expression. A subtle point: square!(1 + 2) expands to (1 + 2) * (1 + 2) and not 1 + 2 * 1 + 2, because expr captures a parsed expression, not text. That is unlike C preprocessor macros.

Fragment specifiers

SpecifierMatches
expran expression
identan identifier
tya type
pata pattern
stmta statement
blocka block { ... }
literala literal
ttone token tree, the escape hatch

Repetition

A vec!-like macro:

rust
macro_rules! my_vec {
    () => { Vec::new() };
    ($($x:expr),+ $(,)?) => {{
        let mut v = Vec::new();
        $( v.push($x); )+
        v
    }};
}

fn main() {
    let v = my_vec![1, 2, 3,];
    println!("{v:?}");
}

It prints [1, 2, 3]. The pattern $($x:expr),+ means one or more expressions separated by commas, and $(,)? accepts a trailing comma. The expansion's $( v.push($x); )+ repeats once per capture. The double braces make the body a block expression so the macro can end with v.

Generating items

Macros can write whole impl blocks, which is where they earn their keep.

rust
macro_rules! impl_area {
    ($t:ident, $body:expr) => {
        impl $t {
            fn area(&self) -> f64 {
                let s = self;
                $body(s)
            }
        }
    };
}

For the same trait implemented for ten numeric types, one macro invocation per type replaces ten copies.

Hygiene

A let inside the macro creates a binding the caller cannot see, and the caller's variables are not captured by accident. Identifiers you want the caller to supply must come in as $name:ident.

When to skip the macro

If a generic function with a trait bound can do it, write that: the compiler error messages are better and tools can see through it. See generics and monomorphization. A macro also hides the call from rust-analyzer's jump to definition. Use cargo expand to see what yours produces. The primary reference is the Rust book's section on declarative macros.

Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.