Macros
In one sentence
Section titled “In one sentence”A macro is code that writes code at compile time, and you can always recognize one: a name with ! (vec!, println!) or an attribute (#[derive(Debug)], #[test]).
The Java you know
Section titled “The Java you know”Lombok and annotation processors generate Java code at compile time. #[derive] does the same job, and it is part of the language:
Java
@Data // generates getters, setters, equals, hashCode, and toStringpublic class Line { private String item; private long cents;}Rust
#[derive(Debug, Clone, PartialEq, Eq)] // a derive macro: generates trait implspub struct Line { pub item: String, pub cents: u64,}
pub fn receipt(lines: &[Line]) -> String { let total: u64 = lines.iter().map(|l| l.cents).sum(); let mut out = vec![format!("{} lines", lines.len())]; // `vec!` and `format!`: function-like macros out.push(format!("total {total}")); debug_assert!(total < 100_000_000, "suspicious total: {total}"); // checked only in debug builds out.join("\n")}Lombok’s @Data changes the class through compiler internals. A Rust macro only adds or replaces code through a public, stable interface, and you can see the result with cargo expand (below).
The Rust you will see
Section titled “The Rust you will see”How to recognize a macro
Section titled “How to recognize a macro”| You see | Kind | Examples |
|---|---|---|
name!(...), name![...], name!{...} |
function-like macro | println!, vec!, format!, assert_eq!, matches!, write! |
#[derive(Trait)] on a type |
derive macro | Debug, Clone, serde::Serialize, thiserror::Error |
#[name] on an item |
attribute macro | #[tokio::main], #[test], #[tracing::instrument] |
macro_rules! name { ... } |
a macro definition | in the code you review |
Why is println! a macro and not a function? It takes a variable number of arguments, and it checks the format string against them at compile time. println!("{} {}", a) is a compile error, not a run-time error as String.format would give.
What a derive generates
Section titled “What a derive generates”#[derive(Debug)] writes an impl Debug block for you. This is about what it generates for a struct with two fields:
/// About what `#[derive(Debug)]` generates for a struct with two fields.pub struct LineByHand { pub item: String, pub cents: u64,}
impl fmt::Debug for LineByHand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("LineByHand") .field("item", &self.item) .field("cents", &self.cents) .finish() }}The test hand_written_debug_matches_derive checks that the two give the same output. You do not need to read generated code often. But it helps to know that a derive is ordinary code that you could have written, not magic.
Two kinds of macros
Section titled “Two kinds of macros”- Declarative macros, written with
macro_rules!. They match patterns of tokens and replace them with code, like a much more powerful C preprocessor that understands Rust syntax.vec!andmatches!are declarative. - Procedural macros: Rust functions, in their own crate, that the compiler runs at compile time. All derive and attribute macros are procedural.
serde,thiserror, andtokioprovide them. They are the Rust equivalent of annotation processors.
How to read it
Section titled “How to read it”macro_rules!
Section titled “macro_rules!”/// Generates the same newtype and trait impls for each ID type.macro_rules! id_type { ($($name:ident),+ $(,)?) => { $( #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct $name(pub u64);
impl fmt::Display for $name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}-{}", stringify!($name), self.0) } } )+ };}
id_type!(OrderId, CustomerId, InvoiceId);- Line 2:
macro_rules! id_typedefines a macro. It is used asid_type!(...). - Line 3: The pattern.
$name:identmatches one identifier.$(...),+repeats it, separated by commas, one or more times.$(,)?allows a trailing comma. - Line 4:
$( ... )+repeats the code inside once for each$namethat matched. - Line 10:
stringify!($name)turns the identifier into a string literal, such as"OrderId". - Line 17: The call. It generates three structs and three
Displayimpls.
You do not need to write macros like this. To review one, read the pattern (what it accepts) and the body (what it generates), then ask if a function, a generic, or a trait could do the same. Here the answer is no: a function cannot define new types, so a macro is a good fit.
See the expanded code
Section titled “See the expanded code”cargo expand prints the code after all macros are expanded. It is a separate tool: cargo install cargo-expand. In an editor, rust-analyzer can expand the macro under the cursor (“Expand macro recursively”). When a macro in a review is hard to read, look at its expansion.
What good looks like
Section titled “What good looks like”- Standard and well-known macros:
vec!,format!,assert_eq!,#[derive(...)],serdeandthiserrorderives. - Custom
macro_rules!only where a function cannot work: repeated type orimpldefinitions, or variable numbers of arguments. - Small macros with a doc comment that says what they generate.
- Derives instead of hand-written impls for
Debug,Clone,PartialEq,Hash,Default.
Red flags in AI output
Section titled “Red flags in AI output”- Custom
macro_rules!in application code where a function is enough. - Macros that hide control flow: a
returnor?inside the macro body. #[macro_use] extern crate: the 2015 way to import macros. Since the 2018 edition, macros are imported withuse. See S27: Outdated idioms (try!,extern crate,lazy_static, old edition).lazy_static!for a global value.std::sync::LazyLockreplaces it. See S27: Outdated idioms (try!,extern crate,lazy_static, old edition).- Procedural macro crates for small tasks, which add compile time.
- S27Severity: LowOutdated idioms (
try!,extern crate,lazy_static, old edition)Old patterns that the current standard library or edition replaces.
Better vs worse
Section titled “Better vs worse”The task: compute an order total, with checks on the input.
WorseTwo custom macros,
check!andprice_of!, each with a hiddenreturn.macro_rules! check {($cond:expr, $msg:expr) => {if !$cond {return Err($msg.to_string()); // a `return`, hidden inside the macro}};}macro_rules! price_of {($prices:expr, $item:expr) => {match $prices.get($item) {Some(p) => *p,None => return Err(format!("unknown item {}", $item)), // another hidden `return`}};}pub fn order_total(prices: &HashMap<&str, u64>, items: &[(&str, u32)]) -> Result<u64, String> {check!(!items.is_empty(), "empty order");let mut total = 0;for (item, qty) in items {check!(*qty > 0, "quantity must be positive");total += price_of!(prices, item) * u64::from(*qty);}Ok(total)}Run in Playground (opens the Rust Playground in a new tab) BetterPlain
ifandreturn, and a function that returnsResult, called with?.pub fn order_total(prices: &HashMap<&str, u64>, items: &[(&str, u32)]) -> Result<u64, String> {if items.is_empty() {return Err("empty order".to_string());}let mut total = 0;for &(item, qty) in items {if qty == 0 {return Err("quantity must be positive".to_string());}let price = price_of(prices, item)?; // the early return is visible: `?`total += price * u64::from(qty);}Ok(total)}fn price_of(prices: &HashMap<&str, u64>, item: &str) -> Result<u64, String> {prices.get(item).copied().ok_or_else(|| format!("unknown item {item}"))}Run in Playground (opens the Rust Playground in a new tab)
Both versions give the same results for the same input; the test both_versions_agree checks four cases. But in the “worse” version, order_total has three exits, and the code shows none of them. A reader who does not open the macro definitions thinks that the loop always runs to the end.
The “better” version is a few lines longer, and every exit is visible: two return statements and one ?.
Check yourself
Section titled “Check yourself”How do you recognize a macro call in Rust?
Show the answer
By the
!after the name (vec!,println!), or by an attribute such as#[derive(...)]or#[tokio::main].What is the Java equivalent of
#[derive(Debug, Clone, PartialEq)]?Show the answer
Code generated by Lombok, an annotation processor, or the IDE:
toString, a copy method, andequals. Java records give a part of it.Why is
println!a macro and not a function?Show the answer
It takes a variable number of arguments, and it checks the format string against them at compile time.
A function has the line
ensure_positive!(qty);. What do you check?Show the answer
The macro definition. It may contain a
returnor?, so the function may exit at that line.