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.
What you'll learn
- ✓Understand how macro_rules! works under the hood
- ✓Use fragment specifiers like expr, ident, ty, and tt
- ✓Write macros with repetitions for variable arguments
- ✓Build practical macros for real codebases
Prerequisites
- •Comfortable with Rust syntax — see /blog/rust-variables-and-types
- •Familiarity with traits and generics — see /blog/rust-traits-basics
Macros let you write code that writes code. In Rust, macro_rules! defines declarative macros — pattern-based transformations that expand at compile time. You already use them constantly: println!, vec!, assert!, and format! are all macros.
Writing your own macros eliminates boilerplate, enforces patterns, and creates domain-specific syntax. This guide teaches you to write macros confidently, starting from simple patterns and building to real-world use cases.
Your First Macro
macro_rules! say_hello {
() => {
println!("Hello, world!");
};
}
fn main() {
say_hello!(); // Expands to: println!("Hello, world!");
}
The macro definition has two parts:
- Pattern (left of
=>): what the macro matches. - Template (right of
=>): what it expands to.
The empty parentheses () mean the macro takes no arguments. The exclamation mark ! in the call is how Rust distinguishes macro invocations from function calls.
Fragment Specifiers
Macros capture input using fragment specifiers — placeholders with types:
| Specifier | Matches | Example |
|---|---|---|
$x:expr | Any expression | 5 + 3, foo() |
$x:ident | An identifier | my_var, String |
$x:ty | A type | i32, Vec<String> |
$x:pat | A pattern | Some(x), _ |
$x:stmt | A statement | let x = 5 |
$x:block | A block | { x + 1 } |
$x:literal | A literal value | 42, "hello" |
$x:tt | A single token tree | anything |
$x:item | An item | fn foo() {}, struct Bar; |
Using expr
macro_rules! square {
($x:expr) => {
$x * $x
};
}
macro_rules! min {
($a:expr, $b:expr) => {
if $a < $b { $a } else { $b }
};
}
fn main() {
println!("{}", square!(5)); // 25
println!("{}", min!(10, 3)); // 3
println!("{}", min!(square!(2), 5)); // 4
}
Warning: the square! macro has a subtle bug. If you call square!(1 + 2), it expands to 1 + 2 * 1 + 2 = 1 + 2 + 2 = 5, not 9. Fix it by wrapping in parentheses:
macro_rules! square {
($x:expr) => {
($x) * ($x)
};
}
Also watch out for double evaluation — $x appears twice, so the expression runs twice. For side-effect-free expressions this is fine, but for function calls it might not be:
macro_rules! square_safe {
($x:expr) => {{
let val = $x;
val * val
}};
}
Using ident
The ident specifier captures identifiers, which you can use to generate names:
macro_rules! make_getter {
($field:ident, $ty:ty) => {
fn $field(&self) -> &$ty {
&self.$field
}
};
}
struct User {
name: String,
email: String,
age: u32,
}
impl User {
make_getter!(name, String);
make_getter!(email, String);
make_getter!(age, u32);
}
fn main() {
let user = User {
name: "Alice".into(),
email: "alice@example.com".into(),
age: 30,
};
println!("{} ({}) age {}", user.name(), user.email(), user.age());
}
Multiple Match Arms
A macro can have multiple patterns, tried in order:
macro_rules! log {
($msg:expr) => {
println!("[LOG] {}", $msg);
};
($fmt:expr, $($arg:expr),+) => {
println!(concat!("[LOG] ", $fmt), $($arg),+);
};
}
fn main() {
log!("Server started");
log!("Listening on port {}", 8080);
log!("{} connections from {}", 42, "10.0.0.1");
}
The macro tries to match the first arm. If it fails, it tries the second. This lets you support multiple calling conventions.
Repetitions
Repetitions handle variable numbers of arguments. The syntax is $(...) separator repetition_operator:
*— zero or more+— one or more?— zero or one
macro_rules! vec_of_strings {
($($s:expr),* $(,)?) => {
vec![$($s.to_string()),*]
};
}
fn main() {
let names = vec_of_strings!["Alice", "Bob", "Charlie"];
println!("{:?}", names); // ["Alice", "Bob", "Charlie"]
let empty: Vec<String> = vec_of_strings![];
println!("{:?}", empty); // []
}
The $(,)? at the end allows an optional trailing comma.
Building a HashMap Macro
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,
};
for (name, score) in &scores {
println!("{}: {}", name, score);
}
}
The $( map.insert($key, $value); )* expands once for each key-value pair. If you pass three pairs, you get three insert calls.
Nested Repetitions
Repetitions can be nested for more complex patterns:
macro_rules! matrix {
($([$($val:expr),* $(,)?]),* $(,)?) => {{
vec![$(vec![$($val),*]),*]
}};
}
fn main() {
let m = matrix![
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
for row in &m {
println!("{:?}", row);
}
}
Practical Macro: Builder Pattern
Generate builder methods for a struct:
macro_rules! builder {
($name:ident { $($field:ident : $ty:ty),* $(,)? }) => {
#[derive(Debug, Default)]
struct $name {
$($field: Option<$ty>),*
}
impl $name {
fn new() -> Self {
Self::default()
}
$(
fn $field(mut self, value: $ty) -> Self {
self.$field = Some(value);
self
}
)*
}
};
}
builder!(ServerConfig {
host: String,
port: u16,
workers: usize,
tls: bool,
});
fn main() {
let config = ServerConfig::new()
.host("localhost".to_string())
.port(8080)
.workers(4)
.tls(true);
println!("{:?}", config);
}
Practical Macro: Enum with Display
macro_rules! named_enum {
($name:ident { $($variant:ident),* $(,)? }) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum $name {
$($variant),*
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
$(Self::$variant => write!(f, stringify!($variant))),*
}
}
}
impl $name {
fn all() -> &'static [Self] {
&[$(Self::$variant),*]
}
}
};
}
named_enum!(Color { Red, Green, Blue, Yellow });
fn main() {
for color in Color::all() {
println!("{}", color);
}
// Red
// Green
// Blue
// Yellow
}
stringify! converts a token to a string literal at compile time.
Practical Macro: Test Helpers
macro_rules! assert_approx_eq {
($left:expr, $right:expr) => {
assert_approx_eq!($left, $right, 1e-6);
};
($left:expr, $right:expr, $tolerance:expr) => {{
let left = $left;
let right = $right;
let diff = (left - right).abs();
assert!(
diff < $tolerance,
"assertion failed: |{} - {}| = {} >= {}",
left, right, diff, $tolerance
);
}};
}
macro_rules! test_cases {
($func:ident, $($name:ident: $input:expr => $expected:expr),* $(,)?) => {
$(
#[test]
fn $name() {
assert_eq!($func($input), $expected);
}
)*
};
}
fn double(n: i32) -> i32 {
n * 2
}
test_cases!(double,
double_zero: 0 => 0,
double_positive: 5 => 10,
double_negative: -3 => -6,
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_approx() {
assert_approx_eq!(0.1 + 0.2, 0.3);
assert_approx_eq!(std::f64::consts::PI, 3.14159, 0.001);
}
}
Token Trees and Recursive Macros
The tt specifier matches any single token tree — a token or a group in parentheses, brackets, or braces. This is useful for macros that need maximum flexibility:
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
}
This macro recursively counts tokens. Each expansion peels off one token and adds 1.
Recursive Macro for Nested Structures
macro_rules! json_like {
(null) => { JsonValue::Null };
(true) => { JsonValue::Bool(true) };
(false) => { JsonValue::Bool(false) };
($n:literal) => { JsonValue::Number($n as f64) };
([$($val:tt),* $(,)?]) => {
JsonValue::Array(vec![$(json_like!($val)),*])
};
({ $($key:literal : $val:tt),* $(,)? }) => {{
let mut map = std::collections::HashMap::new();
$(map.insert($key.to_string(), json_like!($val));)*
JsonValue::Object(map)
}};
}
#[derive(Debug)]
enum JsonValue {
Null,
Bool(bool),
Number(f64),
Array(Vec<JsonValue>),
Object(std::collections::HashMap<String, JsonValue>),
}
fn main() {
let data = json_like!({
"name": "Alice",
"age": 30,
"scores": [95, 87, 92],
"active": true
});
println!("{:#?}", data);
}
Macro Hygiene
Rust macros are partially hygienic — variables defined inside a macro do not conflict with variables outside:
macro_rules! make_var {
($val:expr) => {
let x = $val; // This `x` is in the macro's scope
println!("Inside macro: {}", x);
};
}
fn main() {
let x = 10;
make_var!(42);
println!("Outside macro: {}", x); // Still 10, not 42
}
However, if you use $x:ident to accept a variable name from the caller, it operates in the caller’s scope. This is by design.
Debugging Macros
Use cargo expand to see what your macros expand to:
// cargo install cargo-expand
// cargo expand
For quick debugging during development, the compile_error! macro is useful:
macro_rules! my_macro {
($x:expr) => { $x + 1 };
($($unknown:tt)*) => {
compile_error!("Invalid syntax for my_macro!");
};
}
When Not to Use Macros
Macros add complexity. Before writing a macro, consider:
- Functions — if a regular function works, use it. Functions have better error messages and IDE support.
- Generics — if the variation is in types, generics are cleaner.
- Traits — if the variation is in behavior, traits are more idiomatic.
Use macros when you need to:
- Generate repetitive code that cannot be abstracted with functions or generics.
- Create custom syntax or DSLs.
- Work with variable numbers of arguments.
- Generate code based on identifiers or types.
Wrapping Up
Declarative macros with macro_rules! are a powerful tool in Rust’s metaprogramming toolkit. The essential concepts are:
- Macros match patterns and expand to code at compile time.
- Fragment specifiers (
expr,ident,ty,tt, etc.) capture different kinds of syntax. - Repetitions (
*,+,?) handle variable arguments. - Multiple match arms support different calling conventions.
- Recursive macros can process arbitrarily complex input.
stringify!andconcat!are useful for generating strings from tokens.
Start with simple macros for eliminating boilerplate, and gradually build up to more complex patterns. Use cargo expand to verify your macros do what you expect, and remember that a well-placed function is almost always better than a poorly-motivated macro.
Related articles
- 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.
- 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.