Rust Macros: When Your Code Writes Your Code 🦀🪄
Rust Macros: When Your Code Writes Your Code 🦀🪄
Hot take: If you've never used Rust macros, you've been copy-pasting code like a caveman when you could've been wielding the power of meta-programming! 🔥
You've seen println!(), vec![], and panic!() everywhere in Rust. Notice those exclamation marks? Those aren't functions - they're MACROS! Code that generates code at compile time!
Macros in Rust let you write code that writes code, eliminate boilerplate, and do things that are literally impossible with functions. And the best part? Zero runtime cost! It all happens at compile time! 🚀
What Even ARE Macros? 🤔
The simple explanation: Macros are code that generates code BEFORE compilation.
In JavaScript, you'd write:
// Same code, repeated 5 times
console.log("User ID:", user.id);
console.log("User Name:", user.name);
console.log("User Email:", user.email);
console.log("User Age:", user.age);
console.log("User City:", user.city);
// Or use a loop... but what if you need custom logic?
With Rust macros:
// Write the pattern ONCE, generate code for all fields!
macro_rules! log_field {
($obj:expr, $($field:ident),+) => {
$(
println!("{}: {:?}", stringify!($field), $obj.$field);
)+
};
}
log_field!(user, id, name, email, age, city);
// Generates all 5 println! calls at compile time!
The magic: The macro expands into 5 println! calls BEFORE compilation. No runtime loops. No overhead. Just pure, generated code! ✨
Why Macros Beat Functions (Sometimes) 💪
Functions can't do THIS:
1. Variable Number of Arguments
// vec! macro - you've used this!
let numbers = vec![1, 2, 3, 4, 5];
// How does it work? You can't do this with functions:
fn vec(args: ???) -> Vec<T> {
// How many arguments? What type? Can't know at compile time!
}
// But macros can!
macro_rules! vec {
($($x:expr),*) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
2. Different Return Types
// println! returns ()
println!("Hello");
// But format! returns String
let s = format!("Hello");
// Same macro pattern, different types!
// Functions can't change return types like this!
3. Generate Code Based on Input
// Macros can inspect your code and generate different code!
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("You called {:?}()", stringify!($func_name));
}
};
}
// Generate three different functions!
create_function!(foo);
create_function!(bar);
create_function!(baz);
// Now you have foo(), bar(), and baz() functions!
// Try doing THAT with a function! 🤯
Declarative Macros: The Pattern Matching Kind 🎯
These are the macro_rules! macros you see everywhere.
Example 1: A Simple Logger
macro_rules! log {
($msg:expr) => {
println!("[LOG] {}", $msg);
};
($level:expr, $msg:expr) => {
println!("[{}] {}", $level, $msg);
};
}
// Use it:
log!("Something happened"); // [LOG] Something happened
log!("ERROR", "Database connection failed"); // [ERROR] Database...
The pattern: Different "arms" like match expressions! The macro expands to different code based on what you pass!
Example 2: HashMap Creation (Like vec!)
macro_rules! hashmap {
($($key:expr => $value:expr),* $(,)?) => {
{
let mut map = std::collections::HashMap::new();
$(
map.insert($key, $value);
)*
map
}
};
}
// Beautiful syntax!
let scores = hashmap! {
"Alice" => 100,
"Bob" => 85,
"Charlie" => 92,
};
// No more HashMap::new() and manual inserts! 🎉
Example 3: Implement Trait for Multiple Types
macro_rules! impl_display {
($($type:ty),+) => {
$(
impl std::fmt::Display for $type {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
)+
};
}
// Implement Display for multiple types at once!
impl_display!(Point, Rectangle, Circle);
// No copy-paste! The macro generates all three implementations!
Procedural Macros: The Heavy Artillery 🚀
These are macros that run actual Rust code to generate code!
The #[derive] Magic
You've used these:
#[derive(Debug, Clone, PartialEq)]
struct User {
id: u64,
name: String,
}
// The derive macro GENERATES implementations for you!
What's happening behind the scenes:
deriveis a procedural macro- It reads your struct definition
- Generates implementations of Debug, Clone, PartialEq
- All at compile time!
Without macros, you'd write:
impl Debug for User {
fn fmt(&self, f: &mut Formatter) -> Result {
// 15 lines of boilerplate...
}
}
impl Clone for User {
fn clone(&self) -> Self {
// More boilerplate...
}
}
impl PartialEq for User {
fn eq(&self, other: &Self) -> bool {
// Even more boilerplate...
}
}
// 50+ lines of code you didn't have to write! 🎁
Custom Derive Macros (The Power Move)
Example: Auto-generate builder pattern
// With a custom derive macro:
#[derive(Builder)]
struct User {
id: u64,
name: String,
email: String,
}
// The macro generates:
let user = User::builder()
.id(1)
.name("Alice".into())
.email("[email protected]".into())
.build();
// Entire builder pattern generated automatically! 🪄
Try maintaining that without macros! You'd have 50+ lines of builder code PER struct!
Attribute Macros: The Annotations on Steroids 💉
These look like annotations but do WAY more!
Example: Web Framework Routes
// In frameworks like Axum or Rocket:
#[get("/users/<id>")]
fn get_user(id: u64) -> Json<User> {
// Your code here
}
// The macro generates:
// - Route registration
// - Path parameter parsing
// - Type conversions
// - Error handling
// All invisible to you! 🎯
Example: Async Runtime
#[tokio::main]
async fn main() {
// Your async code
}
// The macro expands to:
fn main() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
// Your async code here
})
}
// Saves you from writing runtime setup every time!
Real-World Magic: The Power Patterns 🔮
Pattern 1: Testing Macros
macro_rules! test_math_op {
($name:ident, $op:tt, $a:expr, $b:expr, $expected:expr) => {
#[test]
fn $name() {
assert_eq!($a $op $b, $expected);
}
};
}
// Generate 10 tests with one macro!
test_math_op!(test_add, +, 2, 2, 4);
test_math_op!(test_sub, -, 5, 3, 2);
test_math_op!(test_mul, *, 3, 4, 12);
test_math_op!(test_div, /, 10, 2, 5);
// Each call generates a complete test function!
Pattern 2: Configuration Structs
macro_rules! config_struct {
($name:ident { $($field:ident: $type:ty = $default:expr),* }) => {
pub struct $name {
$(pub $field: $type,)*
}
impl Default for $name {
fn default() -> Self {
Self {
$($field: $default,)*
}
}
}
};
}
// Use it:
config_struct!(ServerConfig {
host: String = "localhost".into(),
port: u16 = 8080,
workers: usize = 4,
timeout: u64 = 30
});
// Generates struct + Default implementation! 🎁
Pattern 3: Error Type Generation
macro_rules! define_errors {
($($name:ident => $msg:expr),+ $(,)?) => {
#[derive(Debug)]
pub enum Error {
$($name,)+
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
$(Error::$name => write!(f, $msg),)+
}
}
}
};
}
// Define your entire error enum + Display in one shot!
define_errors! {
NotFound => "Resource not found",
Unauthorized => "Authentication required",
ServerError => "Internal server error",
Timeout => "Request timed out",
}
// All error handling code generated! 🚀
The dbg!() Macro: Your New Best Friend 🐛
Built-in debugging magic:
let x = 5;
let y = 10;
// Instead of:
println!("x = {:?}, y = {:?}", x, y);
// Use dbg!:
dbg!(x, y);
// Prints: [src/main.rs:3] x = 5
// [src/main.rs:3] y = 10
// Shows FILE and LINE NUMBER automatically! 🎯
Even better - use in expressions:
fn expensive_calculation() -> i32 {
42
}
let result = dbg!(expensive_calculation()) * 2;
// Prints: [src/main.rs:8] expensive_calculation() = 42
// Still returns 42 for the calculation!
// Debug AND keep your expression! Magic! ✨
When Macros Solve Real Problems 🛠️
Problem: Repetitive JSON Serialization
Without macros:
impl Serialize for User {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// 30 lines of boilerplate...
}
}
// For EVERY struct! 😱
With #[derive(Serialize)]:
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}
// Done! One line! 🎉
Problem: SQL Query Building
Without macros:
let query = format!(
"SELECT * FROM users WHERE id = {} AND name = '{}'",
id, name
);
// SQL injection vulnerability! 💀
With macros (sqlx):
let user = sqlx::query_as!(
User,
"SELECT * FROM users WHERE id = $1 AND name = $2",
id,
name
)
.fetch_one(&pool)
.await?;
// Compile-time SQL validation!
// Type-safe parameters!
// No SQL injection possible! 🛡️
Common Macro Patterns You'll Love ❤️
Pattern 1: Match-like Syntax
macro_rules! execute {
(if $cond:expr => $then:expr) => {
if $cond { $then }
};
(if $cond:expr => $then:expr; else => $else:expr) => {
if $cond { $then } else { $else }
};
}
execute!(if x > 0 => println!("Positive"));
execute!(if x > 0 => println!("+"); else => println!("-"));
Pattern 2: Repeated Code
macro_rules! implement_ops {
($type:ty) => {
impl Add for $type {
type Output = Self;
fn add(self, other: Self) -> Self {
self + other
}
}
impl Sub for $type {
type Output = Self;
fn sub(self, other: Self) -> Self {
self - other
}
}
};
}
// Implement for multiple types
implement_ops!(Point);
implement_ops!(Vector);
Macro Hygiene: The Safety Net 🥅
The cool part: Macros can't accidentally capture your variables!
macro_rules! using_a {
($e:expr) => {
{
let a = 42; // Macro's internal variable
$e
}
};
}
let a = 100;
let result = using_a!(a * 2); // Uses YOUR a (100), not macro's a (42)!
// Result is 200, not 84!
// Macros have "hygiene" - they can't accidentally mess with your variables! 🎯
Common Gotchas (Save Yourself Some Pain) 🚨
Gotcha #1: Macro Evaluation
macro_rules! times_two {
($e:expr) => {
$e + $e // ❌ Evaluates $e TWICE!
};
}
let mut x = 1;
let y = times_two!(x += 1); // x += 1 happens twice!
// y = 3, x = 3 (unexpected!)
// FIX:
macro_rules! times_two {
($e:expr) => {
{
let temp = $e; // ✅ Evaluate once!
temp + temp
}
};
}
Gotcha #2: Macro Scoping
// Macros need to be defined BEFORE use
foo!(); // ❌ Error: macro not found
macro_rules! foo {
() => { println!("foo"); };
}
foo!(); // ✅ Works!
Gotcha #3: Debugging Macro Expansions
# See what your macros expand to:
cargo expand
# Or for a specific file:
cargo expand --lib my_module
# Mind = blown when you see the generated code! 🤯
The Bottom Line 🎯
Macros in Rust aren't just a feature - they're a superpower that lets you:
- Eliminate boilerplate (derive macros = free implementations!)
- Extend the language (create your own syntax!)
- Generate code at compile time (zero runtime cost!)
- Type-safe code generation (compiler checks everything!)
- DRY principle on steroids (write once, generate everywhere!)
Think about it: Would you rather copy-paste 50 lines of boilerplate for every struct, or write #[derive(Debug)] and call it a day?
I know my answer! 🦀
Remember:
- Declarative macros (
macro_rules!) for pattern-based generation - Derive macros (
#[derive]) for trait implementations - Attribute macros (
#[custom]) for annotation-based code gen - All macro expansion happens at COMPILE TIME (zero cost!)
- Use
cargo expandto see what macros generate (mind-blowing!)
Rust macros prove that meta-programming doesn't have to be scary. It can be safe, powerful, and actually fun to use! 🪄✨
Want to geek out about macros? Connect with me on LinkedIn - Let's talk code generation!
Ready to write code that writes code? Check out my GitHub and follow this blog!
Now go macro-ify all the things! 🦀🚀