Polyglot
Languages Rust macros
Rust § macros

Macros

Macros are code that runs at compile time and generates more code. Rust admits two kinds: declarative macros (defined with macro_rules!) — pattern-based rewriting; and procedural macros — Rust functions that take and produce token streams. The conventional standard-library macros (println!, vec!, format!, assert!, assert_eq!, dbg!, todo!) are written with macro_rules!. The derive macros (#[derive(Debug, Clone, ...)]) and many domain-specific macros (e.g., serde_derive, tokio::main) are procedural macros. The combination — declarative macros for pattern-based syntactic generation, procedural macros for elaborate code generation — admits substantial metaprogramming with hygienic, type-aware behaviour.

This page covers the principal macro forms, the conventional uses, and the macro_rules! syntax.

Why macros

Rust macros admit several things functions cannot:

  • Variable argumentsvec![1, 2, 3], println!("{}, {}", a, b).
  • Type-aware behaviour — different code generated based on the types involved.
  • Compile-time computation — assertions, format strings checked at compile time.
  • Code generation#[derive(Debug)] produces an entire trait implementation.
  • DSLs — domain-specific syntax embedded in Rust.

The ! distinguishes a macro invocation from a function call:

println!("hello");                              // macro
print_fn("hello");                              // function

Standard-library macros

The conventional standard-library macros:

print!("no newline");
println!("with newline");
eprint!("to stderr");
eprintln!("to stderr with newline");

let s = format!("{} is {}", name, value);

// Format specifiers:
println!("{}", x);                              // Display
println!("{:?}", x);                            // Debug
println!("{:#?}", x);                           // pretty Debug
println!("{:5}", n);                            // width 5
println!("{:05}", n);                           // zero-padded
println!("{:<5}", s);                           // left-align
println!("{:>5}", s);                           // right-align
println!("{:^5}", s);                           // centre
println!("{:.2}", x);                           // precision 2
println!("{:b}", n);                            // binary
println!("{:o}", n);                            // octal
println!("{:x}", n);                            // lowercase hex
println!("{:X}", n);                            // uppercase hex
println!("{:e}", x);                            // scientific

Format strings are checked at compile time; mismatches between specifiers and arguments produce compile errors. The mechanism is one of macros’ substantial advantages over runtime-based formatting.

Vectors

let v = vec![1, 2, 3, 4, 5];
let zeros = vec![0; 100];                       // 100 zeros

Assertions

assert!(condition);                              // panics if false
assert!(x > 0, "x must be positive, got {}", x);

assert_eq!(a, b);                                // panics if a != b
assert_ne!(a, b);                                // panics if a == b

debug_assert!(condition);                        // only in debug builds
debug_assert_eq!(a, b);                          // only in debug builds

Panics and placeholders

panic!("unrecoverable");
panic!("error: {}", message);

todo!();                                         // for "I'll implement this later"
todo!("handle the {} case", variant);

unimplemented!();                                // for "this is not implemented"
unreachable!();                                  // for "this code can't be reached"

The panic-style macros admit substantial flexibility for the conventional development workflow.

Debugging

let v = vec![1, 2, 3];
dbg!(&v);                                        // prints "[1, 2, 3]" with file:line
let x = dbg!(compute_value());                   // prints and returns

The dbg! is a substantial improvement over manual println! for debugging.

Logging

The standard-library does not include logging macros; the third-party log crate provides them:

use log::{trace, debug, info, warn, error};

trace!("low-level detail");
debug!("debug info: {:?}", state);
info!("server started on port {}", port);
warn!("retrying after {} failures", attempts);
error!("failed to connect: {}", e);

Concurrency

use std::thread;

let handle = thread::spawn(|| {                  // function, not macro
    // ...
});

Tokio (third-party):

tokio::spawn(async {                             // function
    // ...
});

#[tokio::main]                                    // attribute macro
async fn main() {
    // ...
}

macro_rules!

Declarative macros are defined with macro_rules!:

macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
}

say_hello!();

The form: macro_rules! name { (pattern) => { body }; }. The pattern matches the macro invocation; the body is the substituted code.

Captures

Macros admit capturing parts of their input as fragment specifiers:

macro_rules! square {
    ($x:expr) => {
        $x * $x
    };
}

let result = square!(5);                         // 25
let result = square!(2 + 3);                     // 25 (the parens are added)

The $x:expr captures an expression. The principal fragment specifiers:

