Rust - Macros

Overview

Estimated time: 45–55 minutes

Master declarative macros in Rust using macro_rules!. Learn pattern matching, repetition syntax, macro hygiene, and how to create powerful code generation tools that write Rust code for you.

Learning Objectives

Prerequisites

What Are Macros?

Macros are code that writes other code (metaprogramming). Unlike functions that operate on values at runtime, macros operate on tokens at compile time and generate code before the program runs.

Why Use Macros?

Basic Macro Syntax

// Simple macro that prints a message
macro_rules! say_hello {
    () => {
        println!("Hello from a macro!");
    };
}

// Macro with parameters
macro_rules! say_something {
    ($message:expr) => {
        println!("Message: {}", $message);
    };
}

// Macro with multiple patterns
macro_rules! calculate {
    (add $a:expr, $b:expr) => {
        $a + $b
    };
    (multiply $a:expr, $b:expr) => {
        $a * $b
    };
}

fn main() {
    say_hello!();
    say_something!("Hello, World!");
    say_something!(42);
    
    let sum = calculate!(add 5, 3);
    let product = calculate!(multiply 4, 7);
    
    println!("Sum: {}", sum);
    println!("Product: {}", product);
}

Expected Output:

Hello from a macro!
Message: Hello, World!
Message: 42
Sum: 8
Product: 28

Pattern Matching and Types

Macros use specific designators to match different types of tokens:

macro_rules! demonstrate_types {
    // Expression
    (expr $e:expr) => {
        println!("Expression: {}", $e);
    };
    
    // Identifier
    (ident $i:ident) => {
        println!("Identifier: {}", stringify!($i));
    };
    
    // Type
    (type $t:ty) => {
        println!("Type: {}", stringify!($t));
    };
    
    // Pattern
    (pat $p:pat) => {
        let $p = 42;
        println!("Pattern matched!");
    };
    
    // Statement  
    (stmt $s:stmt) => {
        $s
        println!("Statement executed!");
    };
    
    // Block
    (block $b:block) => {
        println!("Before block");
        $b
        println!("After block");
    };
}

fn main() {
    demonstrate_types!(expr 2 + 3);
    demonstrate_types!(ident my_variable);
    demonstrate_types!(type Vec<String>);
    demonstrate_types!(pat x);
    demonstrate_types!(stmt let y = 10);
    demonstrate_types!(block {
        println!("Inside block");
        let z = 5;
    });
}

Expected Output:

Expression: 5
Identifier: my_variable
Type: Vec<String>
Pattern matched!
Statement executed!
Before block
Inside block
After block

Repetition Patterns

Macros can handle variable numbers of arguments using repetition:

// Macro that sums any number of expressions
macro_rules! sum {
    ($($x:expr),*) => {
        {
            let mut total = 0;
            $(
                total += $x;
            )*
            total
        }
    };
}

// Macro that creates a vector with any number of elements
macro_rules! vec_of {
    ($($x:expr),*) => {
        {
            let mut v = Vec::new();
            $(
                v.push($x);
            )*
            v
        }
    };
}

// Macro with different separators
macro_rules! print_all {
    ($($x:expr);*) => {
        $(
            println!("{}", $x);
        )*
    };
}

fn main() { 
    let result1 = sum!(1, 2, 3, 4, 5);
    let result2 = sum!(10, 20);
    let result3 = sum!(); // Empty case
    
    println!("Sum of 1,2,3,4,5: {}", result1);
    println!("Sum of 10,20: {}", result2); 
    println!("Sum of nothing: {}", result3);
    
    let numbers = vec_of!(1, 2, 3, 4);
    println!("Vector: {:?}", numbers);
    
    print_all!("First"; "Second"; "Third");
}

Expected Output:

Sum of 1,2,3,4,5: 15
Sum of 10,20: 30
Sum of nothing: 0
Vector: [1, 2, 3, 4]
First
Second
Third

Creating a HashMap Literal Macro

use std::collections::HashMap;

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

fn main() {
    let scores = hashmap! {
        "Alice" => 95,
        "Bob" => 87,
        "Charlie" => 92,
    };
    
    println!("Scores: {:?}", scores);
    
    // Works with different types
    let config = hashmap! {
        "debug".to_string() => true,
        "max_connections".to_string() => false,
    };
    
    println!("Config: {:?}", config);
}

Expected Output:

