Skip to content

Macros

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]).

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 toString
public class Line {
private String item;
private long cents;
}

Rust

#[derive(Debug, Clone, PartialEq, Eq)] // a derive macro: generates trait impls
pub 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).

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.

#[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.

  • 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! and matches! 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, and tokio provide them. They are the Rust equivalent of annotation processors.
/// 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);
  1. Line 2: macro_rules! id_type defines a macro. It is used as id_type!(...).
  2. Line 3: The pattern. $name:ident matches one identifier. $(...),+ repeats it, separated by commas, one or more times. $(,)? allows a trailing comma.
  3. Line 4: $( ... )+ repeats the code inside once for each $name that matched.
  4. Line 10: stringify!($name) turns the identifier into a string literal, such as "OrderId".
  5. Line 17: The call. It generates three structs and three Display impls.

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.

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.

  • Standard and well-known macros: vec!, format!, assert_eq!, #[derive(...)], serde and thiserror derives.
  • Custom macro_rules! only where a function cannot work: repeated type or impl definitions, 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.

The task: compute an order total, with checks on the input.

  1. WorseTwo custom macros, check! and price_of!, each with a hidden return.

    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)
  2. BetterPlain if and return, and a function that returns Result, 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 ?.

  1. 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].

  2. 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, and equals. Java records give a part of it.

  3. 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.

  4. A function has the line ensure_positive!(qty);. What do you check?

    Show the answer

    The macro definition. It may contain a return or ?, so the function may exit at that line.