Skip to content
Codeloom
Rust

Rust macro_rules! Practical Guide

Master Rust's macro_rules! with practical examples covering syntax matching, repetition, common patterns, and debugging tips for declarative macros.

·6 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How macro_rules! pattern matching works
  • Repetition syntax for variadic macros
  • Common macro patterns you can use today
  • How to debug macros when things go wrong

Prerequisites

  • Basic Rust knowledge
  • Familiarity with Rust syntax

Rust’s macro_rules! system lets you write code that writes code. Unlike procedural macros, declarative macros (the macro_rules! kind) work entirely through pattern matching on token trees. They are simpler to write, faster to compile, and handle the majority of macro use cases.

Basic Syntax

A macro_rules! definition looks like a match expression. Each arm has a pattern (the matcher) and a body (the expansion):

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

fn main() {
    say_hello!(); // Expands to: println!("Hello, world!");
}

The matcher can capture parts of the input using designators. The most common ones are:

  • $name:expr — matches an expression
  • $name:ty — matches a type
  • $name:ident — matches an identifier
  • $name:tt — matches a single token tree (the most flexible)
  • $name:literal — matches a literal value
  • $name:pat — matches a pattern
  • $name:stmt — matches a statement
  • $name:block — matches a block
macro_rules! create_function {
    ($name:ident, $body:expr) => {
        fn $name() -> i32 {
            $body
        }
    };
}

create_function!(answer, 42);
create_function!(double_answer, 42 * 2);

fn main() {
    println!("{}", answer());        // 42
    println!("{}", double_answer()); // 84
}

Multiple Arms

Like match expressions, macros can have multiple arms. The macro tries each arm in order and uses the first one that matches:

macro_rules! inspect {
    ($val:expr) => {
        println!("{} = {:?}", stringify!($val), $val);
    };
    ($label:literal, $val:expr) => {
        println!("{}: {:?}", $label, $val);
    };
}

fn main() {
    let x = vec![1, 2, 3];
    inspect!(x);                 // x = [1, 2, 3]
    inspect!("my vector", x);   // my vector: [1, 2, 3]
}

Repetition

The real power of macro_rules! is repetition. The syntax $( ... ),* matches zero or more comma-separated items. You can also use + for one or more, and ; or any other token as the separator.

macro_rules! vec_of_strings {
    ($( $s:expr ),* $(,)?) => {
        vec![ $( String::from($s) ),* ]
    };
}

fn main() {
    let names = vec_of_strings!["Alice", "Bob", "Charlie"];
    println!("{:?}", names); // ["Alice", "Bob", "Charlie"]
}

The $(,)? at the end allows an optional trailing comma, which is good practice for ergonomic macros.

Here is a more practical example that builds a HashMap:

macro_rules! hashmap {
    ($( $key:expr => $value:expr ),* $(,)?) => {{
        let mut map = std::collections::HashMap::new();
        $( map.insert($key, $value); )*
        map
    }};
}

fn main() {
    let scores = hashmap! {
        "Alice" => 95,
        "Bob" => 87,
        "Charlie" => 92,
    };
    println!("{:?}", scores);
}

Practical Pattern: Builder-Style API

Macros can generate repetitive struct implementations:

macro_rules! builder {
    ($name:ident { $( $field:ident : $ty:ty ),* $(,)? }) => {
        #[derive(Debug)]
        struct $name {
            $( $field: Option<$ty>, )*
        }

        impl $name {
            fn new() -> Self {
                $name {
                    $( $field: None, )*
                }
            }

            $(
                fn $field(mut self, value: $ty) -> Self {
                    self.$field = Some(value);
                    self
                }
            )*
        }
    };
}

builder!(Config {
    host: String,
    port: u16,
    max_retries: u32,
});

fn main() {
    let config = Config::new()
        .host("localhost".into())
        .port(8080)
        .max_retries(3);
    println!("{:?}", config);
}

Practical Pattern: Enum Dispatch

Generate match arms for an enum automatically:

