title: Macros

Macros are code that writes code. They are invoked with a ! suffix (println!, vec!) and run at compile time, generating Rust code before the compiler sees it.

There are two main kinds:

  • Declarative macros (macro_rules!) — pattern-match on syntax and expand to code
  • Procedural macros — functions that receive a token stream and return a token stream (used for #[derive(...)] and similar attributes)

This page focuses on declarative macros, which are the most common.

Why macros exist

Some things can’t be done with functions:

  • Functions have a fixed number of parameters — println! accepts any number
  • Functions can’t accept mixed types in a single variadic argument list
  • Functions can’t generate impl blocks or struct fields

Macros solve these by operating on the syntax itself, before types are resolved.

println!

println! prints a line to stdout. It takes a format string and any number of arguments.

println!("hello");
println!("x = {}", x);
println!("x = {x}");        // captured variable syntax (Rust 1.58+)
println!("{:?}", some_val); // debug format

The format string is checked at compile time — a mismatch between placeholders and arguments is a compile error.

What it expands to

println!("x = {}", x);

expands roughly to:

{
    use std::io::Write;
    let stdout = std::io::stdout();
    let mut handle = stdout.lock();
    handle.write_fmt(format_args!("x = {}\n", x)).unwrap();
}

format_args! is a compiler built-in that constructs an Arguments struct from the format string and values — no heap allocation. println! locks stdout, writes the formatted output, and appends a newline.

The {} placeholders call Display::fmt. Use {:?} for Debug::fmt and {:#?} for pretty-printed debug output.

MacroBehaviour
print!same as println! but no trailing newline
eprintln!prints to stderr
format!same formatting, returns a String
write! / writeln!writes to any Write implementor

vec!

vec! creates a Vec<T> from a list of values.

let v = vec![1, 2, 3];
let v = vec!["a", "b", "c"];
let v = vec![0; 5];  // five zeros: [0, 0, 0, 0, 0]

What it expands to

let v = vec![1, 2, 3];

expands to:

let v = {
    let mut temp = Vec::new();
    temp.push(1);
    temp.push(2);
    temp.push(3);
    temp
};

In practice the standard library uses a slightly more optimised form with with_capacity to avoid reallocations:

let v = {
    let mut temp = Vec::with_capacity(3);
    temp.push(1);
    temp.push(2);
    temp.push(3);
    temp
};

The repeat form vec![value; n] expands to vec::from_elem(value, n), which calls Clone on value n times.

You can inspect what any macro expands to with cargo expand (from the cargo-expand tool):

cargo expand

Writing a simple macro

Declarative macros use macro_rules! and match on syntax patterns.

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

Patterns can capture arguments:

macro_rules! double {
    ($x:expr) => {
        $x * 2
    };
}
 
let n = double!(5); // expands to: 5 * 2

:expr is a fragment specifier — it tells the macro what kind of syntax to match. Common ones:

SpecifierMatches
exprany expression
identan identifier (variable/function name)
tya type
stmta statement
blocka { ... } block
tta single token tree (catch-all)

Variadic patterns

Use $(...)* to match zero or more repetitions, or $(...)+ for one or more.

macro_rules! my_vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp = Vec::new();
            $(
                temp.push($x);
            )*
            temp
        }
    };
}
 
let v = my_vec![1, 2, 3];

The $( $x:expr ),* pattern matches a comma-separated list of expressions. Inside the expansion, $( temp.push($x); )* repeats the push for each captured $x.

Hygiene

Macros in Rust are hygienic — identifiers introduced inside a macro don’t accidentally collide with identifiers at the call site.

macro_rules! make_x {
    () => {
        let x = 42;
    };
}
 
let x = 1;
make_x!();
println!("{x}"); // prints 1, not 42 — the macro's x is separate

This is different from C preprocessor macros, which do simple text substitution and have no hygiene.

Procedural macros

Procedural macros are more powerful and are used for things like #[derive(Debug)], #[derive(Serialize)], or custom attribute macros. They are defined in a separate crate and operate on the compiler’s token stream directly. That’s a larger topic worth its own page.