SpecifierMatches
$x:exprAn expression
$x:tyA type
$x:identAn identifier
$x:patA pattern
$x:stmtA statement
$x:blockA block
$x:itemAn item (fn, struct, etc.)
$x:literalA literal
$x:ttA token tree (anything that fits in matching brackets)
$x:metaA meta-item (inside #[...])
$x:visA visibility (pub, etc.)
$x:pathA path

Repetition

The $( ... ),* admits matching repeated input:

macro_rules! my_vec {
    ($($x:expr),* $(,)?) => {
        {
            let mut v = Vec::new();
            $(
                v.push($x);
            )*
            v
        }
    };
}

let v = my_vec![1, 2, 3];
let v = my_vec![1, 2, 3,];                       // trailing comma admitted

The $( ... ),* admits zero or more comma-separated repetitions; $( ... ),+ admits one or more. The $(,)? admits an optional trailing comma. The repetition appears both in the pattern and in the body — the body is repeated for each match.

Multiple arms

A macro may have several arms, tried in order:

macro_rules! min {
    ($x:expr) => { $x };
    ($x:expr, $($rest:expr),+) => {
        std::cmp::min($x, min!($($rest),+))
    };
}

let m = min!(3, 1, 4, 1, 5, 9, 2, 6);            // 1

The recursive form admits arbitrary-arity macros.

A vec! lookalike

macro_rules! my_vec {
    () => { Vec::new() };
    ($($x:expr),+ $(,)?) => {{
        let mut v = Vec::new();
        $( v.push($x); )+
        v
    }};
    ($x:expr; $n:expr) => {{
        let mut v = Vec::with_capacity($n);
        for _ in 0..$n {
            v.push($x);
        }
        v
    }};
}

let v1 = my_vec!();
let v2 = my_vec![1, 2, 3];
let v3 = my_vec![0; 100];

Hygiene

Rust macros are hygienic — they do not capture identifiers from the call site:

macro_rules! using_a {
    () => {
        let a = 5;                              // local to the macro
        println!("{}", a);
    };
}

let a = 99;
using_a!();
println!("{}", a);                               // 99 (not affected)

The hygiene admits writing macros without worrying about identifier collisions.

Exporting macros

#[macro_export]
macro_rules! my_macro {
    () => { println!("hi"); };
}

The #[macro_export] admits using the macro from other crates. Without it, the macro is module-private.

The conventional access from another crate:

use my_crate::my_macro;
// or: use my_crate::*;

my_macro!();

Procedural macros

Procedural macros are Rust functions that operate on token streams. They are conventionally defined in a separate proc-macro crate. There are three kinds:

Custom #[derive] macros

Auto-implement traits:

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct Person {
    name: String,
    age: u32,
}

The Debug, Clone, PartialEq are standard-library derives; Serialize and Deserialize are from the serde_derive crate. Each #[derive(...)] produces a complete trait implementation at compile time.

Attribute macros

Apply to items:

#[tokio::main]
async fn main() {
    // ...
}

#[get("/")]
fn index() -> &'static str {
    "Hello"
}

The #[tokio::main] rewrites the main function to set up a Tokio runtime; #[get("/")] (from a web framework) registers an HTTP handler.

Function-like macros

Like macro_rules! but defined procedurally:

let s = sql!(SELECT * FROM users WHERE id = 1);

The principal use is for elaborate parsing (SQL DSLs, query builders, etc.) where macro_rules! patterns are inadequate.

Defining a derive macro

// In a separate proc-macro crate:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Hello)]
pub fn derive_hello(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = input.ident;

    let expanded = quote! {
        impl Hello for #name {
            fn hello(&self) {
                println!("Hello from {}!", stringify!(#name));
            }
        }
    };

    TokenStream::from(expanded)
}

The syn crate parses Rust syntax; quote generates token streams. The mechanism is substantial but admits arbitrary code generation.

Common patterns

Logging macro

macro_rules! log_info {
    ($($arg:tt)*) => {
        eprintln!("[INFO] {}", format!($($arg)*));
    };
}

log_info!("server started on port {}", port);
log_info!("processing {} items", items.len());

Compile-time format string check

The format! and println! macros check the format string at compile time:

let s = format!("{} is {}", name, value);       // OK
let s = format!("{}", a, b);                     // ERROR at compile time: too many args
let s = format!("{} and {}", a);                 // ERROR at compile time: too few args

Hash map literal

macro_rules! hashmap {
    ( $($k:expr => $v:expr),* $(,)? ) => {{
        let mut m = std::collections::HashMap::new();
        $( m.insert($k, $v); )*
        m
    }};
}

let m = hashmap! {
    "a" => 1,
    "b" => 2,
    "c" => 3,
};

Newtype with display

macro_rules! newtype_display {
    ($name:ident, $inner:ty) => {
        struct $name($inner);

        impl std::fmt::Display for $name {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(f, "{}", self.0)
            }
        }
    };
}

newtype_display!(UserId, u64);

let id = UserId(42);
println!("{}", id);                              // 42

Compile-time assertion

const _: () = assert!(std::mem::size_of::<usize>() >= 4);

The form admits compile-time invariants on type sizes and other constants.

Test scaffolding

macro_rules! parse_test {
    ($name:ident, $input:expr, $expected:expr) => {
        #[test]
        fn $name() {
            assert_eq!(parse($input), $expected);
        }
    };
}

parse_test!(parses_zero, "0", 0);
parse_test!(parses_negative, "-5", -5);
parse_test!(parses_large, "1000000", 1_000_000);

The pattern admits substantial conciseness for table-driven tests.

A note on debugging macros

cargo expand (third-party tool) admits seeing the expanded code:

cargo install cargo-expand
cargo expand

The expanded form admits debugging macro behaviour and understanding what derives produce.

A note on the conventional discipline

The contemporary Rust macro advice:

  • Use println!, vec!, format!, assert!, dbg! freely — they’re the foundation.
  • Use #[derive] for Debug, Clone, PartialEq, Hash, etc.
  • Write macro_rules! for genuine repetition you can’t express with functions or generics.
  • Reach for procedural macros only when macro_rules! is inadequate.
  • Prefer functions and generics over macros — they admit better tooling, error messages, and IDE support.
  • Use #[macro_export] when the macro is part of a crate’s public API.
  • Use cargo expand to debug elaborate macros.

The combination — declarative macros for pattern-based generation, procedural macros for arbitrary code generation, hygienic identifier handling, compile-time format checking — admits substantial metaprogramming. The conventional discipline is to reach for macros only when functions and generics are inadequate; when macros are warranted, the language admits substantial expressiveness.