macro_rules! define_operations {
    ($( $variant:ident => $op:expr ),* $(,)?) => {
        #[derive(Debug)]
        enum Operation {
            $( $variant, )*
        }

        impl Operation {
            fn apply(&self, a: f64, b: f64) -> f64 {
                match self {
                    $( Operation::$variant => $op(a, b), )*
                }
            }
        }
    };
}

define_operations! {
    Add => |a: f64, b: f64| a + b,
    Sub => |a: f64, b: f64| a - b,
    Mul => |a: f64, b: f64| a * b,
    Div => |a: f64, b: f64| a / b,
}

fn main() {
    let op = Operation::Mul;
    println!("{:?}: {}", op, op.apply(6.0, 7.0)); // Mul: 42
}

Practical Pattern: Test Generation

Generate multiple test cases from a table:

macro_rules! test_cases {
    ($func:ident, $( ($input:expr, $expected:expr) ),* $(,)?) => {
        $(
            paste::paste! {
                #[test]
                fn [< test_ $func _ $input >]() {
                    assert_eq!($func($input), $expected);
                }
            }
        )*
    };
}

fn double(x: i32) -> i32 {
    x * 2
}

test_cases!(double,
    (0, 0),
    (1, 2),
    (5, 10),
    (100, 200),
);

Without the paste crate, you can pass test names explicitly:

macro_rules! test_cases {
    ($( $name:ident: $input:expr => $expected:expr ),* $(,)?) => {
        $(
            #[test]
            fn $name() {
                assert_eq!($input, $expected);
            }
        )*
    };
}

test_cases! {
    test_addition: 2 + 2 => 4,
    test_string_len: "hello".len() => 5,
    test_vec_sum: vec![1, 2, 3].iter().sum::<i32>() => 6,
}

Recursive Macros

Macros can call themselves for complex patterns:

macro_rules! count {
    () => { 0usize };
    ($head:tt $($tail:tt)*) => { 1usize + count!($($tail)*) };
}

fn main() {
    let n = count!(a b c d e);
    println!("Count: {n}"); // 5
}

A more practical recursive macro that builds a nested if-else chain:

macro_rules! cond {
    ($condition:expr => $result:expr) => {
        if $condition { Some($result) } else { None }
    };
    ($condition:expr => $result:expr, $($rest:tt)*) => {
        if $condition { Some($result) } else { cond!($($rest)*) }
    };
}

fn main() {
    let x = 15;
    let label = cond!(
        x > 100 => "large",
        x > 10 => "medium",
        x > 0 => "small"
    );
    println!("{:?}", label); // Some("medium")
}

Debugging Macros

When macros produce confusing errors, these tools help:

Use cargo expand to see what your macro expands to:

// Install: cargo install cargo-expand
// Run: cargo expand

Use stringify! to print what the macro received:

macro_rules! debug_macro {
    ($($tokens:tt)*) => {
        println!("Received: {}", stringify!($($tokens)*));
    };
}

Use compile_error! for better error messages in your macros:

macro_rules! require_positive {
    ($val:expr) => {
        if $val <= 0 {
            compile_error!("Value must be positive");
        }
    };
}

Build incrementally. Start with the simplest possible matcher, verify it expands correctly, then add complexity one piece at a time.

When to Use macro_rules! vs Procedural Macros

Use macro_rules! when you need to:

  • Reduce boilerplate with simple pattern substitution
  • Create variadic functions or constructors
  • Generate repetitive implementations

Use procedural macros when you need to:

  • Derive trait implementations (custom #[derive])
  • Transform arbitrary Rust syntax with full parsing power
  • Work with attributes on items

Wrapping Up

macro_rules! gives you compile-time code generation through pattern matching. Master the designators (expr, ident, ty, tt), learn the repetition syntax ($(...),*), and build up from simple patterns. Use cargo expand to verify your expansions and compile_error! to provide clear messages when patterns do not match. Start with the patterns in this guide and adapt them to your specific boilerplate.