24.1Declarative macros: macro_rules!

Last updated June 13, 2026

Back in lesson 1.1, in your very first program, you wrote println! and we told you the ! marks a macro, "a supercharged function," with the full story promised much later. This is much later. A macro is code that writes code: at compile time, before your program is built, a macro takes the source you wrote and expands it into more source, which the compiler then compiles as if you'd typed it yourself. That's the supercharging. This lesson explains the most common kind, the declarative macro written with macro_rules!, and at last println! stops being magic.

Why macros exist: what functions can't do

You might reasonably ask why we need macros at all when we have functions. Functions are wonderful, but they have fixed shapes that the language won't let them break. A function takes a fixed number of arguments of fixed types. Yet println! accepts any number of arguments of any types: println!("{}", x), println!("{} and {}", a, b), println!("hi"). No Rust function can do that, the signature would have to commit to an argument count. vec![1, 2, 3] builds a vector from however many elements you list; vec![0; 100] builds a hundred zeros. That flexible, variable syntax is precisely what a function can't express and a macro can.

A macro operates a level up: instead of taking values at run time, it takes syntax at compile time and rewrites it. println!("{}", x) expands, before compilation, into the actual buffer-writing code, with the right number of arguments wired in. The ! is your signal that you're calling a macro, not a function, and so the usual function rules don't apply: variable argument counts, the trailing ; flexibility, all of it comes from the expansion happening at compile time.

A first macro_rules!

Here's a macro that does nothing useful but shows the wiring, it expands into a println!:

macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
}

fn main() {
    say_hello!();
}
Hello!

Read the structure. macro_rules! say_hello defines a macro named say_hello. Inside is one rule: a matcher on the left of => and an expansion (the "transcriber") on the right. The matcher () means "this rule matches when the macro is called with empty parentheses." The expansion { println!("Hello!"); } is the code that replaces the call. So when the compiler sees say_hello!(), it substitutes the expansion in its place, exactly as if you'd written println!("Hello!"); there. A macro is a set of pattern-matching rules: match this syntax, replace it with that syntax.

Capturing input

Empty parentheses aren't interesting. The power is in matching parts of the input and reusing them in the expansion. You capture a piece of syntax into a metavariable, written $name:kind, where kind says what sort of syntax to match. The most common kind is expr, an expression:

macro_rules! print_twice {
    ($x:expr) => {
        println!("{}", $x);
        println!("{}", $x);
    };
}

fn main() {
    print_twice!(5 + 5);
}
10
10

The matcher ($x:expr) says "match one expression and call it $x." We called print_twice!(5 + 5), so $x captures the expression 5 + 5, and the expansion drops $x into both println!s. The macro expands into two prints of 5 + 5. Notice $x is syntax, the expression 5 + 5, substituted into the code, not the value 10 passed at run time. That's the whole difference between a macro and a function: the macro pastes your code in, and the compiler evaluates the result.

Other useful kinds you'll see: ident (an identifier, like a variable or function name), ty (a type), literal (a literal value), block (a { ... } block), tt (a single "token tree," the most flexible). You pick the kind that matches the syntax you want to capture.

Repetition: matching "any number of"

Now the feature that makes vec! and println! possible: matching a variable number of things. Inside a matcher, $( ... ),* means "match the pattern inside, repeated, separated by commas, zero or more times." The same $( ... ),* in the expansion repeats the output once per captured item. Here's a simplified vec!:

macro_rules! my_vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp = Vec::new();
            $(
                temp.push($x);
            )*
            temp
        }
    };
}

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

Trace it. The matcher $( $x:expr ),* captures a comma-separated list of expressions, here 1, 2, 3. The expansion builds a vector and, for each captured $x, emits a temp.push($x), that's what the $( temp.push($x); )* does. So my_vec![1, 2, 3] expands, at compile time, into roughly:

{
    let mut temp = Vec::new();
    temp.push(1);
    temp.push(2);
    temp.push(3);
    temp
}

which evaluates to the vector. That is how the real vec! works, and why it accepts any number of elements where a function couldn't. The macro generated one push per element by repeating its body, at compile time, before the compiler ever saw the code. (This pays off lesson 1.1's promise from twenty-three chapters back: the ! means "this is a macro, expanded into code," and println!/vec! are exactly these pattern-and-repetition rules, just more elaborate.)

Key insight

A macro_rules! macro is a compile-time find-and-replace driven by syntax patterns. It matches the shape of the code you wrote ($x:expr, repeated lists via $(...),*) and substitutes it into a template that becomes real source code. This is why macros can do things functions can't, variable argument counts, generating repeated code, building new syntax: they run before compilation and produce code, rather than running during execution and producing values. The ! you've typed since lesson 1.1 was always announcing "this gets expanded, not called."

A word on the rough edges

Honesty, as with async: macro_rules! is powerful but its syntax is genuinely cryptic, the $, the :expr, the $(...),* are a small language unto themselves, and macro error messages are often worse than ordinary ones, because they can point at generated code you never wrote. Writing macros is a more advanced skill than writing functions, and you'll use hundreds of macros for every one you write. The next lesson is specifically about that judgment: when a macro is the right tool, and when a function or generic was the answer all along.

Quiz time

Question #1

What does a macro do, and how is that different from what a function does?

Show solution

A macro is code that writes code: at compile time, it takes the syntax you wrote and expands it into more source code, which the compiler then compiles. A function runs at run time and operates on values. The difference is the level and timing: a macro manipulates syntax before compilation (so it can do things like accept a variable number of arguments and generate repeated code), while a function takes a fixed set of value arguments during execution.

Question #2

Why can println! and vec! accept any number of arguments when an ordinary function can't?

Show solution

Because they're macros, not functions. A function's signature commits to a fixed number and type of parameters. A macro matches syntax with patterns, including repetition patterns like $( $x:expr ),* that match "zero or more comma-separated expressions," and expands its body once per captured item. So the macro generates the right code for however many arguments you pass, something a fixed function signature cannot express.

Question #3

In macro_rules!, what does $x:expr capture, and is $x a value or syntax?

Show solution

$x:expr captures one expression from the macro's input into the metavariable $x (the :expr is the fragment kind, here "expression"). $x is syntax, not a runtime value: the macro substitutes the captured expression into the expansion's code as-is, and the compiler evaluates the result. So print_twice!(5 + 5) pastes 5 + 5 into the generated code, it doesn't pass the value 10.

You can read and write a basic declarative macro now. But should you? The next lesson (24.2) is about that judgment: the cases where a macro earns its complexity, and the more common cases where a function or generic is the better answer.