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 arguments —
vec![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 and format
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:
| Specifier | Matches |
|---|---|
$x:expr | An expression |
$x:ty | A type |
$x:ident | An identifier |
$x:pat | A pattern |
$x:stmt | A statement |
$x:block | A block |
$x:item | An item (fn, struct, etc.) |
$x:literal | A literal |
$x:tt | A token tree (anything that fits in matching brackets) |
$x:meta | A meta-item (inside #[...]) |
$x:vis | A visibility (pub, etc.) |
$x:path | A 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]forDebug,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 expandto 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.