16. Macros
📋 Jump to Takeaways🎁 You've called println!, vec!, assert!, and panic! throughout this course. Did you notice the !? That's not decoration — it means something specific. These aren't functions. They're macros, and they work in a fundamentally different way.
What Are Macros
Macros run at compile time and generate code. The ! suffix is how Rust distinguishes a macro call from a function call.
The key thing macros can do that functions can't: accept a variable number of arguments with different types in the same position.
println!("hello"); // 1 arg
println!("x = {}", x); // 2 args
println!("{} + {} = {}", a, b, a + b); // 4 argsA function signature is fixed — it can't take 1, 2, or 4 arguments depending on the call site. A macro matches patterns in your source code and rewrites them before compilation. The compiler never sees the macro call, only the generated code.
Rust has no variadic functions. Go has ...T — you can write func sum(nums ...int) and pass any number of ints. Rust deliberately omits this. Macros are the answer instead, and they're more powerful: a Go variadic is limited to one type, while a Rust macro can accept mixed types, generate arbitrary code, and capture source locations. The trade-off is that macros are harder to write and debug than functions.
Macro Delimiters
You've seen macros called with (), [], and {}. All three work for any macro — Rust accepts them all:
vec![1, 2, 3] // conventional
vec!(1, 2, 3) // also valid
vec!{1, 2, 3} // also validThe choice is purely convention, not syntax. The community settled on delimiters that make the call site look like what it produces:
[]for macros that produce collections (vec!,matches!)()for macros that look like function calls (println!,assert!,panic!){}for macros that contain blocks of statements (macro_rules!definitions)
You won't break anything using the wrong one, but other Rust developers will notice.
Macros You Already Know
Before writing your own, it helps to see why the built-ins are macros:
println!/format!— variadic formatted output; a function can't take a varying number of args with different typesvec![1, 2, 3]— creates aVecfrom a list; the length isn't fixedassert!(cond)/assert_eq!(a, b)— checks a condition and panics with the source file and line number if it fails; only macros have access to that information at compile timepanic!("msg")— terminates with a messagetodo!()— marks unfinished code; panics if reached at runtimedbg!(value)— prints the file, line, and value to stderr, then returns the value (useful for quick debugging)
fn main() {
let x = dbg!(2 + 3); // [src/main.rs:3] 2 + 3 = 5
println!("{}", x); // 5
}macro_rules!
Declarative macros use macro_rules! to match patterns and emit code. The structure looks like a match expression, but for syntax rather than values.
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
fn main() {
say_hello!(); // Hello!
}The () is the pattern — matches an empty argument list. The => block is the code to generate.
Capturing Arguments
Patterns capture arguments using designators — expr, ident, ty, and literal are built-in keywords in the macro pattern language. They restrict what kind of syntax the caller can pass at that position:
$x:expr— any expression:5,x + 1,foo(),if a { b } else { c }$x:ident— a bare name only:x,my_var,HashMap— notx + 1$x:ty— a type:i32,Vec<String>,&str$x:literal— a literal value only:42,"hello",true— not a variable
The distinction matters when the macro needs a valid identifier to generate code with:
macro_rules! declare {
($name:ident) => {
let $name = 0;
};
}
fn main() {
declare!(counter); // ✅ — "counter" is a valid identifier
// declare!(x + 1); ❌ — "x + 1" is an expression, not an identifier
}macro_rules! double {
($x:expr) => {
$x * 2
};
}
fn main() {
println!("{}", double!(5)); // 10
println!("{}", double!(3 + 1)); // 8 — the whole expression is captured
}$x:expr captures any expression — a literal, a variable, a calculation. The macro substitutes it verbatim into the generated code.
Variadic Macros
Use $(...)* to match zero or more repetitions. This is how vec![] works internally:
macro_rules! sum {
($($x:expr),*) => {{
let mut total = 0;
$(total += $x;)*
total
}};
}
fn main() {
println!("{}", sum!(1, 2, 3)); // 6
println!("{}", sum!(10, 20, 30, 40)); // 100
}$($x:expr),* means "zero or more expressions separated by commas." The $(total += $x;)* expands into one statement per captured expression at compile time.
Multiple Patterns
A macro can have multiple arms, tried in order — first match wins:
macro_rules! greet {
() => {
println!("Hello, world!");
};
($name:expr) => {
println!("Hello, {}!", $name);
};
}
fn main() {
greet!(); // Hello, world!
greet!("Alice"); // Hello, Alice!
}When to Use Macros
Macros are powerful but harder to read and debug than functions. Reach for them when:
- You need variadic arguments (different numbers of args at different call sites)
- You need the caller's source location (
assert!uses this to show exactly where it failed) - You need to generate repetitive code at compile time
For everything else, a function is clearer and easier to test.
Key Takeaways
- The
!suffix marks a macro call; macros run at compile time and generate code println!,vec!,assert!,panic!,todo!,dbg!are macros because functions can't handle variadic or mixed-type argumentsmacro_rules!defines declarative macros using pattern matching on syntax$x:exprcaptures any expression;$x:identcaptures an identifier;$x:tycaptures a type$($x:expr),*matches zero or more comma-separated expressions — the basis of variadic macros- Multiple arms are tried in order; first match wins
- Prefer functions over macros unless you specifically need variadic args, source location info, or compile-time code generation
🎁 You've written sequential Rust. But what if you want two things to happen at the same time? Next up: threads, and how Rust makes concurrent code safe by construction.