Files
Rodin d3c5780e13 docs: macro patterns from rust-lang/rust
10 patterns, 519 lines. Full spec compliance.
Patterns: macro_rules!, trailing comma, $crate hygiene, multiple
arms, #[derive], attribute macros, type-level repetition, built-in
macros, format macros, macro visibility/export.
2026-04-30 15:13:10 -07:00

14 KiB

Rust Macro Patterns

Patterns for declarative and procedural macros in Rust, extracted from the standard library source.

Source: rust-lang/rust at commit f53b654

Stats: 607 macro_rules! definitions, 63 #[macro_export], 1,116 #[derive(...)] usages, 28,639 #[inline] annotations.


1. macro_rules! for Repetitive Code

Source:

library/alloc/src/macros.rs (vec! macro)

607 macro_rules! definitions in the stdlib.

#[macro_export]
macro_rules! vec {
    () => ( $crate::vec::Vec::new() );
    ($elem:expr; $n:expr) => ( $crate::vec::from_elem($elem, $n) );
    ($($x:expr),+ $(,)?) => (
        $crate::boxed::box_assume_init_into_vec_unsafe(
            $crate::intrinsics::write_box_via_move(
                $crate::boxed::Box::new_uninit(), [$($x),+]
            )
        )
    );
}

Why

macro_rules! eliminates repetitive boilerplate that can't be abstracted with generics or traits. The vec! macro lets you write vec![1, 2, 3] instead of constructing and pushing manually. Macros operate on syntax trees — they can generate code that functions can't.

When to Use

Triggers:

  • Repetitive code that differs only in types or count
  • Syntax sugar that makes common patterns readable
  • Implementing traits for many types at once (numeric types)
  • Generating test cases from a table

Example — before:

// Implementing Add for 10 numeric types manually:
impl Add for u8 { type Output = u8; fn add(self, rhs: u8) -> u8 { ... } }
impl Add for u16 { type Output = u16; fn add(self, rhs: u16) -> u16 { ... } }
impl Add for u32 { type Output = u32; fn add(self, rhs: u32) -> u32 { ... } }
// ... 7 more identical implementations

Example — after:

macro_rules! impl_add {
    ($($t:ty),*) => {
        $(
            impl Add for $t {
                type Output = $t;
                fn add(self, rhs: $t) -> $t { self + rhs }
            }
        )*
    }
}

impl_add!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128);

When NOT to Use

Don't use this when:

  • A generic function or trait works (prefer generics)
  • The macro is used in only 1-2 places (inline the code)
  • The macro makes code harder to understand/debug
  • Error messages from the macro would be confusing

2. Trailing Comma Support ($( ,)?)

Source:

Standard pattern in all stdlib macros:

macro_rules! vec {
    ($($x:expr),+ $(,)?) => { ... }
    //                ^^^^ optional trailing comma
}

Why

Without $(,)?, users get cryptic errors when they add a trailing comma (common when formatting multi-line macro invocations). Every public macro should accept trailing commas.

When to Use

Always. Every macro with comma-separated arguments should accept an optional trailing comma.

Example — before:

macro_rules! my_macro {
    ($($x:expr),+) => { ... }
}

my_macro!(1, 2, 3,);  // ERROR: no rules matched — confusing

Example — after:

macro_rules! my_macro {
    ($($x:expr),+ $(,)?) => { ... }
}

my_macro!(1, 2, 3,);  // Works!
my_macro!(1, 2, 3);   // Also works!

3. $crate for Hygiene

Source:

All #[macro_export] macros in the stdlib use $crate:

macro_rules! vec {
    () => ( $crate::vec::Vec::new() );
    //      ^^^^^^ hygienic path to the defining crate
}

Why

$crate resolves to the crate where the macro is defined, regardless of where it's used. Without it, macros break when the user has a different name for your crate or doesn't import the right modules.

When to Use

Triggers:

  • Any #[macro_export] macro that references items from its own crate
  • Macros that will be used in other crates

Example — before:

#[macro_export]
macro_rules! create_thing {
    () => { Thing::new() }
    // BROKEN: if user doesn't have `Thing` in scope, this fails
}

Example — after:

