Skip to Content
Course content

185: Declarative Macros with macro_rules! In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How do I actually tell Rust what parts of my macro call are variables and what parts are syntax?

When you're looking at a macro_rules! block, you'll see these strange symbols like $x:expr or $name:ident. Think of these as "matchers." You're essentially telling the Rust compiler, "If you see something that looks like an expression here, capture it and call it $x."

The part after the colon is the designator. If you use expr, it matches almost any valid Rust expression. If you use ident, it only matches a valid identifier (like a function or variable name). I've seen a lot of people default to expr for everything, but that's a mistake. If you're trying to define a new function name inside a macro, expr won't work because a function name isn't an expression—it's an identifier.

macro_rules! create_function {
    ($name:ident, $body:expr) => {
        fn $name() {
            println!("Executing: {}", $body);
        }
    };
}

create_function!(say_hello, "Hello from the macro!");

fn main() {
    say_hello();
}

Notice how $name uses ident. If I had used expr there, the compiler would have thrown a fit because you can't have an expression where a function name is required.

How do I handle a variable number of arguments without writing a dozen versions of the macro?

This is where declarative macros actually become powerful. You use repetition patterns: $( ... ) repeated_separator. It looks a bit like regex, and honestly, it's the part of macro_rules! that takes the most getting used to.

Let's say you want a macro that creates a Vec of strings from a list of inputs, but you want to automatically wrap each one in a specific prefix. You don't want to define the macro once for one argument, once for two, and so on. Instead, you do this:

macro_rules! prefix_list {
    ($prefix:expr, $($item:expr),*) => {
        {
            let mut v = Vec::new();
            $(
                v.push(format!("{}{}", $prefix, $item));
            )*
            v
        }
    };
}

fn main() {
    let items = prefix_list!("ID_", "apple", "banana", "cherry");
    // Result: ["ID_apple", "ID_banana", "ID_cherry"]
    println!("{:?}", items);
}

Break down that $($item:expr),* part: the $( ... ) is the block to repeat, and the ,* tells Rust that the items are separated by commas, and there can be zero or more of them. If you wanted to require at least one item, you'd use + instead of *. I usually prefer * unless the macro literally cannot function without at least one input.

I keep hearing about "token trees" (tt). When do I actually need those over something like "expr"?

This is the "advanced" level of declarative macros. A tt (Token Tree) is the most basic unit. It matches anything—a single punctuation mark, a keyword, an identifier, or a balanced group of tokens in parentheses.

Why would you want that? Because expr is "greedy." If you're trying to build a complex Domain Specific Language (DSL) inside Rust, expr might gobble up more than you intended, or it might fail because the input isn't a valid Rust expression yet. Using tt allows you to capture raw chunks of code and then pass them into another macro or process them recursively.

I rarely use tt for simple utilities, but it's indispensable when you're building something like a custom HTML-like macro or a sophisticated configuration block where the syntax doesn't map 1:1 to Rust expressions. If you find yourself fighting with the compiler because it says your "expression" is invalid, try switching to tt and see if that lets you capture the tokens you need.




📋 Practical Task

Exercise: Build a "Map Initializer" Macro

One of the most tedious parts of writing tests in Rust is manually inserting a dozen key-value pairs into a HashMap. Your task is to create a declarative macro called map_init! that simplifies this process.

Requirements:

  • The macro should take a generic type for the key and value as its first two arguments.
  • It should then take a variable number of key-value pairs, where each pair is separated by a => symbol, and each pair itself is separated by a comma.
  • The macro must return a HashMap containing all the provided pairs.

Expected Usage:

let scores = map_init!(&str, i32, "Alice" => 10, "Bob" => 20, "Charlie" => 15);
// This should expand to a HashMap with those three entries.

Hint: You will need to use repetition patterns for the pairs. Think about how to group the key and value together inside the repetition block $( ... ).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.