Macros in Rust
Macros are one of Rust's most distinctive features. At their core, macros are
code that writes code — a technique called metaprogramming. You have already
used macros extensively: every time you call println!, vec!, or assert_eq!,
you are invoking a macro, not a function.
The exclamation mark ! is the universal signal that something is a macro rather
than a regular function call. This single character tells you — and your editor —
that expansion is happening at compile time, not a normal function dispatch at runtime.
Why Does Rust Need Macros?
Rust's type system and function abstraction are extremely powerful — but they have limits that macros are designed to fill.
Variadic arguments — functions in Rust have a fixed number of parameters.
println!accepts any number of arguments of any type. That is only possible with a macro.Code generation — writing repetitive implementations by hand is error-prone. Macros generate them automatically at compile time.
Domain-specific syntax — macros can accept syntax that is not valid Rust (like SQL queries or HTML templates) and transform it into valid Rust code.
Type-level computation — some operations need to happen before the type checker even runs; only macros can do this.
Compile-time constants — embedding file contents or environment variables requires values to be known at compile time, not runtime.
Two Families of Macros
Rust has two fundamentally different kinds of macros. Understanding the distinction matters because they work in completely different ways and suit different tasks.
Family | How it works | Typical use |
|---|---|---|
Declarative ( | Pattern matching on syntax trees | Repetitive patterns, DSLs, simple code generation |
Procedural (proc macros) | Rust functions that receive and return token streams | Custom derive, attribute-based transformations, complex generation |
Declarative macros use a macro_rules! block with pattern arms — similar to a
match expression but operating on syntax rather than runtime values.
Procedural macros are separate crates that implement a Rust function. The function receives a stream of tokens representing the input code and returns a new stream of tokens to substitute in. They are more powerful but considerably more complex to write.
Output Macros
The most immediately familiar macros are those that produce output or format strings.
fn main() {
// println! — print to stdout with a trailing newline
println!("Hello, {}!", "world");
// print! — print to stdout WITHOUT a newline
print!("count: ");
print!("1 ");
print!("2 ");
println!("3");
// eprintln! — print to stderr (useful for diagnostics and error messages)
eprintln!("Warning: retrying connection...");
// format! — create a String without printing it
let greeting = format!("Hello, {}! You are {} years old.", "Alice", 30);
println!("{}", greeting);
}Hello, world! count: 1 2 3 Warning: retrying connection... Hello, Alice! You are 30 years old.
eprintln! writes to standard error (stderr), not standard output. This means it appears in the terminal but can be separated from normal output when redirecting streams — useful in scripts and CI pipelines.Collection Macros
fn main() {
// vec! — create a Vec with initial values
let numbers = vec![1, 2, 3, 4, 5];
// Repeat syntax: N copies of a value
let zeros: Vec<i32> = vec![0; 5];
println!("{:?}", numbers);
println!("{:?}", zeros);
}[1, 2, 3, 4, 5] [0, 0, 0, 0, 0]
vec! you would write Vec::new() followed by repeated .push() calls. The macro generates exactly that code — it is pure syntactic convenience with zero runtime overhead.Assertion Macros
Rust provides several assertion macros invaluable in tests and defensive programming.
They all panic! with an informative message when the condition fails.
fn main() {
let x = 5;
let y = 5;
assert!(x > 0, "x must be positive, got {}", x);
assert_eq!(x, y, "x and y should be equal");
assert_ne!(x, 0, "x must not be zero");
println!("All assertions passed!");
}
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
panic!("Division by zero is not allowed");
}
a / b
}All assertions passed!
Macro | Purpose |
|---|---|
| Panics if the expression evaluates to false |
| Panics if a != b; prints both values on failure |
| Panics if a == b; prints both values on failure |
| Unconditionally terminates with the given message |
Placeholder Macros
Three macros serve as intentional placeholders during development. They all panic at runtime, but their names communicate why a code path is not yet available.
fn process(input: &str) -> String {
todo!("implement string processing for: {}", input)
}
fn legacy_api() {
unimplemented!("this API was removed in v2.0 — use process() instead")
}
fn classify(n: i32) -> &'static str {
match n {
i32::MIN..=-1 => "negative",
0 => "zero",
1..=i32::MAX => "positive",
_ => unreachable!("all i32 values are covered above"),
}
}
fn main() {
println!("{}", classify(42));
println!("{}", classify(-7));
println!("{}", classify(0));
}positive negative zero
todo!()— marks code you intend to write later; panics if reached at runtimeunimplemented!()— marks functionality intentionally absent; clearer thantodo!for removed APIsunreachable!()— documents that a code path should logically never execute
The dbg! Macro
dbg! is a debug-printing macro with a superpower: it prints the file name, line
number, and the evaluated value of an expression — then returns the value. You
can insert it anywhere inside an expression without restructuring your code.
fn main() {
let a = 2;
// dbg! prints the debug info AND passes the value through
let b = dbg!(a * 2) + 1;
println!("b = {}", b);
let numbers = vec![1, 2, 3];
let doubled: Vec<i32> = numbers
.iter()
.map(|&x| dbg!(x * 2))
.collect();
println!("{:?}", doubled);
}[src/main.rs:3] a * 2 = 4 [src/main.rs:9] x * 2 = 2 [src/main.rs:9] x * 2 = 4 [src/main.rs:9] x * 2 = 6 b = 5 [2, 4, 6]
dbg! calls before committing production code. They write to stderr, which is noisy in production. Many teams add a CI lint rule to catch stray dbg! calls before they reach the main branch.Compile-Time Macros
Several macros do their work entirely at compile time, embedding information directly into the binary with zero runtime overhead.
// include_str! — embed a text file as a &str at compile time
const README: &str = include_str!("../README.md");
// include_bytes! — embed a binary file as a &[u8] at compile time
// const ICON: &[u8] = include_bytes!("../assets/icon.png");
// env! — read a Cargo environment variable at compile time
const VERSION: &str = env!("CARGO_PKG_VERSION");
const PKG_NAME: &str = env!("CARGO_PKG_NAME");
// concat! — concatenate literals into a &str at compile time
const BANNER: &str = concat!("Welcome to ", env!("CARGO_PKG_NAME"), "!");
// cfg! — evaluate compilation flags as a bool at runtime
fn main() {
println!("{} v{}", PKG_NAME, VERSION);
println!("{}", BANNER);
if cfg!(debug_assertions) {
println!("Running in DEBUG mode — extra checks are active");
} else {
println!("Running in RELEASE mode — optimised build");
}
}include_str! and include_bytes! are resolved relative to the source file containing the macro call, not the current working directory. The file must exist at compile time; it does not need to be present at runtime.Macro Hygiene
One of Rust's most important macro guarantees is hygiene: a macro cannot accidentally capture or interfere with variables in the calling scope.
In languages with unhygienic macros (like the C preprocessor), a macro could shadow a variable name from the call site, causing subtle and hard-to-find bugs. Rust prevents this by keeping macro-introduced variable names scoped to the expansion.
macro_rules! compute {
($x:expr) => {{
// 'result' here lives in the macro's own scope
let result = $x + 5;
result
}};
}
fn main() {
let result = 100; // caller's 'result'
let answer = compute!(20);
println!("caller result = {}", result); // still 100 — not overwritten
println!("answer = {}", answer); // 25
}caller result = 100 answer = 25
When Macros Are Expanded
Macros are expanded before type checking occurs. The compiler transforms all macro calls into regular Rust code first, then runs the type checker and borrow checker on the fully expanded result. This ordering has important practical consequences.
A macro can generate code referencing types not visible at the call site — the types just need to exist after expansion
Syntax errors inside macros often produce confusing error messages pointing to generated code, not the macro call
Macros cannot inspect the types of their arguments — they only see syntax tokens
Macro expansion order is deterministic: dependencies are expanded before your own crate
Functions vs Macros at a Glance
Aspect | Functions | Macros |
|---|---|---|
Call syntax |
|
|
Argument count | Fixed (defined in signature) | Variable — any number or syntax structure |
Type checking | At the call site | After expansion on generated code |
Overloading | Not supported | Multiple match arms simulate overloading |
Return value | Single typed return | Expands to arbitrary Rust code |
Debuggability | Easy — stack traces point here | Harder — errors point into generated code |
When to use | Default choice for reusable logic | When functions genuinely cannot do the job |
Common Macros Reference
Macro | Category | What it does |
|---|---|---|
| Output | Print formatted string to stdout with newline |
| Output | Print formatted string to stdout, no newline |
| Output | Print formatted string to stderr with newline |
| String | Build a String from a format template |
| Collections | Create a Vec with initial elements |
| Control flow | Terminate with an error message |
| Testing | Panic if condition is false |
| Testing | Panic if two values are not equal |
| Testing | Panic if two values are equal |
| Placeholder | Mark unimplemented code — panics if reached |
| Placeholder | Mark intentionally absent functionality |
| Placeholder | Assert a code path is never executed |
| Debugging | Print expression + value to stderr, return value |
| Compile-time | Embed a text file as &str |
| Compile-time | Embed a binary file as &[u8] |
| Compile-time | Read an environment variable at compile time |
| Compile-time | Concatenate literals into a &str |
| Compile-time | Check compilation flags, returns a bool |