#[macro_export]
macro_rules! create_thing {
    () => { $crate::Thing::new() }
    // Works regardless of user's imports — $crate is always valid
}

When NOT to Use

Don't use this when:

  • The macro is not exported (crate-internal macros)
  • You're referencing items from other crates (use full path)

4. Multiple Match Arms (Overloaded Syntax)

Source:

library/core/src/macros/mod.rs (assert_eq!)

macro_rules! assert_eq {
    ($left:expr, $right:expr $(,)?) => { ... };
    ($left:expr, $right:expr, $($arg:tt)+) => { ... };
}

Why

Multiple arms let one macro serve different use cases. assert_eq! works both with and without a custom message. The most specific arm should come first (Rust matches top-to-bottom).

When to Use

Triggers:

  • A macro should work with different numbers of arguments
  • You want syntactic variants (with/without options)
  • Default behavior when optional arguments are omitted

Example — before:

// Two separate macros — awkward
macro_rules! log { ($msg:expr) => { println!("{}", $msg) } }
macro_rules! log_with_level { ($level:expr, $msg:expr) => { ... } }

Example — after:

macro_rules! log {
    ($msg:expr) => { log!(INFO, $msg) };                    // default level
    ($level:expr, $msg:expr) => {
        println!("[{}] {}", stringify!($level), $msg);
    };
    ($level:expr, $fmt:expr, $($arg:tt)*) => {              // format args
        println!("[{}] {}", stringify!($level), format!($fmt, $($arg)*));
    };
}

log!("simple");              // [INFO] simple
log!(WARN, "watch out");     // [WARN] watch out
log!(ERROR, "code {}", 42); // [ERROR] code 42

5. #[derive] Custom Derive Macros

Source:

1,116 #[derive(...)] usages. Common derives: Debug (869), Clone (880), Copy (537), PartialEq (388), Eq (285).

Why

Derive macros generate trait implementations automatically at compile time. The compiler's built-in derives handle standard traits. Third-party derives (serde, thiserror) can generate complex code from annotations.

When to Use

Triggers:

  • Standard trait implementations (Debug, Clone, PartialEq, etc.)
  • Serialization (serde::Serialize/Deserialize)
  • Error generation (thiserror::Error)
  • Any repetitive trait impl that follows a pattern

Example — before:

struct Config { host: String, port: u16, debug: bool }

impl fmt::Debug for Config {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Config")
            .field("host", &self.host)
            .field("port", &self.port)
            .field("debug", &self.debug)
            .finish()
    }
}
impl Clone for Config { ... }
impl PartialEq for Config { ... }
// 50 lines of mechanical code

Example — after:

#[derive(Debug, Clone, PartialEq)]
struct Config { host: String, port: u16, debug: bool }
// One line. Same result.

6. Attribute Macros for Cross-Cutting Concerns

Source:

#[test]
fn it_works() { ... }

#[inline]
pub fn fast_path() { ... }

#[cfg(target_os = "linux")]
fn linux_only() { ... }

28,639 #[inline] annotations in library/.

Why

Attribute macros annotate items with metadata or transform them. Built-in attributes (test, inline, cfg) direct the compiler. Custom attribute macros (proc macros) can rewrite the annotated item entirely.

When to Use

