Macros

Macro Design Principles

Writing maintainable, hygienic, and fast-compiling macros in Rust follows a clear set of design rules that prevent subtle double-evaluation bugs, name-resolution breakages, and binary bloat.

1. Bind $expr Arguments to Local Variables Once (Prevent Multiple Evaluation)

Just like C macros, substituting a metavariable $x:expr twice inside a macro body evaluates the expression twice at runtime—duplicating work or running side effects twice! Always bind $x:expr to a single local let variable (or match subject) at the top of the macro block:

// BAD: Evaluates `$x` twice if `$x` is a function call with side effects!
macro_rules! square_bad {
    ($x:expr) => {
        $x * $x
    };
}

// GOOD: Evaluates `$x` once and binds it to `val` in a hygienic block:
macro_rules! square_good {
    ($x:expr) => {{
        let val = $x;
        val * val
    }};
}

fn fetch_and_log(n: i32) -> i32 {
    println!("  [side effect] fetch_and_log({n}) called");
    n
}

fn main() {
    println!("Calling square_bad:");
    let a = square_bad!(fetch_and_log(5));

    println!("Calling square_good:");
    let b = square_good!(fetch_and_log(5));

    println!("a = {a}, b = {b}");
}
Calling square_bad:
  [side effect] fetch_and_log(5) called
  [side effect] fetch_and_log(5) called
Calling square_good:
  [side effect] fetch_and_log(5) called
a = 25, b = 25

2. Use Fully Qualified Paths ($crate:: and ::core:: / ::std::)

Because macro_rules! resolves type, trait, and module paths at the call site, a caller module might not have imported a helper trait or might have shadowed Result or Option with a custom type alias. In exported macros:

  • Prefix your own crate's items with $crate:: ($crate::internal::Helper).
  • Prefix standard library items with ::core:: or ::std:: (::core::result::Result::Ok(x), ::core::option::Option::Some(x)).

3. Keep Macro Expansions Thin (Delegate Heavy Logic to Regular Functions)

Every time a macro expands 100 lines of code at 50 call sites, the compiler must parse, type-check, borrow-check, and optimize all 5,000 lines of expanded code. Instead, have the macro expand into a 1–3 line call to a regular generic or non-generic helper function:

// Heavy logic lives in a normal, type-checked, compiled-once function:
pub fn log_structured_impl(file: &str, line: u32, key: &str, value: &dyn std::fmt::Debug) {
    println!("{file}:{line} | {key}={value:?}");
}

// The macro only captures caller location (`file!()`, `line!()`) and forwards to the function:
macro_rules! log_kv {
    ($key:expr, $val:expr) => {
        log_structured_impl(file!(), line!(), $key, &$val)
    };
}

fn main() {
    log_kv!("port", 8080);
    log_kv!("hosts", ["db-1", "db-2"]);
}
src/main.rs:14 | port=8080
src/main.rs:15 | hosts=["db-1", "db-2"]

Summary Checklist for Macro Authors

Principle Practice
Prefer functions/generics first Only write a macro when variadic arity, item generation, or token inspection is required
Single evaluation Bind $x:expr to let val = $x; (or match $x { val => ... }) if referenced more than once
Trailing commas Add $(,)? to comma-separated repetition rules so multi-line calls format cleanly with rustfmt
Path hygiene Use $crate:: for local items and ::core:: / ::std:: for standard library types and traits
Thin expansions Delegate runtime work from the macro transcriber to a regular Rust function