🧙♂️ Rust Macros: Your Code’s Magic Spell Book
The Big Idea: Imagine you’re a wizard with a spell book. Instead of writing the same spell words over and over, you create shortcuts. Say one magic word, and the spell book writes the full spell for you! That’s what macros do in Rust—they’re your code shortcuts.
🎭 What Are Macros?
Think of macros like a cookie cutter. You have one shape, and you can stamp out as many cookies as you want. Each cookie looks the same, but you didn’t have to draw each one by hand!
In Rust, macros write code for you. You tell them the pattern once, and they stamp out the code wherever you need it.
graph TD A["You write a macro"] --> B["Macro is like a recipe"] B --> C["Use the macro"] C --> D["Rust expands it to full code"] D --> E["Code runs!"]
There are two main types of macros:
- Declarative Macros – Simple pattern matching (like fill-in-the-blank)
- Procedural Macros – More powerful (like a mini-program that writes code)
📝 Declarative Macros
Declarative macros are the simpler kind. You say: “When you see THIS pattern, replace it with THAT code.”
It’s like having a friend who finishes your sentences. You start with “I want ice cream with…” and they know to say “sprinkles on top!”
The macro_rules! Syntax
This is how you create a declarative macro. The name always has ! at the end when you use it.
macro_rules! say_hello {
() => {
println!("Hello, friend!");
};
}
// Using it:
say_hello!();
// Prints: Hello, friend!
Let’s break it down:
macro_rules!= “I’m making a new macro”say_hello= the name of your macro()= the pattern to match (empty means no input)=>= “then do this…”{ ... }= the code to generate
🎯 Macro Patterns
Patterns are like fill-in-the-blank templates. You leave spots for the user to fill in.
Pattern Variables
Use $name:type to capture input:
| Type | What It Matches | Example |
|---|---|---|
expr |
Any expression | 5 + 3, x, "hi" |
ident |
An identifier/name | my_var, foo |
ty |
A type | i32, String |
tt |
A single token | anything! |
stmt |
A statement | let x = 5; |
macro_rules! make_double {
($val:expr) => {
$val * 2
};
}
let x = make_double!(5);
// Rust sees: let x = 5 * 2;
// x = 10
Think of it like Mad Libs:
- You write: “I love eating
___” - Someone fills in: “pizza”
- Result: “I love eating pizza”
🔄 Macro Repetition
What if you want your macro to handle many items? Like a machine that can stamp out 1 cookie OR 100 cookies!
Use $(...)* for “zero or more” or $(...)+ for “one or more”:
macro_rules! make_list {
( $( $item:expr ),* ) => {
{
let mut v = Vec::new();
$( v.push($item); )*
v
}
};
}
let nums = make_list!(1, 2, 3, 4, 5);
// Creates: [1, 2, 3, 4, 5]
How repetition works:
graph TD A["amp;#35;40; ... #41;*"] --> B["Match zero or more times"] A --> C["Comma separator: ,"] D["$item:expr"] --> E["Each item is an expression"] B --> F["Expands for each match"]
| Symbol | Meaning |
|---|---|
* |
Zero or more |
+ |
One or more |
? |
Zero or one |
🔧 Procedural Macros Overview
Now we level up! Procedural macros are like having a tiny robot that reads your code and writes new code.
They’re actual Rust functions that:
- Take code as input
- Do something with it
- Output new code
graph TD A["Your Code"] --> B["Procedural Macro"] B --> C["Reads & Analyzes"] C --> D["Generates New Code"] D --> E["Final Program"]
Why use them?
- More powerful than
macro_rules! - Can do complex logic
- Can inspect code structure
Three types of procedural macros:
- Derive macros – Add traits automatically
- Attribute-like macros – Custom attributes
- Function-like macros – Look like function calls
⭐ Derive Macros
Derive macros are the most common. You’ve probably seen them already!
When you write #[derive(Something)], a derive macro automatically writes code for you.
#[derive(Debug, Clone)]
struct Cat {
name: String,
age: u8,
}
// The macro AUTOMATICALLY writes:
// - Debug printing code
// - Clone copying code
// You didn't have to write it!
It’s like getting a free bonus:
- You build a toy car
- The factory adds wheels automatically
- You didn’t have to attach them yourself!
Creating Your Own Derive Macro
// In a separate crate:
use proc_macro::TokenStream;
#[proc_macro_derive(MyTrait)]
pub fn my_derive(input: TokenStream)
-> TokenStream {
// Read the input struct
// Generate implementation
// Return new code
}
🏷️ Attribute-like Macros
These macros attach to items like a sticky note with instructions.
#[route(GET, "/")]
fn home() {
// This function handles GET /
}
The #[route(...)] is an attribute macro. It reads your function and generates routing code.
Think of it like:
- You put a label on a box: “FRAGILE”
- The delivery person sees it and handles it carefully
- The label changes how things work!
Creating an Attribute Macro
#[proc_macro_attribute]
pub fn route(
attr: TokenStream, // The (GET, "/")
item: TokenStream // The function
) -> TokenStream {
// Generate routing code
}
📞 Function-like Macros
These look like regular function calls but with !:
let query = sql!(
SELECT * FROM users
WHERE active = true
);
Why not just a regular function?
- Can do things at compile time
- Can create custom syntax
- Can generate specialized code
Creating a Function-like Macro
#[proc_macro]
pub fn sql(input: TokenStream)
-> TokenStream {
// Parse the SQL
// Validate it at compile time
// Generate safe query code
}
🗺️ The Macro Family Tree
graph TD A["Rust Macros"] --> B["Declarative"] A --> C["Procedural"] B --> D["macro_rules!"] C --> E["Derive"] C --> F["Attribute-like"] C --> G["Function-like"] D --> H["Pattern matching<br>Simple & fast"] E --> I["#[derive#40;...#41;]<br>Auto-implement traits"] F --> J["#[custom_attr]<br>Modify items"] G --> K["custom!#40;...#41;<br>Custom syntax"]
🎁 Quick Comparison
| Feature | Declarative | Procedural |
|---|---|---|
| Syntax | macro_rules! |
proc_macro functions |
| Power | Pattern matching | Full Rust logic |
| Complexity | Simpler | More complex |
| Use case | Simple repetition | Complex generation |
| Crate needed? | No | Yes (separate crate) |
🚀 Real-World Examples
1. The vec! Macro (Declarative)
// You write:
let nums = vec![1, 2, 3];
// Macro expands to:
let nums = {
let mut temp = Vec::new();
temp.push(1);
temp.push(2);
temp.push(3);
temp
};
2. The #[derive(Debug)] (Derive)
// You write:
#[derive(Debug)]
struct Point { x: i32, y: i32 }
// Macro generates:
impl std::fmt::Debug for Point {
fn fmt(&self, f: &mut Formatter)
-> Result {
// ... printing code ...
}
}
3. Web Framework Routes (Attribute)
// You write:
#[get("/hello/<name>")]
fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}
// Macro generates routing logic
💡 Key Takeaways
- Macros = Code that writes code 📝
- Declarative macros use
macro_rules!with patterns - Procedural macros are Rust functions that generate code
- Derive macros auto-implement traits
- Attribute macros modify items with
#[attr] - Function-like macros look like
name!(...)
🎯 Remember This!
Macros are like recipes. You write the recipe once, and anyone can follow it to make the same dish. The recipe (macro) stays the same, but you can make it with different ingredients (inputs) each time!
You’re now ready to:
- ✅ Read and understand macro code
- ✅ Use built-in macros confidently
- ✅ Know which macro type to choose
- ✅ Start writing your own simple macros!
Happy coding, fellow Rustacean! 🦀✨
