Rust Macros
Macros are powerful metaprogramming tools in Rust that allow you to write code that generates other code. This tutorial will comprehensively introduce various types of Rust macros, their usage methods, and practical application scenarios.
🎯 Content Overview
Through this chapter, you will understand and master:
- Understanding the concept and purpose of macros
- Mastering writing declarative macros
- Understanding the basic concepts of procedural macros
- Learning to use macros in actual projects
📖 What are Macros?
Definition of Macros
Rust Macros are a metaprogramming technique that allows you to write code that generates other code (creating custom syntax extensions when writing code). Unlike functions, macros are expanded at compile time, can accept a variable number of arguments, and can manipulate Rust's syntactic structures.
There are two types of macros in Rust: Declarative Macros and Procedural Macros.
This chapter mainly introduces declarative macros.
Difference Between Macros and Functions
| Feature | Functions | Macros |
|---|---|---|
| Expansion Time | Called at runtime | Expanded at compile time |
| Parameter Count | Fixed parameters | Variable parameters |
| Type Checking | Strict type checking | Pattern matching |
| Performance | Runtime overhead | Zero runtime overhead |
| Complexity | Relatively simple | Can be very complex |
Advantages of Macros
- Code Generation: Automatically generate repetitive code
- Zero-cost Abstraction: Expanded at compile time with no runtime overhead
- Syntax Extension: Create domain-specific languages (DSL)
- Type Safety: Checked at compile time
🔧 Declarative Macros
Declarative macros are defined using the macro_rules! syntax and are the most common type of macro.
Basic Syntax
macro_rules! macro_name {
(pattern) => {
// expanded code
};
}Simple Example: Print Debug Information
macro_rules! debug_print {
($x:expr) => {
println!("Debug info: {} = {:?}", stringify!($x), $x);
};
}
fn main() {
let number = 42;
let name = "Zhang San";
debug_print!(number); // Output: Debug info: number = 42
debug_print!(name); // Output: Debug info: name = "Zhang San"
}Analysis of vec! Macro Implementation
Let's analyze in detail a simplified implementation of the vec! macro from the standard library:
macro_rules! vec {
// Empty vec case
() => {
Vec::new()
};
// With initial values case
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
// Repeated value case: vec![0; 5]
( $x:expr; $n:expr ) => {
vec![0; $n].into_iter().map(|_| $x).collect()
};
}
fn main() {
let empty_vec: Vec<i32> = vec![];
let number_vec = vec![1, 2, 3, 4, 5];
let repeated_vec = vec!["Hello"; 3];
println!("Empty vector: {:?}", empty_vec);
println!("Number vector: {:?}", number_vec);
println!("Repeated vector: {:?}", repeated_vec);
}Pattern Matching Details
Common Identifier Types
| Identifier | Description | Example |
|---|---|---|
$x:expr | Expression | 42, variable_name, function_call() |
$x:ident | Identifier | Variable name, function name |
$x:ty | Type | i32, String, Vec<T> |
$x:pat | Pattern | Some(x), Ok(_) |
$x:stmt | Statement | let x = 5; |
$x:block | Code block | { ... } |
$x:item | Item | Function, struct definition |
$x:tt | Token tree | Any token sequence |
Repetition Patterns
macro_rules! create_function {
(
$func_name:ident(
$($param_name:ident: $param_type:ty),*
) -> $return_type:ty {
$($stmt:stmt;)*
}
) => {
fn $func_name($($param_name: $param_type),*) -> $return_type {
$($stmt;)*
}
};
}
create_function!(
calculate_sum(a: i32, b: i32) -> i32 {
let result = a + b;
return result;
}
);
fn main() {
let sum = calculate_sum(10, 20);
println!("10 + 20 = {}", sum);
}Advanced Declarative Macro Examples
Macro for Creating Hash Maps
use std::collections::HashMap;
macro_rules! hashmap {
($($key:expr => $value:expr),* $(,)?) => {
{
let mut map = HashMap::new();
$(
map.insert($key, $value);
)*
map
}
};
}
fn main() {
let student_scores = hashmap!{
"Zhang San" => 85,
"Li Si" => 92,
"Wang Wu" => 78,
};
for (name, score) in &student_scores {
println!("{}: {}", name, score);
}
}Conditional Compilation Macro
macro_rules! platform_specific_code {
(windows => $windows_code:block, unix => $unix_code:block) => {
#[cfg(target_os = "windows")]
$windows_code
#[cfg(any(target_os = "linux", target_os = "macos"))]
$unix_code
};
}
fn main() {
platform_specific_code!(
windows => {
println!("Running on Windows system");
// Windows-specific code
},
unix => {
println!("Running on Unix system");
// Unix-specific code
}
);
}🔬 Procedural Macros
Procedural macros are a more advanced type of macro. They are actually functions that can manipulate Rust code's abstract syntax tree (AST).
Types of Procedural Macros
- Function-like macros
- Derive macros
- Attribute macros
Derive Macro Example
// Add dependency in Cargo.toml
// [dependencies]
// serde = { version = "1.0", features = ["derive"] }
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
name: String,
age: u32,
email: String,
}
fn main() {
let user1 = User {
name: "Li Ming".to_string(),
age: 25,
email: "liming@example.com".to_string(),
};
// Debug trait auto-implemented
println!("User info: {:?}", user1);
// Clone trait auto-implemented
let user2 = user1.clone();
// Serialize trait auto-implemented
let json = serde_json::to_string(&user1).unwrap();
println!("JSON: {}", json);
}Custom Derive Macro
To create a custom derive macro, you need to create a procedural macro crate:
# Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
proc-macro2 = "1.0"// src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(SayHello)]
pub fn say_hello_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let expanded = quote! {
impl #name {
pub fn say_hello(&self) -> String {
format!("Hello, I am {}!", stringify!(#name))
}
}
};
TokenStream::from(expanded)
}Using custom derive macro:
use my_macro_crate::SayHello;
#[derive(SayHello)]
struct Student {
name: String,
}
#[derive(SayHello)]
struct Teacher {
subject: String,
}
fn main() {
let student1 = Student { name: "Xiao Ming".to_string() };
let teacher1 = Teacher { subject: "Math".to_string() };
println!("{}", student1.say_hello()); // Output: Hello, I am Student!
println!("{}", teacher1.say_hello()); // Output: Hello, I am Teacher!
}🛠️ Practical Application Scenarios
1. Logging Macro
macro_rules! log {
(Error, $($arg:tt)*) => {
eprintln!("[Error] {}", format!($($arg)*));
};
(Warning, $($arg:tt)*) => {
println!("[Warning] {}", format!($($arg)*));
};
(Info, $($arg:tt)*) => {
println!("[Info] {}", format!($($arg)*));
};
}
fn main() {
log!(Info, "Application started");
log!(Warning, "Memory usage: {}%", 85);
log!(Error, "Failed to connect to database: {}", "Timeout");
}2. Testing Macro
macro_rules! assert_equal {
($left:expr, $right:expr) => {
if $left != $right {
panic!(
"Assertion failed: {} != {}\n Left: {:?}\n Right: {:?}",
stringify!($left),
stringify!($right),
$left,
$right
);
} else {
println!("✓ Test passed: {} == {}", stringify!($left), stringify!($right));
}
};
}
fn main() {
let a = 5;
let b = 5;
let c = 10;
assert_equal!(a, b); // Test passes
// assert_equal!(a, c); // Will trigger panic
}3. Configuration Macro
macro_rules! config {
{
database: {
host: $db_host:expr,
port: $db_port:expr,
user: $db_user:expr,
},
server: {
port: $server_port:expr,
workers: $workers:expr,
}
} => {
pub struct AppConfig {
pub database_host: String,
pub database_port: u16,
pub database_user: String,
pub server_port: u16,
pub worker_count: usize,
}
impl AppConfig {
pub fn new() -> Self {
Self {
database_host: $db_host.to_string(),
database_port: $db_port,
database_user: $db_user.to_string(),
server_port: $server_port,
worker_count: $workers,
}
}
}
};
}
config! {
database: {
host: "localhost",
port: 5432,
user: "admin",
},
server: {
port: 8080,
workers: 4,
}
}
fn main() {
let config = AppConfig::new();
println!("Database connection: {}:{}", config.database_host, config.database_port);
println!("Server port: {}", config.server_port);
}⚠️ Best Practices for Macros
1. Naming Conventions
- Use
snake_caseor Chinese naming for macro names - Avoid conflicting with standard library macros
- Use descriptive names
2. Error Handling
macro_rules! safe_division {
($a:expr, $b:expr) => {
{
if $b == 0 {
panic!("Cannot divide by zero!");
}
$a / $b
}
};
}
fn main() {
let result1 = safe_division!(10, 2); // Normal
println!("10 / 2 = {}", result1);
// let result2 = safe_division!(10, 0); // Will panic
}3. Documentation
/// Creates a vector with specified elements
///
/// # Examples
/// ```
/// let v = my_vec![1, 2, 3];
/// assert_eq!(v, vec![1, 2, 3]);
/// ```
macro_rules! my_vec {
($($x:expr),* $(,)?) => {
{
let mut v = Vec::new();
$(v.push($x);)*
v
}
};
}🎯 Common Pitfalls and Solutions
1. Hygiene Issues
// Problem: Variable name conflicts
macro_rules! problematic_macro {
($x:expr) => {
{
let temp = $x * 2; // May conflict with temp in user code
temp
}
};
}
// Solution: Use unique variable names
macro_rules! safe_macro {
($x:expr) => {
{
let __macro_internal_temp = $x * 2; // Use prefix to avoid conflicts
__macro_internal_temp
}
};
}2. Multiple Evaluation Problem
// Problem: Expression may be evaluated multiple times
macro_rules! side_effect_macro {
($x:expr) => {
$x + $x // $x is evaluated twice
};
}
// Solution: Evaluate first then use
macro_rules! safe_evaluation {
($x:expr) => {
{
let value = $x; // Evaluate only once
value + value
}
};
}
fn counter() -> i32 {
static mut COUNT: i32 = 0;
unsafe {
COUNT += 1;
COUNT
}
}
fn main() {
// Side effect macro will call counter() twice
// let result1 = side_effect_macro!(counter());
// Safe evaluation calls only once
let result2 = safe_evaluation!(counter());
println!("Result: {}", result2);
}📚 Summary
This tutorial introduced the core concepts and practical applications of Rust macros:
Main Content Review
- Basic Concepts of Macros: Understanding the difference between macros and functions
- Declarative Macros: Using
macro_rules!to create code generation templates - Procedural Macros: More advanced macro types that can manipulate AST
- Practical Applications: Logging, testing, configuration, and other scenarios
- Best Practices: Avoiding common pitfalls and writing safe macros
Key Points
- Macros are expanded at compile time, providing zero-cost abstraction
- Declarative macros are suitable for simple code generation
- Procedural macros provide more powerful features but are more complex
- Pay attention to hygiene and multiple evaluation issues when writing macros
Practice Recommendations
- Start practicing with simple declarative macros
- Read standard library macro source code to learn techniques
- Gradually apply macros in actual projects to reduce repetitive code
- Write detailed documentation and tests for complex macros
Important Notes
- Macros increase compilation time
- Overusing macros may reduce code readability
- Macro error messages are often difficult to understand
- Debugging macro code is challenging
Continue Learning: Next Chapter - Rust Smart Pointers