Rust Macros: Metaprogramming
Master Metaprogramming in Rust: Declarative Macros with macro_rules! and Procedural Macros (Custom Derive, Attribute-like, and Function-like).
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:
#[macro_export]
macro_rules! my_vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
fn main() {
let numbers = my_vec![1, 2, 3, 4, 5];
println!("Custom vector: {numbers:?}");
}rustSyntax 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;rustB. 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
}rustC. Function-like Macros#
Look like function calls but can accept arbitrary domain-specific syntax:
let sql = sql!(SELECT * FROM users WHERE id = 1);rustFunctions vs Macros Comparison#
| Feature | Functions | Macros |
|---|---|---|
| Execution Phase | Runtime | Compile-time |
| Arity | Fixed number & type of arguments | Variadic (arbitrary number of arguments) |
| Scope of Action | Manipulates runtime values | Manipulates source code syntax (AST/Tokens) |
| Complexity | Simple to write, read, and maintain | More 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.