Introduction to Rust Macros
Macros in Rust allow you to write code that writes code (metaprogramming). Unlike functions, macros are expanded at compile time, enabling powerful abstractions and reducing boilerplate. Rust has two types of macros: declarative macros (macro_rules!) and procedural macros.
1. Declarative Macros (macro_rules!)
Basic Syntax
// Basic macro that creates a function
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("You called {:?}()", stringify!($func_name));
}
};
}
create_function!(foo);
create_function!(bar);
fn main() {
foo(); // You called "foo"()
bar(); // You called "bar"()
}
Pattern Matching
macro_rules! vec_custom {
// No arguments: empty vector
() => {
Vec::new()
};
// Single element
($elem:expr) => {
{
let mut v = Vec::new();
v.push($elem);
v
}
};
// Multiple elements
($($elem:expr),+ $(,)?) => {
{
let mut v = Vec::new();
$(
v.push($elem);
)+
v
}
};
}
fn main() {
let v1 = vec_custom![];
let v2 = vec_custom![1];
let v3 = vec_custom![1, 2, 3, 4, 5];
println!("{:?}", v1); // []
println!("{:?}", v2); // [1]
println!("{:?}", v3); // [1, 2, 3, 4, 5]
}
Advanced Pattern Matching
macro_rules! hashmap {
($($key:expr => $value:expr),* $(,)?) => {
{
let mut map = std::collections::HashMap::new();
$(
map.insert($key, $value);
)*
map
}
};
}
fn main() {
let map = hashmap! {
"name" => "Alice",
"age" => "30",
"city" => "NYC",
};
println!("{:?}", map);
}
Repetition Patterns
macro_rules! calculate {
// Base case: single number
($value:expr) => {
$value
};
// Addition
($left:expr + $($rest:tt)*) => {
$left + calculate!($($rest)*)
};
// Multiplication
($left:expr * $($rest:tt)*) => {
$left * calculate!($($rest)*)
};
}
fn main() {
let result1 = calculate!(5 + 3 + 2);
let result2 = calculate!(5 * 3 * 2);
println!("Addition: {}", result1); // 10
println!("Multiplication: {}", result2); // 30
}
2. Procedural Macros
Procedural macros are more powerful and flexible. They operate on the token stream and can generate arbitrary Rust code. There are three types:
- Derive macros: #[derive(MyTrait)]
- Attribute macros: #[my_attribute]
- Function-like macros: my_macro!()
Derive Macros
// In your procedural macro crate (separate crate)
// Cargo.toml:
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = "2.0"
// quote = "1.0"
// proc-macro2 = "1.0"
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let builder_name = syn::Ident::new(
&format!("{}Builder", name),
name.span()
);
let fields = if let syn::Data::Struct(data) = &input.data {
if let syn::Fields::Named(fields) = &data.fields {
&fields.named
} else {
panic!("Builder only works with named fields");
}
} else {
panic!("Builder only works with structs");
};
let field_names: Vec<_> = fields.iter()
.map(|f| &f.ident)
.collect();
let field_types: Vec<_> = fields.iter()
.map(|f| &f.ty)
.collect();
let expanded = quote! {
pub struct #builder_name {
#(#field_names: Option<#field_types>,)*
}
impl #builder_name {
pub fn new() -> Self {
Self {
#(#field_names: None,)*
}
}
#(
pub fn #field_names(mut self, value: #field_types) -> Self {
self.#field_names = Some(value);
self
}
)*
pub fn build(self) -> Result<#name, String> {
Ok(#name {
#(
#field_names: self.#field_names
.ok_or(format!("{} is required", stringify!(#field_names)))?,
)*
})
}
}
impl #name {
pub fn builder() -> #builder_name {
#builder_name::new()
}
}
};
TokenStream::from(expanded)
}
#[derive(Builder)]
pub struct User {
name: String,
age: u32,
email: String,
}
fn main() {
let user = User::builder()
.name("Alice".to_string())
.age(30)
.email("alice@example.com".to_string())
.build()
.unwrap();
println!("User: {} ({}) - {}", user.name, user.age, user.email);
}
Attribute Macros
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn time_execution(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let fn_name = &input.sig.ident;
let fn_block = &input.block;
let fn_sig = &input.sig;
let fn_vis = &input.vis;
let expanded = quote! {
#fn_vis #fn_sig {
let start = std::time::Instant::now();
let result = (|| #fn_block)();
let duration = start.elapsed();
println!(
"Function '{}' took {:.2?}",
stringify!(#fn_name),
duration
);
result
}
};
TokenStream::from(expanded)
}
// Usage:
#[time_execution]
fn slow_function() {
std::thread::sleep(std::time::Duration::from_millis(100));
println!("Doing work...");
}
Function-like Macros
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let input_str = input.to_string();
// Basic SQL validation
let query = input_str.trim_matches('"');
if !query.to_uppercase().starts_with("SELECT") &&
!query.to_uppercase().starts_with("INSERT") &&
!query.to_uppercase().starts_with("UPDATE") &&
!query.to_uppercase().starts_with("DELETE") {
panic!("Invalid SQL query");
}
let expanded = quote! {
{
const QUERY: &str = #query;
QUERY
}
};
TokenStream::from(expanded)
}
// Usage:
fn main() {
let query = sql!("SELECT * FROM users WHERE age > 18");
println!("Query: {}", query);
}
3. Advanced Macro Techniques
Hygiene and Scope
// Macros are hygienic - they don't capture external variables
macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
};
}
fn main() {
let four = using_a!(a / 10); // This works
println!("{}", four); // 4
// But external 'a' is not affected
let a = 100;
let result = using_a!(a); // Uses macro's 'a', not external
println!("{}", result); // 42, not 100
}
Debugging Macros
// Use cargo expand to see expanded macros
// Install: cargo install cargo-expand
// Run: cargo expand
macro_rules! debug_vars {
($($var:ident),*) => {
$(
println!("{} = {:?}", stringify!($var), $var);
)*
};
}
fn main() {
let x = 10;
let y = 20;
let z = 30;
debug_vars!(x, y, z);
// Output:
// x = 10
// y = 20
// z = 30
}
4. Real-World Example: Testing Framework
macro_rules! test_suite {
(
suite: $suite_name:ident,
$(
test $test_name:ident $body:block
)*
) => {
mod $suite_name {
use super::*;
pub fn run_all() {
println!("\nRunning test suite: {}", stringify!($suite_name));
let mut passed = 0;
let mut failed = 0;
$(
print!(" Test {}: ", stringify!($test_name));
match std::panic::catch_unwind(|| $body) {
Ok(_) => {
println!("✓ PASSED");
passed += 1;
}
Err(_) => {
println!("✗ FAILED");
failed += 1;
}
}
)*
println!("\nResults: {} passed, {} failed", passed, failed);
}
}
};
}
test_suite! {
suite: math_tests,
test addition {
assert_eq!(2 + 2, 4);
}
test subtraction {
assert_eq!(5 - 3, 2);
}
test multiplication {
assert_eq!(3 * 4, 12);
}
test division {
assert_eq!(10 / 2, 5);
}
}
fn main() {
math_tests::run_all();
}
5. Macro Best Practices
🔑 Best Practices
- Keep it Simple: Prefer functions when possible
- Document Well: Explain what the macro does and how to use it
- Test Thoroughly: Macros can hide bugs
- Use Hygiene: Don't capture external variables unexpectedly
- Provide Good Errors: Use
compile_error!for clear messages - Limit Scope: Make macros as specific as possible
macro_rules! must_be_positive {
($val:expr) => {
if $val <= 0 {
compile_error!(
"Value must be positive! Use a positive literal."
);
}
$val
};
}
// This will cause a compile error:
// let x = must_be_positive!(-5);
Summary
✅ You've learned:
- Declarative macros with
macro_rules! - Pattern matching and repetition in macros
- Procedural macros: derive, attribute, and function-like
- Advanced techniques: hygiene, debugging
- Real-world applications and best practices
Macros are a powerful feature that sets Rust apart from many other languages. Master them to write more expressive, maintainable, and DRY code. Remember: with great power comes great responsibility - use macros wisely!