Scores: {"Alice": 95, "Bob": 87, "Charlie": 92}
Config: {"debug": true, "max_connections": false}

Struct Generation Macro

// Macro that generates struct with getter methods
macro_rules! define_struct {
    ($name:ident { $($field:ident: $type:ty),* $(,)? }) => {
        #[derive(Debug)]
        struct $name {
            $(
                $field: $type,
            )*
        }
        
        impl $name {
            fn new($($field: $type),*) -> Self {
                Self {
                    $(
                        $field,
                    )*
                }
            }
            
            $(
                paste::paste! {
                    fn [](&self) -> &$type {
                        &self.$field
                    }
                    
                    fn [](&mut self, value: $type) {
                        self.$field = value;
                    }
                }
            )*
        }
    };
}

// Note: The paste crate is used for identifier concatenation
// Add to Cargo.toml: paste = "1.0"

// Simpler version without paste crate
macro_rules! simple_struct {
    ($name:ident { $($field:ident: $type:ty),* $(,)? }) => {
        #[derive(Debug)]
        struct $name {
            $(
                $field: $type,
            )*
        }
        
        impl $name {
            fn new($($field: $type),*) -> Self {
                Self {
                    $(
                        $field,
                    )*
                }
            }
        }
    };
}

simple_struct! {
    Person {
        name: String,
        age: u32,
        email: String,
    }
}

fn main() {
    let person = Person::new(
        "Alice".to_string(),
        30,
        "[email protected]".to_string(),
    );
    
    println!("Person: {:?}", person);
}

Expected Output:

Person: Person { name: "Alice", age: 30, email: "[email protected]" }

Debugging and Utility Macros

// Debug macro that prints variable name and value
macro_rules! dbg_var {
    ($var:expr) => {
        println!("{} = {:?}", stringify!($var), $var);
    };
}

// Macro for timing code execution
macro_rules! time_it {
    ($label:expr, $code:block) => {
        {
            let start = std::time::Instant::now();
            let result = $code;
            let duration = start.elapsed();
            println!("{} took: {:?}", $label, duration);
            result
        }
    };
}

// Macro for conditional compilation based on features
macro_rules! feature_code {
    ($feature:expr, $code:block) => {
        #[cfg(feature = $feature)]
        $code
    };
}

// Macro that creates test functions
macro_rules! test_cases {
    ($test_name:ident: $($input:expr => $expected:expr),* $(,)?) => {
        #[cfg(test)]
        mod tests {
            use super::*;
            
            #[test]
            fn $test_name() {
                $(
                    assert_eq!(some_function($input), $expected);
                )*
            }
        }
    };
}

fn some_function(x: i32) -> i32 {
    x * 2
}

fn main() {
    let x = 42;
    let y = vec![1, 2, 3];
    
    dbg_var!(x);
    dbg_var!(y);
    dbg_var!(x + 10);
    
    let result = time_it!("Heavy computation", {
        let mut sum = 0;
        for i in 0..1_000_000 {
            sum += i;
        }
        sum
    });
    
    println!("Result: {}", result);
}

// This would create tests (commented out to avoid test framework requirement)
// test_cases! {
//     double_function_tests:
//     1 => 2,
//     5 => 10,
//     -3 => -6,
// }

Expected Output:

x = 42
y = [1, 2, 3]
x + 10 = 52
Heavy computation took: 1.234ms
Result: 499999500000

Error Handling in Macros

// Macro with compile-time error checking
macro_rules! compile_time_assert {
    ($condition:expr, $message:expr) => {
        const _: () = {
            if !$condition {
                panic!($message);
            }
        };
    };
}

// Macro that validates at compile time
macro_rules! create_array {
    ($size:expr, $value:expr) => {
        {
            compile_time_assert!($size > 0, "Array size must be positive");
            compile_time_assert!($size <= 1000, "Array size too large");
            [$value; $size]
        }
    };
}

// Macro with better error messages
macro_rules! expect_type {
    ($value:expr, i32) => {
        $value as i32
    };
    ($value:expr, f64) => {
        $value as f64
    };
    ($value:expr, $t:ty) => {
        compile_error!(concat!("Unsupported type: ", stringify!($t)));
    };
}

fn main() {
    let arr1 = create_array!(5, 0);
    println!("Array: {:?}", arr1);
    
    // This would cause a compile error:
    // let arr2 = create_array!(0, 1);
    
    let x = 42;
    let y = expect_type!(x, i32);
    let z = expect_type!(x, f64);
    
    println!("y: {}, z: {}", y, z);
    
    // This would cause a compile error:
    // let w = expect_type!(x, String);
}

