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:
- Declarative macros: Defined with
macro_rules!. Code generation through pattern matching - 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.