Updated Jul 30, 2026

16. Macros

📋 Jump to Takeaways

🎁 You've called println!, vec!, assert!, and panic! throughout this course. Did you notice the !? That's not decoration — it means something specific. These aren't functions. They're macros, and they work in a fundamentally different way.

What Are Macros

Macros run at compile time and generate code. The ! suffix is how Rust distinguishes a macro call from a function call.

The key thing macros can do that functions can't: accept a variable number of arguments with different types in the same position.

println!("hello");               // 1 arg
println!("x = {}", x);          // 2 args
println!("{} + {} = {}", a, b, a + b); // 4 args

A function signature is fixed — it can't take 1, 2, or 4 arguments depending on the call site. A macro matches patterns in your source code and rewrites them before compilation. The compiler never sees the macro call, only the generated code.

Rust has no variadic functions. Go has ...T — you can write func sum(nums ...int) and pass any number of ints. Rust deliberately omits this. Macros are the answer instead, and they're more powerful: a Go variadic is limited to one type, while a Rust macro can accept mixed types, generate arbitrary code, and capture source locations. The trade-off is that macros are harder to write and debug than functions.

Macro Delimiters

You've seen macros called with (), [], and {}. All three work for any macro — Rust accepts them all:

vec![1, 2, 3]  // conventional
vec!(1, 2, 3)  // also valid
vec!{1, 2, 3}  // also valid

The choice is purely convention, not syntax. The community settled on delimiters that make the call site look like what it produces:

  • [] for macros that produce collections (vec!, matches!)
  • () for macros that look like function calls (println!, assert!, panic!)
  • {} for macros that contain blocks of statements (macro_rules! definitions)

You won't break anything using the wrong one, but other Rust developers will notice.

Macros You Already Know

Before writing your own, it helps to see why the built-ins are macros:

  • println! / format! — variadic formatted output; a function can't take a varying number of args with different types
  • vec![1, 2, 3] — creates a Vec from a list; the length isn't fixed
  • assert!(cond) / assert_eq!(a, b) — checks a condition and panics with the source file and line number if it fails; only macros have access to that information at compile time
  • panic!("msg") — terminates with a message
  • todo!() — marks unfinished code; panics if reached at runtime
  • dbg!(value) — prints the file, line, and value to stderr, then returns the value (useful for quick debugging)
fn main() {
    let x = dbg!(2 + 3); // [src/main.rs:3] 2 + 3 = 5
    println!("{}", x);   // 5
}

macro_rules!

Declarative macros use macro_rules! to match patterns and emit code. The structure looks like a match expression, but for syntax rather than values.

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

fn main() {
    say_hello!(); // Hello!
}

The () is the pattern — matches an empty argument list. The => block is the code to generate.

Capturing Arguments

Patterns capture arguments using designators — expr, ident, ty, and literal are built-in keywords in the macro pattern language. They restrict what kind of syntax the caller can pass at that position:

  • $x:expr — any expression: 5, x + 1, foo(), if a { b } else { c }
  • $x:ident — a bare name only: x, my_var, HashMap — not x + 1
  • $x:ty — a type: i32, Vec<String>, &str
  • $x:literal — a literal value only: 42, "hello", true — not a variable

The distinction matters when the macro needs a valid identifier to generate code with:

macro_rules! declare {
    ($name:ident) => {
        let $name = 0;
    };
}

fn main() {
    declare!(counter); // ✅ — "counter" is a valid identifier
    // declare!(x + 1); ❌ — "x + 1" is an expression, not an identifier
}
macro_rules! double {
    ($x:expr) => {
        $x * 2
    };
}

fn main() {
    println!("{}", double!(5));     // 10
    println!("{}", double!(3 + 1)); // 8 — the whole expression is captured
}

$x:expr captures any expression — a literal, a variable, a calculation. The macro substitutes it verbatim into the generated code.

Variadic Macros

Use $(...)* to match zero or more repetitions. This is how vec![] works internally:

macro_rules! sum {
    ($($x:expr),*) => {{
        let mut total = 0;
        $(total += $x;)*
        total
    }};
}

fn main() {
    println!("{}", sum!(1, 2, 3));        // 6
    println!("{}", sum!(10, 20, 30, 40)); // 100
}

$($x:expr),* means "zero or more expressions separated by commas." The $(total += $x;)* expands into one statement per captured expression at compile time.

Multiple Patterns

A macro can have multiple arms, tried in order — first match wins:

macro_rules! greet {
    () => {
        println!("Hello, world!");
    };
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
}

fn main() {
    greet!();        // Hello, world!
    greet!("Alice"); // Hello, Alice!
}

When to Use Macros

Macros are powerful but harder to read and debug than functions. Reach for them when:

  • You need variadic arguments (different numbers of args at different call sites)
  • You need the caller's source location (assert! uses this to show exactly where it failed)
  • You need to generate repetitive code at compile time

For everything else, a function is clearer and easier to test.

Key Takeaways

  • The ! suffix marks a macro call; macros run at compile time and generate code
  • println!, vec!, assert!, panic!, todo!, dbg! are macros because functions can't handle variadic or mixed-type arguments
  • macro_rules! defines declarative macros using pattern matching on syntax
  • $x:expr captures any expression; $x:ident captures an identifier; $x:ty captures a type
  • $($x:expr),* matches zero or more comma-separated expressions — the basis of variadic macros
  • Multiple arms are tried in order; first match wins
  • Prefer functions over macros unless you specifically need variadic args, source location info, or compile-time code generation

🎁 You've written sequential Rust. But what if you want two things to happen at the same time? Next up: threads, and how Rust makes concurrent code safe by construction.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy