Macros
Rust's macro system is one of its most distinctive and powerful features. Unlike macros in C/C++ that perform simple text substitution, Rust macros operate on the abstract syntax tree (AST), allowing them to generate complex, type-safe code at compile time.
You've already been using macros without perhaps realizing it. println!, vec!, assert!, and format! are all macros. The exclamation mark (!) after a name is the telltale sign that something is a macro, not a regular function.
Why Macros?
Macros solve problems that regular functions cannot handle:
- Variable argument counts —
println!accepts any number of format arguments - Code generation — eliminating repetitive boilerplate
- Domain-specific syntax — creating mini-languages embedded within Rust
- Compile-time computation — performing work before the program runs
Rust has two main categories of macros: declarative macros (using macro_rules!) and procedural macros (using proc_macro). This lesson focuses on declarative macros, which are the most common and approachable type.
Declarative Macros with macro_rules!
Declarative macros work by matching patterns against input and generating code based on those patterns. Think of them as a powerful match expression, but for Rust syntax itself.
Here is your first macro:
macro_rules! greet {
() => {
println!("Hello, World!");
};
($name:expr) => {
println!("Hello, {}!", $name);
};
}
fn main() {
greet!(); // matches the first arm
greet!("Rustacean"); // matches the second arm
greet!(String::from("Alice")); // also matches the second arm
}
The macro has two arms separated by semicolons. The first arm () matches when called with no arguments. The second arm ($name:expr) matches when given any expression.
The $name:expr syntax is called a metavariable. The :expr part is a fragment specifier that tells Rust what kind of syntax to capture. Common specifiers include:
expr— any expressionident— an identifier such as a variable or function namety— a typeliteral— a literal value like42or"hello"stmt— a statementblock— a block of code surrounded by{}
Repeating Patterns
One of the most powerful macro features is repetition, written as $(...)* or $(...)+. This lets you handle a variable number of arguments cleanly.
Let's recreate something similar to the built-in vec! macro:
macro_rules! my_vec {
($($element:expr),* $(,)?) => {
{
let mut v = Vec::new();
$(
v.push($element);
)*
v
}
};
}
fn main() {
let numbers = my_vec![10, 20, 30, 40, 50];
let words = my_vec!["rust", "macros", "are", "powerful"];
let with_trailing = my_vec![1, 2, 3,]; // trailing comma is fine
println!("Numbers: {:?}", numbers);
println!("Words: {:?}", words);
println!("With trailing comma: {:?}", with_trailing);
}
Breaking down $($element:expr),* $(,)?:
$(...)starts a repetition group$element:exprcaptures each expression,means elements are comma-separated*means zero or more repetitions$(,)?allows an optional trailing comma
Building Practical Macros
Let's create something genuinely useful: a macro that constructs a HashMap from key-value pairs with clean, readable syntax.
use std::collections::HashMap;
macro_rules! map {
($($key:expr => $value:expr),* $(,)?) => {
{
let mut m = HashMap::new();
$(
m.insert($key, $value);
)*
m
}
};
}
fn main() {
let config = map! {
"host" => "localhost",
"port" => "8080",
"debug" => "true",
};
let scores = map! {
"Alice" => 95_u32,
"Bob" => 87_u32,
"Charlie" => 92_u32,
};
println!("Config: {:?}", config);
let top = scores.iter().max_by_key(|entry| entry.1);
println!("Top scorer: {:?}", top);
}
This macro expands at compile time into individual .insert() calls, giving you clean syntax with zero runtime overhead compared to building the map manually.
Multiple Pattern Arms
Macros can have many arms, just like match expressions. Rust tries each arm in order and uses the first one that matches. This lets you build macros with flexible calling conventions:
macro_rules! log {
($msg:literal) => {
println!("[INFO] {}", $msg);
};
($level:ident, $msg:literal) => {
println!("[{}] {}", stringify!($level), $msg);
};
($level:ident, $msg:literal, $val:expr) => {
println!("[{}] {}: {:?}", stringify!($level), $msg, $val);
};
}
fn main() {
log!("Application started");
log!(WARN, "Memory usage is high");
log!(ERROR, "Failed to connect", "timeout after 30s");
let user_count = 42;
log!(INFO, "Active users", user_count);
}
Notice the use of the built-in stringify! macro, which converts an identifier token into its string representation at compile time — no runtime allocation required. This is a common technique when you want to print the name of something rather than its value.
Try It Yourself
Create a min! macro that finds the minimum value among any number of arguments. It uses a base case for a single value and recursively reduces a longer list:
macro_rules! min {
// Base case: a single value is the minimum of itself
($x:expr) => ($x);
// Recursive case: compare the first value with the min of the rest
($x:expr, $($rest:expr),+) => {
std::cmp::min($x, min!($($rest),+))
};
}
fn main() {
let smallest = min!(5, 3, 8, 1, 9, 2);
println!("Smallest of 5,3,8,1,9,2 is: {}", smallest);
let a = min!(100, 42);
println!("Smaller of 100 and 42 is: {}", a);
// Challenge: write a max! macro using std::cmp::max
// Challenge: write a sum! macro that adds all values together
}
The $($rest:expr),+ pattern uses + instead of *, requiring at least one additional argument beyond the first. The macro calls itself recursively, peeling off one value at a time until it reaches the single-value base case.
Key Takeaways
- Rust macros operate on the abstract syntax tree at compile time, not on raw text like C macros
- The
!suffix distinguishes macro calls from function calls macro_rules!defines declarative macros using pattern matching with multiple arms- Metavariables (
$name:fragment) capture different kinds of Rust syntax - The
$(...)*repetition syntax handles variable numbers of arguments - Trailing comma support is a common ergonomic convention:
$(,)? - Macros can recurse, enabling patterns like processing lists one element at a time
- Built-in macros like
vec!,println!, andassert!follow exactly the same rules as your own
Pro Tip: When debugging a macro, install
cargo-expandwithcargo install cargo-expandand runcargo expandin your project. This prints the fully expanded source code, showing exactly what Rust compiles after all macros have been applied. It is the single most effective tool for understanding why a macro is generating unexpected output.
Next Steps
With macros understood, you're ready for async/await — Rust's approach to writing efficient, non-blocking concurrent code.
Next lesson
Async/Await
Master asynchronous programming in Rust with async/await syntax, Futures, and the Tokio runtime
25 min