Expected Output:

Array: [0, 0, 0, 0, 0]
y: 42, z: 42

Advanced Patterns and Techniques

// Recursive macro for generating nested structures
macro_rules! nested_vec {
    ($item:expr) => {
        $item
    };
    ($item:expr; $($rest:expr);+) => {
        vec![nested_vec!($item), nested_vec!($($rest);+)]
    };
}

// Macro with internal helper rules
macro_rules! count_items {
    () => { 0 };
    ($head:expr) => { 1 };
    ($head:expr, $($tail:expr),*) => { 1 + count_items!($($tail),*) };
}

// Macro that generates different code based on count
macro_rules! smart_print {
    ($single:expr) => {
        println!("Single item: {}", $single);
    };
    ($first:expr, $second:expr) => {
        println!("Pair: {} and {}", $first, $second);
    };
    ($($items:expr),*) => {
        {
            let count = count_items!($($items),*);
            println!("Multiple items ({}): ", count);
            $(
                print!("{} ", $items);
            )*
            println!();
        }
    };
}

// Macro with optional trailing elements
macro_rules! flexible_vec {
    ($($x:expr),+ $(; default $default:expr)?) => {
        {
            let mut v = vec![$($x),*];
            $(
                if v.is_empty() {
                    v.push($default);
                }
            )?
            v
        }
    };
}

fn main() {
    // let nested = nested_vec![1; 2; 3];
    // println!("Nested: {:?}", nested);
    
    smart_print!(42);
    smart_print!(1, 2);
    smart_print!(1, 2, 3, 4, 5);
    
    let v1 = flexible_vec![1, 2, 3];
    let v2 = flexible_vec![10, 20; default 99];
    
    println!("v1: {:?}", v1);
    println!("v2: {:?}", v2);
}

Expected Output:

Single item: 42
Pair: 1 and 2
Multiple items (5): 1 2 3 4 5 
v1: [1, 2, 3]
v2: [10, 20]

Macro Hygiene

Rust macros are hygienic, meaning they don't accidentally capture variables from the calling scope:

macro_rules! declare_variable {
    ($name:ident, $value:expr) => {
        let $name = $value; // This creates a new binding
    };
}

macro_rules! use_temp_var {
    ($expr:expr) => {
        {
            let temp = 100; // This temp won't conflict with outer temp
            $expr + temp
        }
    };
}

fn main() {
    let temp = 50;
    
    declare_variable!(x, 42);
    println!("x: {}", x);
    
    let result = use_temp_var!(temp); // Uses outer temp (50) + inner temp (100)
    println!("Result: {}", result); // 150
    
    // The macro's temp variable doesn't leak out
    println!("Outer temp: {}", temp); // Still 50
}

Expected Output:

x: 42
Result: 150
Outer temp: 50

Best Practices

1. Keep Macros Simple

2. Handle Edge Cases

macro_rules! safe_division {
    ($a:expr, $b:expr) => {
        {
            let denominator = $b;
            if denominator != 0 {
                Some($a / denominator)
            } else {
                None
            }
        }
    };
}

3. Use Appropriate Visibility

// Export macro from crate
#[macro_export]
macro_rules! public_macro {
    () => { println!("This macro is public"); };
}

// Local macro (default)
macro_rules! private_macro {
    () => { println!("This macro is private"); };
}

4. Test Your Macros

#[cfg(test)]
mod tests {
    #[test]
    fn test_sum_macro() {
        assert_eq!(sum!(1, 2, 3), 6);
        assert_eq!(sum!(), 0);
        assert_eq!(sum!(42), 42);
    }
}

Common Pitfalls

1. Multiple Evaluation

// Bad: evaluates expression multiple times
macro_rules! bad_max {
    ($a:expr, $b:expr) => {
        if $a > $b { $a } else { $b }
    };
}

// Good: evaluates once
macro_rules! good_max {
    ($a:expr, $b:expr) => {
        {
            let a_val = $a;
            let b_val = $b;
            if a_val > b_val { a_val } else { b_val }
        }
    };
}

Summary

Declarative macros provide powerful metaprogramming capabilities:

Use macros when you need to eliminate repetitive code patterns or create domain-specific syntax that would be impossible with functions alone.


← PreviousNext →