Triggers:

  • Performance hints (#[inline], #[cold])
  • Test marking (#[test], #[bench])
  • Conditional compilation (#[cfg(...)])
  • Framework integration (#[tokio::main], #[actix_web::get])

When NOT to Use

Don't use this when:

  • A regular function call would suffice
  • The attribute hides too much magic (hard to debug)
  • You're using #[inline] without benchmarking (let the compiler decide)

7. macro_rules! for Type-Level Repetition

Source:

The stdlib uses macros to implement traits for all numeric types:

// core/src/num/mod.rs pattern (simplified)
macro_rules! int_impl {
    ($Type:ty, $BITS:expr) => {
        impl $Type {
            pub const BITS: u32 = $BITS;
            pub const MIN: Self = -(1 << ($BITS - 1));
            pub const MAX: Self = (1 << ($BITS - 1)) - 1;

            pub const fn wrapping_add(self, rhs: Self) -> Self {
                // identical for all integer types
                intrinsics::wrapping_add(self, rhs)
            }
        }
    }
}

int_impl!(i8, 8);
int_impl!(i16, 16);
int_impl!(i32, 32);
int_impl!(i64, 64);
int_impl!(i128, 128);

Why

When you need the SAME implementation for many types but generics don't work (e.g., intrinsics that take specific types, or const values that differ per type), macros generate the repetitive code.

When to Use

Triggers:

  • Same implementation for many concrete types
  • Generics can't express the constraint (type-level constants)
  • The difference between types is a value, not a behavior

When NOT to Use

Don't use this when:

  • Generics with trait bounds work (always prefer generics)
  • The implementations actually differ in behavior (not just types)
  • The macro output is so large it hurts compile time

8. concat!, stringify!, file!, line! (Built-in Macros)

Source:

// Built-in compiler macros used throughout:
panic!("assertion failed at {}:{}", file!(), line!());
const NAME: &str = concat!("v", env!("CARGO_PKG_VERSION"));

Why

Built-in macros provide compile-time information that can't be obtained any other way: file names, line numbers, environment variables, string concatenation at compile time.

When to Use

Triggers:

  • Error messages with location info (file!(), line!())
  • Compile-time string building (concat!())
  • Compile-time assertions (compile_error!())
  • Environment info (env!(), option_env!())

9. Format Macros (println!, format!, write!)

Source:

The format macro family is the most-used macro system in Rust:

println!("Hello, {name}!");            // named argument
format!("{value:.2}");                  // format specifier
write!(f, "{:#?}", self)?;             // Debug with pretty-print
eprintln!("error: {err}");             // stderr
log::info!("request from {ip}");       // logging crate pattern

Why

Format macros are type-safe at compile time. println!("{}", x) verifies that x implements Display. The compiler checks argument count and types — no runtime format string bugs.

When to Use

Triggers:

  • String interpolation (building strings from values)
  • Logging (use format macros, not string concatenation)
  • Debug output ({:?} for Debug, {:#?} for pretty-print)

When NOT to Use

Don't use this when:

  • Building a string in a loop (push_str is more efficient than repeated format!)
  • The format string is determined at runtime (use write! with explicit formatter)

10. Macro Visibility and Export

Source:

63 #[macro_export] macros in the stdlib.

// Exported — available to users of the crate
#[macro_export]
macro_rules! vec { ... }

// Not exported — internal to the crate
macro_rules! internal_helper { ... }

Why

By default, macro_rules! macros are scoped to the module they're defined in. #[macro_export] puts them at the crate root (available via use my_crate::my_macro). This is intentional — most macros are implementation details.

When to Use

Triggers:

  • #[macro_export]: Users should be able to call this macro
  • No export: Internal helper macro used within your crate only

When NOT to Use

Don't use this when:

  • A function would work (macros are harder to debug/document)
  • The macro is only used in one module (keep it local)

Summary: Macro Decision Tree

Do you need a macro?
├── Can a function do it? → NO MACRO (use a function)
├── Can generics do it? → NO MACRO (use generics)
├── Need syntax transformation? → Macro
│   ├── Declarative (pattern matching)? → macro_rules!
│   │   ├── Internal only → no #[macro_export]
│   │   └── Public → #[macro_export] + $crate paths
│   └── Complex code generation? → Proc macro
│       ├── Derive impl → #[derive(MyTrait)]
│       ├── Attribute → #[my_attr]
│       └── Function-like → my_macro!(...)
└── Need compile-time info? → Built-in macros

Writing macro_rules?
├── Accept trailing commas? → $(,)? (ALWAYS)
├── Multiple arg counts? → Multiple match arms
├── Used across crates? → $crate:: paths (hygiene)
└── Repetition over types? → $($Type:ty),* pattern
Pattern Use when
macro_rules! Repetitive code that can't use generics
Trailing comma $(,)? Every comma-separated macro (always)
$crate paths Exported macros referencing own crate items
Multiple arms Overloaded macro syntax
#[derive] Automatic trait implementations
Attribute macros Cross-cutting concerns (test, inline, cfg)
Type-level repetition Same impl for many concrete types
Built-in macros Compile-time info (file, line, env)
Format macros Type-safe string interpolation
#[macro_export] Making macros available to users

See also: