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.
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.
Related articles
- Rust Rust macro_rules!: Write Your Own Macros
Learn to write Rust declarative macros with macro_rules!. Covers syntax, fragment specifiers, repetitions, hygiene, and practical macro patterns.
- Rust Rust Macros: Declarative vs Procedural
A practical comparison of declarative macro_rules! and procedural macros in Rust, including derive, function-like, and attribute macros with examples.
- Rust Rust Async Programming with Tokio: Getting Started
Learn async Rust with Tokio from scratch. Covers async/await syntax, spawning tasks, channels, TCP servers, and common pitfalls with working examples.
- Rust Rust Concurrency: Channels, Mutex, and Arc Patterns
Learn Rust concurrency with channels, Mutex, Arc, and thread-safe patterns. Covers mpsc, shared state, thread pools, and real-world examples.