Code of the Day
AdvancedEcosystem & tooling

Macros: an introduction

Write code that writes code with declarative macro_rules! macros — understand the pattern matching syntax, common use cases, and when to reach for proc macros instead.

RustAdvanced12 min read
Recommended first
By the end of this lesson you will be able to:
  • Explain the difference between macros and functions
  • Write a declarative macro with macro_rules!
  • Read the vec! and println! macros as examples
  • Identify when to write a macro vs when a function suffices
  • Understand at a high level what procedural macros are and when they're used

Macros in Rust are different from macros in C. C macros are textual substitution — fragile, scope-unaware, a common source of subtle bugs. Rust macros are syntactic: they match patterns in the token stream and produce valid Rust syntax, checked by the compiler. They're a metaprogramming tool — code that writes code.

You've been using macros from the start: println!, vec!, assert_eq!, format!, todo!. This lesson shows you how they work and when to write your own.

Macros vs functions

A function is called at runtime with a fixed, typed signature. A macro is expanded at compile time and can:

  • Accept a variable number of arguments (like println!("{} {}", a, b, c))
  • Accept different types in the same position
  • Generate different code depending on the input
vec![1, 2, 3]          // macro — variable number of args
[1, 2, 3]              // array literal — fixed-size, known at compile time
Vec::from([1, 2, 3])   // function — but can't take variable args like vec!

The trade-off: macros are more powerful but harder to read, harder to write, and harder to debug. Use functions when they can do the job.

macro_rules!

use macro_rules! and work by matching patterns:

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

say_hello!();             // Hello!
say_hello!("Ferris");     // Hello, Ferris!

A macro_rules! definition contains one or more rules: (pattern) => { expansion }. The pattern uses metavariables$name:expr matches any Rust expression and binds it to $name in the expansion.

Common metavariable types

DesignatorMatches
exprAny expression
identAn identifier
tyA type
stmtA statement
literalA literal value
ttA single token tree (very flexible)

Repetition patterns

Use $(...)* or $(...)+ to match repeated tokens:

macro_rules! my_vec {
    ($($element:expr),*) => {
        {
            let mut v = Vec::new();
            $(v.push($element);)*
            v
        }
    };
}

let v = my_vec![1, 2, 3, 4];

$($element:expr),* matches zero or more comma-separated expressions. $(v.push($element);)* expands the push call once for each matched element. This is (approximately) how the standard vec! macro works.

Macro expansion happens before type checking. The compiler first expands all macros into their generated code, then type-checks the result. This means macro errors can be confusing — the error message points to generated code, not the macro call site. Rustfmt and cargo expand (a community tool) can show you the expanded output.

vec! and println! dissected

// vec! is approximately:
macro_rules! vec {
    () => (std::vec::Vec::new());
    ($($x:expr),+ $(,)?) => ({
        let mut v = std::vec::Vec::new();
        $(v.push($x);)+
        v
    });
}

// println! wraps format_args! and writes to stdout:
// It accepts a format string literal (checked at compile time) and any number of arguments.

println!'s format string is checked at compile time — println!("{} {}", a) is a compile error (too few args), not a runtime error. Functions can't provide this guarantee; the macro's compile-time pattern matching makes it possible.

When to write a macro

Macros are appropriate when:

  1. You need a variable number of arguments.
  2. You need to generate repetitive code from a shorter specification (e.g., implementing a trait for many types).
  3. You need compile-time checking of input structure (e.g., SQL queries, format strings).

Prefer functions otherwise. A function that takes a Vec of arguments is usually clearer than a macro.

Macros are not namespaced like functions — they are in scope from the point of #[macro_export] or use. A poorly-named macro can shadow standard library items. Give macros distinctive names and only export ones you intend as public API.

Procedural macros: a brief overview

Declarative macros (macro_rules!) match patterns. Procedural macros (proc macros) are full Rust programs that receive a token stream and return a token stream. They're more powerful but require a separate crate:

  • : #[derive(Debug, Serialize)] — auto-implement traits for types.
  • Attribute macros: #[route(GET, "/")] — transform annotated items.
  • Function-like macros: sql!(SELECT * FROM users) — macro with function call syntax but arbitrary parsing.

You've used proc macros many times: #[derive(Debug)] is a proc macro from the standard library. #[tokio::main] is a proc macro from Tokio. The serde crate's #[derive(Serialize, Deserialize)] generates serialization code automatically.

Writing proc macros requires the proc-macro2, quote, and syn crates. It's advanced territory — worth knowing exists, but not necessary until you're building a library that others will use.

Check your understanding

Knowledge check

  1. 1.
    Which capability can a macro provide that a function cannot?
  2. 2.
    When does a macro_rules! macro run?
  3. 3.
    #[derive(Debug)] is implemented as a procedural macro, not a macro_rules! macro.

Do it yourself

Write a map! macro that creates a HashMap from key-value pairs:

let m = map!{ "a" => 1, "b" => 2, "c" => 3 };

Hint: use $(key:expr => $val:expr),* as the pattern. The expansion should create a HashMap::new(), insert each pair, and return the map.

When the agent's away

# See the expanded output of a macro call
cargo install cargo-expand
cargo expand          # shows the full expanded source

# Check a specific item
cargo expand --item my_function

cargo expand is invaluable for debugging macros — it shows you exactly what code the macro generated.

Where to go next

This completes the ecosystem and tooling module. The lab at the end of this module puts the advanced topics together. After that, you've covered the full Rust track — from ownership basics through async concurrency and metaprogramming.

Finished reading? Mark it complete to track your progress.

On this page