Wonderfull Rust

Macros

Why Rust's Macros Are Special

Many languages have some form of macro system. C/C++ preprocessor macros (#define) are the most well-known example. However, C preprocessor macros are mere text substitution -- they lack type safety, are difficult to debug, and are prone to unexpected side effects.

Rust's macros are fundamentally different. They operate at the syntax tree level and are expanded before type checking. It's structured code generation, not text substitution.

Rust has two kinds of macros:

  1. Declarative macros: Defined with macro_rules!. Code generation through pattern matching
  2. Procedural macros: Programs that manipulate the syntax tree using Rust code

Declarative Macros (macro_rules!)

Basics

macro_rules! defines macros based on pattern matching:

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

fn main() {
    say_hello!();  // Expands to println!("Hello!");
}

Macro invocations are indicated by !. println!, vec!, format! -- all of these that we used as "magic spells" in Bitterless Rust are macros.

Pattern Matching

The true power of declarative macros lies in pattern matching:

macro_rules! create_function {
    ($func_name:ident) => {
        fn $func_name() {
            println!("Called: {}", stringify!($func_name));
        }
    };
}

create_function!(foo);  // Generates fn foo() { ... }
create_function!(bar);  // Generates fn bar() { ... }

fn main() {
    foo();  // "Called: foo"
    bar();  // "Called: bar"
}

The :ident in $func_name:ident is called a fragment specifier, which specifies the kind of syntactic element the macro accepts:

Fragment Specifier Matches
ident Identifiers (foo, my_var)
expr Expressions (1 + 2, func())
ty Types (i32, Vec<String>)
pat Patterns (Some(x), _)
stmt Statements (let x = 1;)
block Blocks ({ ... })
item Items (fn, struct, impl)
tt Any single token tree
literal Literals (42, "hello")

Repetition

Macros support repetition patterns:

macro_rules! vec_of_strings {
    ($($x:expr),* $(,)?) => {
        vec![$($x.to_string()),*]
    };
}

fn main() {
    let v = vec_of_strings!["hello", "world", "rust"];
    // Expands to vec!["hello".to_string(), "world".to_string(), "rust".to_string()]
    println!("{:?}", v);
}
  • $(...),* means "zero or more repetitions, separated by commas"
  • $(...),+ means "one or more repetitions, separated by commas"
  • $(,)? means "optional trailing comma"

Multiple Patterns

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

fn main() {
    let sum = calculate!(add 1, 2);    // 3
    let product = calculate!(mul 3, 4); // 12
}

Like a match expression, you can define multiple patterns and expand based on the input.

How Standard Library Macros Work

A simplified implementation of the vec! macro:

macro_rules! vec {
    () => { Vec::new() };
    ($($x:expr),+ $(,)?) => {
        {
            let mut temp_vec = Vec::new();
            $(temp_vec.push($x);)*
            temp_vec
        }
    };
    ($x:expr; $n:expr) => {
        vec::from_elem($x, $n)
    };
}

Writing vec![1, 2, 3] expands to Vec::new() plus push for each element. Writing vec![0; 10] expands to initialization using from_elem.

Procedural Macros

Procedural macros are programs that manipulate the syntax tree using Rust code itself. Think of them as Rust programs that execute at compile time.

There are three kinds of procedural macros:

1. Derive Macros

Macros usable with #[derive(...)]. They automatically add trait implementations to structs and enums:

// Usage side
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
    name: String,
    age: u32,
}

Debug and Clone are standard library derive macros. Serialize / Deserialize are derive macros from the serde crate.

A derive macro implementation (overview):

// This goes in a separate crate (a proc-macro crate)
use proc_macro::TokenStream;

#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
    // input: the syntax tree of the struct/enum definition
    // -> parse this and generate trait implementation code to return
    // ...
}

Procedural macros are functions that receive a TokenStream (sequence of tokens) and return a TokenStream. They parse the input syntax tree and generate new code.

2. Attribute Macros

Macros usable as arbitrary attributes:

#[route(GET, "/")]
fn index() -> String {
    String::from("Hello!")
}

// The route macro transforms the index function and
// auto-generates routing registration code

Widely used in web frameworks (Actix Web, Axum, Rocket, etc.).

3. Function-Like Macros

Macros usable with function-call-like syntax:

let sql = sql!(SELECT * FROM users WHERE id = 1);

// The sql! macro performs SQL syntax checking at compile time
// and emits a compile error if the SQL is invalid

Macro Hygiene

Rust's declarative macros are hygienic. Variables defined within a macro don't collide with variables in the caller's scope:

macro_rules! make_var {
    () => {
        let x = 42;
    };
}

fn main() {
    let x = 10;
    make_var!();
    println!("{}", x);  // 10 (different from the x inside the macro)
}

In C preprocessor macros, such variable name collisions are a serious problem. Rust's macro system structurally prevents them.

Practical Macro Examples

The cfg Macro: Conditional Compilation

#[cfg(target_os = "linux")]
fn platform_specific() {
    println!("Running on Linux");
}

#[cfg(target_os = "macos")]
fn platform_specific() {
    println!("Running on macOS");
}

// At compile time, only the function matching the target OS is compiled

todo! / unimplemented! / unreachable!

fn complex_algorithm(input: &str) -> Result<String, Error> {
    todo!("implement this later")  // Compiles, but panics when executed
}

todo!() is a macro indicating "not implemented yet," functioning as a placeholder that passes type checking. Its return type is ! (the Never type), so it can be inferred as any type.

Summary

Rust's macro system is designed as a two-layer architecture:

Kind Mechanism Use Cases
Declarative macros (macro_rules!) Expansion through pattern matching Reducing boilerplate, DSLs
Procedural macros Syntax tree manipulation via Rust code derive, attributes, code generation

Important properties common to both:

  • Expanded at compile time: No runtime overhead
  • Operate at the syntax tree level: Not text substitution
  • Expanded before type checking: The expanded code undergoes normal type checking
  • Hygiene: Unintended variable name collisions are prevented (declarative macros)

Macros fully align with Rust's principle of "zero-cost abstraction" -- everything is resolved at compile time, with no runtime cost.

Back to book