RustMacros

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 (macro_rules!)

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.

RUST
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.
Note
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

RUST
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]
Tip
Without 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.

RUST
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

assert!(expr)

Panics if the expression evaluates to false

assert_eq!(a, b)

Panics if a != b; prints both values on failure

assert_ne!(a, b)

Panics if a == b; prints both values on failure

panic!(msg)

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.

RUST
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 runtime

  • unimplemented!() — marks functionality intentionally absent; clearer than todo! for removed APIs

  • unreachable!() — 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.

RUST
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]
Warning
Remove 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.

RUST
// 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");
    }
}
Note
Paths in 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.

RUST
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

foo(args)

foo!(args) or foo![args] or foo!{args}

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

println!

Output

Print formatted string to stdout with newline

print!

Output

Print formatted string to stdout, no newline

eprintln!

Output

Print formatted string to stderr with newline

format!

String

Build a String from a format template

vec!

Collections

Create a Vec with initial elements

panic!

Control flow

Terminate with an error message

assert!

Testing

Panic if condition is false

assert_eq!

Testing

Panic if two values are not equal

assert_ne!

Testing

Panic if two values are equal

todo!

Placeholder

Mark unimplemented code — panics if reached

unimplemented!

Placeholder

Mark intentionally absent functionality

unreachable!

Placeholder

Assert a code path is never executed

dbg!

Debugging

Print expression + value to stderr, return value

include_str!

Compile-time

Embed a text file as &str

include_bytes!

Compile-time

Embed a binary file as &[u8]

env!

Compile-time

Read an environment variable at compile time

concat!

Compile-time

Concatenate literals into a &str

cfg!

Compile-time

Check compilation flags, returns a bool

Success
Macros are not magic — they are a well-defined layer of the Rust compilation pipeline that runs before type checking. Understanding when to reach for a macro versus a function, and knowing which built-in macros are available, makes you significantly more productive in everyday Rust code.