Rust Derive Macros
Introduction
If you've been coding in Rust for even a short time, you've likely encountered code like this:
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
The #[derive(...)]
attribute is one of Rust's most powerful features, allowing you to automatically implement traits for your data structures. But what exactly is happening behind the scenes? How do derive macros work, and how can you create your own?
In this tutorial, we'll explore Rust derive macros in detail - from using built-in derive macros to creating custom ones for your projects.
What Are Derive Macros?
Derive macros are a special type of procedural macro in Rust that allow you to automatically implement traits for your custom data types. They work by generating code at compile time, saving you from writing boilerplate implementations.
The #[derive(...)]
attribute is placed above a struct, enum, or union declaration and tells the Rust compiler to automatically implement the specified traits for that type.
How Derive Macros Differ from Other Macros
Rust has several types of macros:
- Declarative macros (
macro_rules!
) - Pattern-matching macros that look like function calls - Procedural macros - More powerful macros that operate on the token stream of Rust code
- Function-like macros - Look like function calls but operate at compile time
- Attribute macros - Define custom attributes to attach to items
- Derive macros - Automatically implement traits for data structures
Derive macros are special because they focus specifically on implementing traits for data types, making them perfect for reducing repetitive code.
Using Built-in Derive Macros
Rust's standard library provides many useful derive macros that you can use right away:
Common Built-in Derive Macros
Debug
The Debug
trait enables formatted debug output with the {:?}
formatting specifier:
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
println!("{:?}", person); // Output: Person { name: "Alice", age: 30 }
}
Clone and Copy
These traits allow you to duplicate values:
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = p1; // Would move p1 without Copy
println!("p1: {:?}", p1); // Still accessible because of Copy
println!("p2: {:?}", p2);
let p3 = p1.clone(); // Explicitly clone
println!("p3: {:?}", p3);
}
Note:
Copy
can only be derived if all fields areCopy
. Types likeString
andVec
are notCopy
.
PartialEq and Eq
These traits enable equality comparisons:
#[derive(Debug, PartialEq, Eq)]
struct Car {
make: String,
model: String,
year: u16,
}
fn main() {
let car1 = Car {
make: String::from("Toyota"),
model: String::from("Corolla"),
year: 2020,
};
let car2 = Car {
make: String::from("Toyota"),
model: String::from("Corolla"),
year: 2020,
};
let car3 = Car {
make: String::from("Honda"),
model: String::from("Civic"),
year: 2021,
};
println!("car1 == car2: {}", car1 == car2); // true
println!("car1 == car3: {}", car1 == car3); // false
}
Default
The Default
trait provides default values for a type:
#[derive(Debug, Default)]
struct Configuration {
hostname: String,
port: u16,
max_connections: usize,
timeout_seconds: u64,
}
fn main() {
// Use all defaults
let config1 = Configuration::default();
println!("Default config: {:?}", config1);
// Use defaults for some fields
let config2 = Configuration {
port: 8080,
..Configuration::default()
};
println!("Custom port config: {:?}", config2);
}
Hash
The Hash
trait enables hashing for use with HashMap
and HashSet
:
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq, Hash)]
struct Student {
id: u32,
name: String,
}
fn main() {
let mut grades = HashMap::new();
let alice = Student {
id: 1,
name: String::from("Alice"),
};
let bob = Student {
id: 2,
name: String::from("Bob"),
};
grades.insert(alice, 95);
grades.insert(bob, 87);
println!("Grades: {:?}", grades);
}
How Derive Macros Work
When you use a derive macro, the Rust compiler:
- Parses your struct or enum definition into a token stream
- Passes this token stream to the derive macro implementation
- The macro analyzes the structure and generates code to implement the trait
- The generated code is included in your program at compile time
Let's see a simplified visualization of this process:
Creating Custom Derive Macros
Creating your own derive macros requires a bit more setup, but it's incredibly powerful for reducing boilerplate in your codebase.
Example: Creating a SimpleLogger Derive Macro
Let's create a SimpleLogger
trait and a derive macro for it. Our trait will provide a simple log()
method that prints the struct's fields.
Step 1: Project Setup
You'll need two crates:
- A "proc-macro" crate that defines the macro
- A main crate that exports the trait definition and re-exports the derive macro
First, create a new workspace:
# Cargo.toml
[workspace]
members = [
"simple_logger",
"simple_logger_derive",
]
Step 2: Create the Trait Definition Crate
// simple_logger/src/lib.rs
pub trait SimpleLogger {
fn log(&self);
}
// Re-export the derive macro
pub use simple_logger_derive::SimpleLogger;
# simple_logger/Cargo.toml
[package]
name = "simple_logger"
version = "0.1.0"
edition = "2021"
[dependencies]
simple_logger_derive = { path = "../simple_logger_derive" }
Step 3: Create the Derive Macro Crate
# simple_logger_derive/Cargo.toml
[package]
name = "simple_logger_derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"
// simple_logger_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};
#[proc_macro_derive(SimpleLogger)]
pub fn derive_simple_logger(input: TokenStream) -> TokenStream {
// Parse the input tokens into a syntax tree
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
// Extract the fields from the struct
let fields = match &input.data {
Data::Struct(data) => {
match &data.fields {
Fields::Named(fields) => {
fields.named.iter().map(|f| {
let field_name = &f.ident;
quote! {
.field(stringify!(#field_name), &self.#field_name)
}
}).collect::<Vec<_>>()
},
_ => panic!("SimpleLogger only supports structs with named fields"),
}
},
_ => panic!("SimpleLogger only supports structs with named fields"),
};
// Generate the implementation
let expanded = quote! {
impl SimpleLogger for #name {
fn log(&self) {
println!("{} {{", stringify!(#name));
println!(" {}",
std::fmt::Debug::format(&self)
.to_string()
.trim_start_matches(stringify!(#name))
.trim_start_matches(" { ")
.trim_end_matches(" }")
.replace(", ", ",
")
);
println!("}}");
}
}
};
// Return the generated code as a token stream
TokenStream::from(expanded)
}
Step 4: Using Your Custom Derive Macro
Now you can use your custom derive macro in your code:
use simple_logger::SimpleLogger;
#[derive(Debug, SimpleLogger)]
struct User {
id: u32,
name: String,
email: String,
active: bool,
}
fn main() {
let user = User {
id: 1,
name: String::from("John Doe"),
email: String::from("[email protected]"),
active: true,
};
user.log();
}
Output:
User {
id: 1,
name: "John Doe",
email: "[email protected]",
active: true
}
Advanced Derive Macro Techniques
Attribute Parameters
You can enhance your derive macros with additional parameters:
#[derive(SimpleLogger)]
#[logger(prefix = "USER_LOG")]
struct User {
// fields...
}
This requires additional parsing logic in your macro.
Conditional Trait Implementation
Your derive macro can conditionally implement traits based on the fields or other attributes:
#[proc_macro_derive(MyTrait, attributes(option))]
pub fn derive_my_trait(input: TokenStream) -> TokenStream {
// Parse input
let input = parse_macro_input!(input as DeriveInput);
// Check for specific attributes
let has_special_option = input.attrs.iter()
.any(|attr| attr.path.is_ident("option"));
// Generate different implementations based on conditions
if has_special_option {
// Generate one implementation
} else {
// Generate another implementation
}
// Return the implementation
}
Supporting Enums
To support enums in your derive macros, you'll need to handle variants:
#[proc_macro_derive(MyTrait)]
pub fn derive_my_trait(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match &input.data {
Data::Struct(data) => {
// Handle struct
},
Data::Enum(data) => {
// Handle enum variants
for variant in &data.variants {
// Process each variant
}
},
Data::Union(_) => {
// Handle union
}
}
// Generate and return implementation
}
Real-World Applications
Derive macros are used extensively in the Rust ecosystem:
Serde for Serialization and Deserialization
The Serde library uses derive macros for implementing serialization and deserialization:
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
struct Config {
server: String,
port: u16,
database: DatabaseConfig,
}
#[derive(Serialize, Deserialize, Debug)]
struct DatabaseConfig {
url: String,
username: String,
password: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Deserialize from JSON
let json = r#"
{
"server": "127.0.0.1",
"port": 8080,
"database": {
"url": "postgres://localhost/mydb",
"username": "admin",
"password": "password123"
}
}
"#;
let config: Config = serde_json::from_str(json)?;
println!("Deserialized: {:?}", config);
// Serialize back to JSON
let serialized = serde_json::to_string_pretty(&config)?;
println!("Serialized:
{}", serialized);
Ok(())
}
Builder Pattern with derive_builder
The derive_builder
crate provides a derive macro for implementing the Builder pattern:
use derive_builder::Builder;
#[derive(Builder, Debug)]
#[builder(setter(into))]
struct HttpRequest {
url: String,
method: String,
#[builder(default = "vec![]")]
headers: Vec<(String, String)>,
#[builder(default)]
body: Option<String>,
#[builder(default = "30")]
timeout_seconds: u32,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let request = HttpRequestBuilder::default()
.url("https://api.example.com/data")
.method("GET")
.header(("User-Agent", "MyClient/1.0"))
.header(("Accept", "application/json"))
.timeout_seconds(60)
.build()?;
println!("Request: {:?}", request);
Ok(())
}
Command-Line Arguments with clap
The clap
crate uses derive macros to simplify command-line argument parsing:
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about)]
struct Args {
/// Name of the person to greet
#[clap(short, long)]
name: String,
/// Number of times to greet
#[clap(short, long, default_value_t = 1)]
count: u8,
/// Whether to use loud greeting
#[clap(long)]
loud: bool,
}
fn main() {
let args = Args::parse();
let greeting = if args.loud {
format!("HELLO, {}!", args.name.to_uppercase())
} else {
format!("Hello, {}", args.name)
};
for _ in 0..args.count {
println!("{}", greeting);
}
}
Summary
Derive macros are one of Rust's most powerful features, enabling automatic trait implementation and drastically reducing boilerplate code. In this tutorial, we've explored:
- What derive macros are and how they differ from other macro types
- Using built-in derive macros like
Debug
,Clone
,PartialEq
, etc. - How derive macros work behind the scenes
- Creating custom derive macros for your own traits
- Advanced techniques for more complex derive macros
- Real-world applications of derive macros in popular Rust crates
By understanding and leveraging derive macros, you can make your Rust code more concise, maintainable, and expressive.
Additional Resources
- The Rust Programming Language Book - Procedural Macros
- The syn crate documentation
- The quote crate documentation
- The proc-macro2 crate documentation
- Rust Reference - Procedural Macros
Exercises
-
Modify the
SimpleLogger
derive macro to include the name of each field before its value in the output. -
Create a
JsonSchema
derive macro that generates a JSON schema for a struct. -
Implement a
NewType
derive macro that automatically implements common traits likeDeref
,AsRef
, andFrom
for newtype pattern structs. -
Extend the
SimpleLogger
derive macro to support enums. -
Create a
Validate
derive macro that generates validation methods based on field attributes like#[validate(min = 0, max = 100)]
.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)