macro_rules!
macro_rules! defines declarative macros by pattern-matching token trees and expanding to Rust code. They are the first tool for lightweight DSLs and repetitive boilerplate.
Recipe
macro_rules! hashmap {
($($k:expr => $v:expr),* $(,)?) => {{
let mut m = std::collections::HashMap::new();
$( m.insert($k, $v); )*
m
}};
}When to reach for this: Repetitive syntax that functions cannot abstract (token-level patterns).
Working Example
macro_rules! assert_returned {
($expr:expr, $pat:pat) => {
match $expr {
$pat => {}
other => panic!("unexpected: {:?}", other),
}
};
}
#[test]
fn parses() {
assert_returned!(parse("42"), Ok(42));
}What this demonstrates:
- Multiple arms for different patterns
$(,)?optional trailing comma- Hygienic expansion in same crate
- Export with
#[macro_export]for crate root visibility
Deep Dive
Match Arms
macro_rules! op {
(add $a:expr, $b:expr) => { $a + $b };
(mul $a:expr, $b:expr) => { $a * $b };
}Recursive Macros
macro_rules! count {
() => { 0 };
($head:tt $(, $tail:tt)*) => { 1 + count!($($tail),*) };
}- Recursion limited by compiler recursion limit
- Prefer non-recursive repetition when possible
Gotchas
- Opaque error messages - errors point at expansion site. Fix:
compile_error!with clear text for invalid input. - Multiple ambiguous arms - surprising match. Fix: order arms most specific first; test with
cargo expand. - Importing macro from crate - need
#[macro_export]orpub use. Fix: document macro pathmy_crate::hashmap!. - Type inference surprises - expanded code differs from handwritten. Fix: add explicit types in expansion.
- Overusing for simple helpers - worse DX than fn. Fix: default to functions.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
const fn / generics | Compile-time logic expressible as fn | Need custom syntax |
| Proc macro | Complex AST transforms | Simple token templates |
build.rs codegen | Large generated modules | Small call-site sugar |
FAQs
macro_rules vs proc macro?
macro_rules for pattern templates; proc for full AST (derive, attributes).
How export macro?
#[macro_export] puts at crate root; re-export with pub use crate::macro_name;.
Can macros be async?
Expand to async block inside fn; macro itself not async.
tt specifier?
Token tree - captures arbitrary token subtree for flexible matchers.
Multiple expressions?
Use $( $e:expr );* with semicolon separation.
Document macros?
/// on macro_rules! block documents in rustdoc.
Edition 2024 macros?
Same macro_rules model; follow edition for generated code use paths.
Test macros?
Unit test expanded code via public fns the macro wraps; or integration test macro output types.
compile_error usage?
($($rest:tt)*) => { compile_error!("hashmap! requires key => value"); }; as fallback arm.
Limit recursion?
#![recursion_limit = "256"] at crate root if deep recursive macro required.
Related
- Macros Basics - introduction
- Debugging Macros - cargo expand
- Function-like Proc Macros - heavier DSLs
- Macros Best Practices
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.