Rust macros often feel intimidating at first, especially if you’re coming from traditional object-oriented or scripting languages. But macros in Rust exist for one simple reason: to eliminate boilerplate and enable powerful compile-time code generation.
Once you understand why macros exist and the two types Rust provides, they become an essential tool rather than a scary one.
Why Rust Has Macros
Functions in Rust are used to reuse behavior. Macros, on the other hand, are used to reuse syntax and structure.
Macros:
- Run at compile time
- Generate Rust code
- Have zero runtime cost
This makes them ideal for patterns that functions simply cannot express.
Declarative Macros (macro_rules!)
Declarative macros are the first type of macros m…
Rust macros often feel intimidating at first, especially if you’re coming from traditional object-oriented or scripting languages. But macros in Rust exist for one simple reason: to eliminate boilerplate and enable powerful compile-time code generation.
Once you understand why macros exist and the two types Rust provides, they become an essential tool rather than a scary one.
Why Rust Has Macros
Functions in Rust are used to reuse behavior. Macros, on the other hand, are used to reuse syntax and structure.
Macros:
- Run at compile time
- Generate Rust code
- Have zero runtime cost
This makes them ideal for patterns that functions simply cannot express.
Declarative Macros (macro_rules!)
Declarative macros are the first type of macros most Rust developers encounter.
macro_rules! create_function {
($name:ident) => {
fn $name() {
println!("Hello from {}", stringify!($name));
}
};
}
Using the macro:
create_function!(hello);
At compile time, this expands to:
fn hello() {
println!("Hello from hello");
}
Key Characteristics
- Pattern-based (
ident,expr, etc.) - Simple and predictable
- Excellent for reducing repetitive code
Generating Multiple Functions with Macros
Declarative macros become more powerful with repetition.
macro_rules! generate_functions {
($($name:ident),*) => {
$(
fn $name() {
println!("Hello from {}", stringify!($name));
}
)*
};
}
Usage:
generate_functions!(foo, bar, baz);
This single macro call generates multiple functions automatically.
This kind of code generation is impossible with normal functions, which is exactly why macros exist.
When to Use Declarative Macros
Use macro_rules! when:
- You’re repeating similar code patterns
- You need compile-time code generation
- Functions are not expressive enough
Rule of thumb:
If a function can solve the problem, use a function. Use macros only when you need syntax generation.
Procedural Macros (Macros You Use Every Day)
Even if you’ve never written a macro, you’ve already used procedural macros.
A common example is Serde:
#[derive(Serialize, Deserialize)]
struct User {
username: String,
age: u32,
}
This #[derive] is a procedural macro.
What it does:
- Reads your struct at compile time
- Generates serialization and deserialization logic
- Eliminates massive amounts of boilerplate
Unlike declarative macros, procedural macros:
- Work on Rust’s syntax tree (AST)
- Are more powerful
- Are commonly used by frameworks and libraries
Attribute Macros in Practice
#[serde(rename = "user_name")]
username: String,
This tells Serde how fields should appear in JSON without changing your Rust code.
The behavior is injected at compile time through macros, not runtime logic.
Serialization and Deserialization: Why Macros Matter
Serialization converts Rust data into a transferable format like JSON. Deserialization converts it back into Rust types.
Rust struct → JSON → Client / DB / File
JSON → Rust struct → Business logic
Rust structs cannot be sent directly across systems. Serialization is how Rust communicates with the outside world.
A Real-World Edge Case
JSON does not support NaN.
score: f64::NAN
This will fail during serialization, even though Rust itself allows it. Sometimes the limitation isn’t Rust or macros—it’s the data format.
Why Rust’s Macro Design Works
Rust macros:
- Reduce boilerplate
- Enforce consistency
- Move complexity to compile time
- Keep runtime code clean and fast
Declarative macros help you write less code. Procedural macros help libraries write code for you.
Final Takeaway
macro_rules!→ write your own compile-time patterns- Procedural macros (
derive, attributes) → use powerful library-generated code - Macros exist to solve problems functions cannot
- Serialization is how Rust crosses system boundaries