In the previous tutorial, we learned file I/O. Now we learn macros — one of Rust’s most powerful features for code generation.

Macros let you write code that writes code. They run at compile time and expand into regular Rust code. You have already used macros like println!(), vec![], and format!(). Now you will write your own.

What Are Macros?

A macro is a pattern that expands into code at compile time. When you write println!("hello"), the compiler replaces it with the actual printing code before compilation.

Macros are different from functions:

  • Functions run at runtime. They take values and return values.
  • Macros run at compile time. They take code patterns and produce code.

Functions cannot take a variable number of arguments. Macros can. Functions cannot generate struct definitions. Macros can. That is why Rust uses macros for things like vec![1, 2, 3] — a function cannot accept a comma-separated list of any length.

Your First Macro

The simplest macro takes no arguments:

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

fn main() {
    say_hello!();  // prints: Hello from a macro!
}

macro_rules! defines a macro. Inside, you write match arms. The left side () is the pattern. The right side is the code it expands to.

Macros with Arguments

Macros can take expressions as arguments:

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

macro_rules! add {
    ($a:expr, $b:expr) => {
        $a + $b
    };
}

fn main() {
    let msg = greet!("Alex");     // "Hello, Alex!"
    println!("{}", msg);

    let sum = add!(2, 3);         // 5
    let sum2 = add!(1.5, 2.5);   // 4.0 — works with any type
    println!("{}, {}", sum, sum2);
}

$name:expr means “capture any expression and call it $name”. The :expr part is a fragment specifier. It tells the macro what kind of syntax to expect.

Common fragment specifiers:

SpecifierMatches
exprAny expression: 5, x + 1, foo()
identAn identifier: x, my_var, Config
tyA type: i32, String, Vec<u8>
ttA single token tree (most flexible)
literalA literal value: 42, "hello", true
stmtA statement
blockA block { ... }

Multiple Match Arms

A single macro can have multiple patterns, like match:

macro_rules! calculate {
    (add $a:expr, $b:expr) => {
        $a + $b
    };
    (sub $a:expr, $b:expr) => {
        $a - $b
    };
    (mul $a:expr, $b:expr) => {
        $a * $b
    };
}

fn main() {
    println!("{}", calculate!(add 5, 3));   // 8
    println!("{}", calculate!(sub 10, 4));  // 6
    println!("{}", calculate!(mul 6, 7));   // 42
}

The literal words add, sub, mul are part of the pattern. The macro matches the first arm that fits. This creates a mini domain-specific language inside Rust.

Repetition Patterns

Repetition is what makes macros truly powerful. The syntax is $(...)+ or $(...)*:

  • + means one or more
  • * means zero or more

Building vec! from Scratch

Let’s rebuild the vec! macro:

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

fn main() {
    let empty: Vec<i32> = my_vec![];
    let nums = my_vec![1, 2, 3];
    let with_trailing = my_vec![10, 20, 30,];  // trailing comma OK

    println!("{:?}", empty);           // []
    println!("{:?}", nums);            // [1, 2, 3]
    println!("{:?}", with_trailing);   // [10, 20, 30]
}

Let’s break down ($($element:expr),+ $(,)?):

  • $(...) — starts a repetition group
  • $element:expr — captures each expression
  • , — the separator between elements
  • + — one or more elements
  • $(,)? — optional trailing comma

In the body, $(v.push($element);)+ expands once for each captured element. So my_vec![1, 2, 3] becomes:

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

HashMap Macro

The same pattern works for key-value pairs:

use std::collections::HashMap;

macro_rules! hash_map {
    () => {
        HashMap::new()
    };
    ($($key:expr => $value:expr),+ $(,)?) => {
        {
            let mut map = HashMap::new();
            $(map.insert($key, $value);)+
            map
        }
    };
}

fn main() {
    let scores = hash_map![
        "Alex" => 95,
        "Sam" => 87,
        "Jordan" => 92,
    ];
    println!("{:?}", scores);
}

The => between key and value is just literal syntax in the pattern. You could use : or any other token instead.

Custom Assert Macros

You can make your own assert macros for specific needs:

macro_rules! assert_between {
    ($value:expr, $min:expr, $max:expr) => {
        let val = $value;
        let min_val = $min;
        let max_val = $max;
        if val < min_val || val > max_val {
            panic!(
                "assertion failed: {} is not between {} and {} (value: {})",
                stringify!($value), min_val, max_val, val
            );
        }
    };
}

macro_rules! assert_contains {
    ($haystack:expr, $needle:expr) => {
        let haystack = &$haystack;
        let needle = &$needle;
        if !haystack.contains(needle) {
            panic!(
                "assertion failed: {:?} does not contain {:?}",
                haystack, needle
            );
        }
    };
}

fn main() {
    let age = 25;
    assert_between!(age, 18, 65);  // passes

    assert_contains!("hello world", "world");  // passes
}

stringify!($value) is a built-in macro that converts the expression to a string at compile time. So stringify!(age) becomes "age". This gives better error messages.

Generating Structs with Macros

Macros can generate entire type definitions:

macro_rules! make_struct {
    ($name:ident { $($field:ident : $type:ty),+ $(,)? }) => {
        #[derive(Debug, Clone)]
        struct $name {
            $($field: $type,)+
        }

        impl $name {
            fn new($($field: $type),+) -> Self {
                Self { $($field,)+ }
            }
        }
    };
}

make_struct!(Config {
    host: String,
    port: u16,
    debug: bool,
});

