Macro Rules Basics in Rust With Patterns and Repetitions
macro_rules! matches token patterns and expands them into code. Learn fragment types, repetition and when a macro beats a function.

A macro is code that writes code before the compiler checks types, which is why println! can take any number of arguments and a function cannot. You rarely need to write one, and when you do it is usually a short macro_rules! that removes repetition. This post builds one from nothing and names the pieces. For derive-style macros see writing a derive macro; both sit under traits, generics and trait objects.
Rust 1.90, edition 2024.
The smallest macro
macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(7));
}It prints 49. $x:expr captures any expression. A subtle point: square!(1 + 2) expands to (1 + 2) * (1 + 2) and not 1 + 2 * 1 + 2, because expr captures a parsed expression, not text. That is unlike C preprocessor macros.
Fragment specifiers
| Specifier | Matches |
|---|---|
expr | an expression |
ident | an identifier |
ty | a type |
pat | a pattern |
stmt | a statement |
block | a block { ... } |
literal | a literal |
tt | one token tree, the escape hatch |
Repetition
A vec!-like macro:
macro_rules! my_vec {
() => { Vec::new() };
($($x:expr),+ $(,)?) => {{
let mut v = Vec::new();
$( v.push($x); )+
v
}};
}
fn main() {
let v = my_vec![1, 2, 3,];
println!("{v:?}");
}It prints [1, 2, 3]. The pattern $($x:expr),+ means one or more expressions separated by commas, and $(,)? accepts a trailing comma. The expansion's $( v.push($x); )+ repeats once per capture. The double braces make the body a block expression so the macro can end with v.
Generating items
Macros can write whole impl blocks, which is where they earn their keep.
macro_rules! impl_area {
($t:ident, $body:expr) => {
impl $t {
fn area(&self) -> f64 {
let s = self;
$body(s)
}
}
};
}For the same trait implemented for ten numeric types, one macro invocation per type replaces ten copies.
Hygiene
A let inside the macro creates a binding the caller cannot see, and the caller's variables are not captured by accident. Identifiers you want the caller to supply must come in as $name:ident.
When to skip the macro
If a generic function with a trait bound can do it, write that: the compiler error messages are better and tools can see through it. See generics and monomorphization. A macro also hides the call from rust-analyzer's jump to definition. Use cargo expand to see what yours produces. The primary reference is the Rust book's section on declarative macros.


