Declarative Macros in Rust
Declarative macros — written with macro_rules! — are the most common kind of
macro you will encounter and write in Rust. They work by matching a pattern against
the syntax of the code that invokes them, and replacing the invocation with an
expanded body. Think of them as a very powerful find-and-replace that operates on
code structure rather than raw text.
You have already used declarative macros constantly: vec!, println!, and
assert_eq! are all implemented with macro_rules!.
Basic Syntax
A macro_rules! definition looks like a match expression, but the arms match
syntax patterns rather than runtime values.
macro_rules! say_hello {
() => {
println!("Hello from a macro!");
};
}
fn main() {
say_hello!();
}Hello from a macro!
The structure is:
macro_rules! name— declares a macro namedname( pattern ) => { expansion }— one or more match arms- Patterns use fragment specifiers to capture pieces of the caller's code
Fragment Specifiers
Fragment specifiers tell the macro what kind of syntax to accept and bind to a
metavariable. The metavariable is prefixed with $ and used in the expansion body.
Specifier | Matches | Example input |
|---|---|---|
| Any expression |
|
| An identifier (variable or function name) |
|
| A type |
|
| A literal value |
|
| A single statement |
|
| A pattern (as used in match) |
|
| A block expression |
|
| A single token tree — the catch-all | Any single token or bracketed group |
| A meta item (attribute content) |
|
| A lifetime |
|
macro_rules! print_type_and_value {
($name:ident, $value:expr) => {
println!("{} = {:?}", stringify!($name), $value);
};
}
fn main() {
let temperature = 36.6_f64;
let active = true;
print_type_and_value!(temperature, temperature);
print_type_and_value!(active, active);
print_type_and_value!(computed, 2 + 2 * 10);
}temperature = 36.6 active = true computed = 22
Repetition Syntax
The most powerful feature of macro_rules! is repetition: accepting zero or
more (or one or more) repeated pieces of syntax and generating repeated output.
The syntax is $( ... ),* for zero-or-more and $( ... ),+ for one-or-more,
where the comma is the separator (which can be any token, or omitted entirely).
macro_rules! print_all {
// $( $x:expr ),* matches zero or more comma-separated expressions
( $( $x:expr ),* ) => {
$(
println!("{:?}", $x);
)*
};
}
fn main() {
print_all!(1, "hello", true, 3.14);
}1 "hello" true 3.14
Building vec! From Scratch
The standard library's vec! macro is one of the best examples of declarative macros.
Here is a simplified version that shows exactly how it works:
macro_rules! my_vec {
// Match zero or more comma-separated expressions
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
// Repeat the push for every matched expression
$( temp_vec.push($x); )*
temp_vec
}
};
// Also accept a trailing comma: my_vec![1, 2, 3,]
( $( $x:expr ),+ , ) => {
my_vec![ $( $x ),* ]
};
}
fn main() {
let v1 = my_vec![1, 2, 3];
let v2 = my_vec!["a", "b", "c",]; // trailing comma works too
let v3: Vec<i32> = my_vec![]; // empty vec
println!("{:?}", v1);
println!("{:?}", v2);
println!("{:?}", v3);
}[1, 2, 3] ["a", "b", "c"] []
{ ... } create a block expression that evaluates to the Vec. This is a common pattern when a macro needs to create local variables without leaking them into the caller's scope.Multiple Match Arms
A macro can have multiple arms, tried in order from top to bottom — just like
match. This lets a single macro behave differently depending on how it is called.
macro_rules! greet {
// No arguments — use a default greeting
() => {
println!("Hello, stranger!");
};
// One identifier — greet by name
($name:ident) => {
println!("Hello, {}!", stringify!($name));
};
// Name + title (two arguments separated by a comma)
($title:literal, $name:ident) => {
println!("Hello, {} {}!", $title, stringify!($name));
};
}
fn main() {
greet!();
greet!(Alice);
greet!("Dr.", Smith);
}Hello, stranger! Hello, Alice! Hello, Dr. Smith!
A Practical Logging Macro
Here is a realistic example: a macro that prefixes log messages with a severity level and the current file/line information, without needing to pass those manually.
macro_rules! log {
(INFO, $( $arg:tt )* ) => {
println!("[INFO {}:{}] {}", file!(), line!(), format!( $( $arg )* ));
};
(WARN, $( $arg:tt )* ) => {
println!("[WARN {}:{}] {}", file!(), line!(), format!( $( $arg )* ));
};
(ERROR, $( $arg:tt )* ) => {
eprintln!("[ERROR {}:{}] {}", file!(), line!(), format!( $( $arg )* ));
};
}
fn connect(host: &str, port: u16) -> bool {
log!(INFO, "Connecting to {}:{}", host, port);
if port == 0 {
log!(ERROR, "Invalid port: {}", port);
return false;
}
log!(WARN, "Connection to {} is insecure", host);
true
}
fn main() {
connect("example.com", 80);
connect("example.com", 0);
}[INFO src/main.rs:12] Connecting to example.com:80 [WARN src/main.rs:18] Connection to example.com is insecure [INFO src/main.rs:12] Connecting to example.com:0 [ERROR src/main.rs:15] Invalid port: 0
$( $arg:tt )* (a repetition of token trees) is the standard way to forward format-string arguments to another macro like format!or println!. It accepts any sequence of tokens.Exporting Macros
By default, a macro_rules! macro is only visible in the module where it is defined.
To make it available to other modules and crates, use the #[macro_export] attribute.
// In src/lib.rs (or any module)
#[macro_export]
macro_rules! hashmap {
( $( $key:expr => $val:expr ),* ) => {{
let mut map = std::collections::HashMap::new();
$( map.insert($key, $val); )*
map
}};
}
// Users of your crate can now write:
// use my_crate::hashmap;
// let m = hashmap!{ "a" => 1, "b" => 2 };With #[macro_export], the macro is placed at the crate root and becomes available
with use my_crate::macro_name; — or even #[macro_use] on the extern crate
declaration in older code.
#[macro_export] moves the macro to the crate root regardless of which module it is defined in. If you want to keep it organised, define it in a submodule but document that it is accessible from the crate root.A HashMap Creation Macro in Action
use std::collections::HashMap;
macro_rules! hashmap {
( $( $key:expr => $val:expr ),* ) => {{
let mut map = HashMap::new();
$( map.insert($key, $val); )*
map
}};
}
fn main() {
let scores: HashMap<&str, u32> = hashmap! {
"Alice" => 95,
"Bob" => 87,
"Carol" => 92
};
for (name, score) in &scores {
println!("{}: {}", name, score);
}
}Declarative Macros vs Functions
Knowing when to use a declarative macro versus a plain function is a judgement call. Here is a decision guide.
Situation | Prefer |
|---|---|
Fixed, known number of arguments | Function |
Variable number of arguments of mixed types | Declarative macro |
All arguments have the same type | Function with a slice or iterator parameter |
Need to generate repetitive struct/impl blocks | Declarative macro (or proc macro) |
Embedding compile-time file/env data | Built-in macro (include_str!, env!) |
Complex AST transformation or custom derive | Procedural macro |
Simple, debuggable, reusable logic | Function — always the default choice |
Limitations of Declarative Macros
No type information — patterns match syntax tokens, not types. You cannot write a macro that behaves differently based on whether an argument is
i32orString.Harder to debug — errors in expanded macro code produce messages that point into the generated code, not the macro call, which can be disorienting.
No IDE support for internals — autocompletion and jump-to-definition do not work inside
macro_rules!pattern arms in most editors.Hygiene edge cases — while Rust macros are hygienic by default, some advanced patterns (like intentionally introducing names into the caller scope) require workarounds.
Pattern ambiguity limits — certain fragment specifiers cannot be followed by certain tokens due to the macro parser needing to look ahead unambiguously.