make_struct!(Point {
    x: f64,
    y: f64,
});

fn main() {
    let config = Config::new("localhost".to_string(), 8080, true);
    println!("{:?}", config);

    let point = Point::new(3.0, 4.0);
    println!("{:?}", point);
}

One macro call generates a struct with derives and a constructor. The $name:ident captures an identifier (the struct name). $field:ident captures field names. $type:ty captures types.

Practical Macros

Timing Macro

Measure how long something takes:

macro_rules! time_it {
    ($label:expr, $body:expr) => {{
        let start = std::time::Instant::now();
        let result = $body;
        let elapsed = start.elapsed();
        println!("{}: {:?}", $label, elapsed);
        result
    }};
}

fn main() {
    let sum = time_it!("Sum calculation", {
        (0..1_000_000).sum::<u64>()
    });
    println!("Sum: {}", sum);
}

Debug Variable Macro

Quick debugging:

macro_rules! debug_var {
    ($var:expr) => {
        println!("{} = {:?}", stringify!($var), $var);
    };
}

macro_rules! inspect {
    ($($var:expr),+) => {
        $(println!("  {} = {:?}", stringify!($var), $var);)+
    };
}

fn main() {
    let name = "Alex";
    let score = 95;
    let passed = true;

    debug_var!(name);   // name = "Alex"
    debug_var!(score);  // score = 95

    println!("All variables:");
    inspect!(name, score, passed, 2 + 2);
    // name = "Alex"
    // score = 95
    // passed = true
    // 2 + 2 = 4
}

stringify! turns code into a string literal at compile time. stringify!(2 + 2) becomes "2 + 2", not "4".

Log Macro

A flexible logging macro:

macro_rules! log_msg {
    (debug, $($arg:tt)*) => {
        println!("[DEBUG] {}", format!($($arg)*));
    };
    (info, $($arg:tt)*) => {
        println!("[INFO] {}", format!($($arg)*));
    };
    (warn, $($arg:tt)*) => {
        println!("[WARN] {}", format!($($arg)*));
    };
    (error, $($arg:tt)*) => {
        println!("[ERROR] {}", format!($($arg)*));
    };
}

fn main() {
    log_msg!(info, "Server started on port {}", 8080);
    log_msg!(warn, "Memory usage at {}%", 85);
    log_msg!(error, "Connection failed: {}", "timeout");
}

The $($arg:tt)* pattern captures any number of token trees. This lets you pass format strings with arguments, just like println!.

Macro Hygiene

Rust macros are hygienic. Variables created inside a macro do not leak into the surrounding code:

macro_rules! safe_macro {
    ($val:expr) => {
        {
            let x = $val;
            x * 2
        }
    };
}

fn main() {
    let x = 10;
    let result = safe_macro!(5);
    println!("x = {}, result = {}", x, result);
    // x = 10, result = 10
    // the macro's 'x' did not affect ours
}

This prevents accidental name collisions. The macro’s x and the function’s x are different variables.

Generating Enums with Display

Another practical example — an enum with automatic Display:

macro_rules! string_enum {
    ($name:ident { $($variant:ident => $display:expr),+ $(,)? }) => {
        #[derive(Debug, Clone, PartialEq)]
        enum $name {
            $($variant,)+
        }

        impl std::fmt::Display for $name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                match self {
                    $($name::$variant => write!(f, $display),)+
                }
            }
        }
    };
}

string_enum!(LogLevel {
    Debug => "DEBUG",
    Info => "INFO",
    Warn => "WARN",
    Error => "ERROR",
});

fn main() {
    println!("{}", LogLevel::Warn);    // "WARN"
    println!("{}", LogLevel::Error);   // "ERROR"
}

One macro generates the enum and the Display trait. Without the macro, you would write this boilerplate for every enum.

When to Use Macros vs Functions

Use functions when:

  • The logic works with specific types
  • You need runtime behavior
  • Simple parameter passing is enough

Use macros when:

  • You need a variable number of arguments (vec![1, 2, 3])
  • You want to generate code (struct definitions, trait impls)
  • You need compile-time string manipulation (stringify!)
  • You want domain-specific syntax (calculate!(add 5, 3))

A good rule: start with functions. Switch to macros only when functions cannot do what you need.

Declarative vs Procedural Macros

What we learned here are declarative macros (macro_rules!). Rust also has procedural macros:

  • Derive macros#[derive(Debug)] is a procedural macro. It generates code from struct/enum definitions.
  • Attribute macros#[test] and #[tokio::main] are attribute macros. They transform the item they are attached to.
  • Function-like procedural macros — look like declarative macros but have full access to the Rust AST.

Procedural macros need their own crate and are harder to write. For most cases, macro_rules! is enough.

Debugging Tips

  1. Start simple — get one arm working before adding more
  2. Use stringify! — it shows what the macro captured
  3. Read the error carefully — Rust shows what the macro expanded to
  4. Use cargo expand — install with cargo install cargo-expand, then run cargo expand to see what your macros generate

When the compiler says “expected expression, found ;” inside a macro, it usually means the expanded code has a syntax error. Think about what the macro produces, not what it looks like.

Source Code

You can find the complete source code for this tutorial on GitHub:

kemalcodes/rust-tutorial (branch: tutorial-26-macros)

What’s Next?

In the next tutorial, we learn Unsafe Rust — raw pointers, unsafe blocks, FFI, and how to build safe abstractions over unsafe code.