blog.dopana

Back

Sixteenth post and the finale of the Intermediate Rust series. The term “macro” in Rust refers to a family of powerful features for Metaprogramming — writing code that writes other code at compile time.

Unlike regular functions that take values at runtime, macros operate on syntax tokens and expand into actual Rust code before the compiler checks types and emits binary instructions.

graph TD
    A["Rust Macro System"] --> B["Declarative Macros<br/>macro_rules!"]
    A --> C["Procedural Macros<br/>Operate on TokenStream"]
    C --> D["Custom Derive<br/>#[derive(CustomTrait)]"]
    C --> E["Attribute-like<br/>#[route(GET, '/')]"]
    C --> F["Function-like<br/>sql!(...)"]

Explain Like I’m 10: The 3D Code Printer#

Think about the difference between a Function and a Macro:

  • Function: Like feeding oranges into a juicer — it runs when the store is open (Runtime) and gives you a cup of juice.
  • Macro: Like a 3D printer that manufactures the juicer itself before the factory opens for business (Compile-time).

1. Declarative Macros with macro_rules!#

Declarative macros are the most widely used macros in Rust. They use pattern matching on Rust source code tokens similar to a match expression:

Here is a simplified recreation of Rust’s built-in vec! macro:

Syntax Breakdown#

  • $x:expr: Matches any valid Rust expression and captures it into variable $x.
  • $( ... ),*: Repeats the enclosed code for each comma-separated match, 0 or more times (*).

2. Procedural Macros#

While declarative macros are pattern-matching code templates, Procedural Macros are Rust functions that take code tokens as input (TokenStream), manipulate them using arbitrary Rust code, and return a transformed TokenStream.

There are three flavors of Procedural Macros:

A. Custom Derive Macros#

Automatically generates trait implementations when using #[derive(MyTrait)]:

// Macro definition (inside a dedicated proc-macro crate)
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Parse AST tokens and emit `impl HelloMacro for StructName`
}

// Consumer code
#[derive(HelloMacro)]
struct Pancakes;
rust

B. Attribute-like Macros#

Create custom attributes that can be attached to items like structs, functions, or modules:

#[route(GET, "/users")]
fn get_users() {
    // Web framework route definition
}
rust

C. Function-like Macros#

Look like function calls but can accept arbitrary domain-specific syntax:

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

Functions vs Macros Comparison#

FeatureFunctionsMacros
Execution PhaseRuntimeCompile-time
ArityFixed number & type of argumentsVariadic (arbitrary number of arguments)
Scope of ActionManipulates runtime valuesManipulates source code syntax (AST/Tokens)
ComplexitySimple to write, read, and maintainMore complex syntax, harder to debug

Summary#

  • Macros enable compile-time metaprogramming without runtime overhead.
  • macro_rules! provides an accessible pattern-matching template system for declarative macros.
  • Procedural macros give full programmatic control over Rust’s abstract syntax tree via Custom Derive, Attribute-like, and Function-like macros.

References#