Rust - Macros
Overview
Estimated time: 45–55 minutes
Master declarative macros in Rust using macro_rules!
. Learn pattern matching, repetition syntax, macro hygiene, and how to create powerful code generation tools that write Rust code for you.
Learning Objectives
- Understand what macros are and when to use them.
- Master
macro_rules!
syntax and pattern matching. - Learn repetition patterns and separators.
- Understand macro hygiene and scope.
- Create practical macros for code generation.
- Debug macro expansions and handle edge cases.
Prerequisites
What Are Macros?
Macros are code that writes other code (metaprogramming). Unlike functions that operate on values at runtime, macros operate on tokens at compile time and generate code before the program runs.
Why Use Macros?
- Reduce code duplication
- Create domain-specific languages
- Generate code based on patterns
- Handle variable numbers of arguments
- Perform compile-time computations
Basic Macro Syntax
// Simple macro that prints a message
macro_rules! say_hello {
() => {
println!("Hello from a macro!");
};
}
// Macro with parameters
macro_rules! say_something {
($message:expr) => {
println!("Message: {}", $message);
};
}
// Macro with multiple patterns
macro_rules! calculate {
(add $a:expr, $b:expr) => {
$a + $b
};
(multiply $a:expr, $b:expr) => {
$a * $b
};
}
fn main() {
say_hello!();
say_something!("Hello, World!");
say_something!(42);
let sum = calculate!(add 5, 3);
let product = calculate!(multiply 4, 7);
println!("Sum: {}", sum);
println!("Product: {}", product);
}
Expected Output:
Hello from a macro!
Message: Hello, World!
Message: 42
Sum: 8
Product: 28
Pattern Matching and Types
Macros use specific designators to match different types of tokens:
macro_rules! demonstrate_types {
// Expression
(expr $e:expr) => {
println!("Expression: {}", $e);
};
// Identifier
(ident $i:ident) => {
println!("Identifier: {}", stringify!($i));
};
// Type
(type $t:ty) => {
println!("Type: {}", stringify!($t));
};
// Pattern
(pat $p:pat) => {
let $p = 42;
println!("Pattern matched!");
};
// Statement
(stmt $s:stmt) => {
$s
println!("Statement executed!");
};
// Block
(block $b:block) => {
println!("Before block");
$b
println!("After block");
};
}
fn main() {
demonstrate_types!(expr 2 + 3);
demonstrate_types!(ident my_variable);
demonstrate_types!(type Vec<String>);
demonstrate_types!(pat x);
demonstrate_types!(stmt let y = 10);
demonstrate_types!(block {
println!("Inside block");
let z = 5;
});
}
Expected Output:
Expression: 5
Identifier: my_variable
Type: Vec<String>
Pattern matched!
Statement executed!
Before block
Inside block
After block
Repetition Patterns
Macros can handle variable numbers of arguments using repetition:
// Macro that sums any number of expressions
macro_rules! sum {
($($x:expr),*) => {
{
let mut total = 0;
$(
total += $x;
)*
total
}
};
}
// Macro that creates a vector with any number of elements
macro_rules! vec_of {
($($x:expr),*) => {
{
let mut v = Vec::new();
$(
v.push($x);
)*
v
}
};
}
// Macro with different separators
macro_rules! print_all {
($($x:expr);*) => {
$(
println!("{}", $x);
)*
};
}
fn main() {
let result1 = sum!(1, 2, 3, 4, 5);
let result2 = sum!(10, 20);
let result3 = sum!(); // Empty case
println!("Sum of 1,2,3,4,5: {}", result1);
println!("Sum of 10,20: {}", result2);
println!("Sum of nothing: {}", result3);
let numbers = vec_of!(1, 2, 3, 4);
println!("Vector: {:?}", numbers);
print_all!("First"; "Second"; "Third");
}
Expected Output:
Sum of 1,2,3,4,5: 15
Sum of 10,20: 30
Sum of nothing: 0
Vector: [1, 2, 3, 4]
First
Second
Third
Creating a HashMap Literal Macro
use std::collections::HashMap;
macro_rules! hashmap {
($($key:expr => $value:expr),* $(,)?) => {
{
let mut map = HashMap::new();
$(
map.insert($key, $value);
)*
map
}
};
}
fn main() {
let scores = hashmap! {
"Alice" => 95,
"Bob" => 87,
"Charlie" => 92,
};
println!("Scores: {:?}", scores);
// Works with different types
let config = hashmap! {
"debug".to_string() => true,
"max_connections".to_string() => false,
};
println!("Config: {:?}", config);
}
Expected Output:
Scores: {"Alice": 95, "Bob": 87, "Charlie": 92}
Config: {"debug": true, "max_connections": false}
Struct Generation Macro
// Macro that generates struct with getter methods
macro_rules! define_struct {
($name:ident { $($field:ident: $type:ty),* $(,)? }) => {
#[derive(Debug)]
struct $name {
$(
$field: $type,
)*
}
impl $name {
fn new($($field: $type),*) -> Self {
Self {
$(
$field,
)*
}
}
$(
paste::paste! {
fn [](&self) -> &$type {
&self.$field
}
fn [](&mut self, value: $type) {
self.$field = value;
}
}
)*
}
};
}
// Note: The paste crate is used for identifier concatenation
// Add to Cargo.toml: paste = "1.0"
// Simpler version without paste crate
macro_rules! simple_struct {
($name:ident { $($field:ident: $type:ty),* $(,)? }) => {
#[derive(Debug)]
struct $name {
$(
$field: $type,
)*
}
impl $name {
fn new($($field: $type),*) -> Self {
Self {
$(
$field,
)*
}
}
}
};
}
simple_struct! {
Person {
name: String,
age: u32,
email: String,
}
}
fn main() {
let person = Person::new(
"Alice".to_string(),
30,
"[email protected]".to_string(),
);
println!("Person: {:?}", person);
}
Expected Output:
Person: Person { name: "Alice", age: 30, email: "[email protected]" }
Debugging and Utility Macros
// Debug macro that prints variable name and value
macro_rules! dbg_var {
($var:expr) => {
println!("{} = {:?}", stringify!($var), $var);
};
}
// Macro for timing code execution
macro_rules! time_it {
($label:expr, $code:block) => {
{
let start = std::time::Instant::now();
let result = $code;
let duration = start.elapsed();
println!("{} took: {:?}", $label, duration);
result
}
};
}
// Macro for conditional compilation based on features
macro_rules! feature_code {
($feature:expr, $code:block) => {
#[cfg(feature = $feature)]
$code
};
}
// Macro that creates test functions
macro_rules! test_cases {
($test_name:ident: $($input:expr => $expected:expr),* $(,)?) => {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn $test_name() {
$(
assert_eq!(some_function($input), $expected);
)*
}
}
};
}
fn some_function(x: i32) -> i32 {
x * 2
}
fn main() {
let x = 42;
let y = vec![1, 2, 3];
dbg_var!(x);
dbg_var!(y);
dbg_var!(x + 10);
let result = time_it!("Heavy computation", {
let mut sum = 0;
for i in 0..1_000_000 {
sum += i;
}
sum
});
println!("Result: {}", result);
}
// This would create tests (commented out to avoid test framework requirement)
// test_cases! {
// double_function_tests:
// 1 => 2,
// 5 => 10,
// -3 => -6,
// }
Expected Output:
x = 42
y = [1, 2, 3]
x + 10 = 52
Heavy computation took: 1.234ms
Result: 499999500000
Error Handling in Macros
// Macro with compile-time error checking
macro_rules! compile_time_assert {
($condition:expr, $message:expr) => {
const _: () = {
if !$condition {
panic!($message);
}
};
};
}
// Macro that validates at compile time
macro_rules! create_array {
($size:expr, $value:expr) => {
{
compile_time_assert!($size > 0, "Array size must be positive");
compile_time_assert!($size <= 1000, "Array size too large");
[$value; $size]
}
};
}
// Macro with better error messages
macro_rules! expect_type {
($value:expr, i32) => {
$value as i32
};
($value:expr, f64) => {
$value as f64
};
($value:expr, $t:ty) => {
compile_error!(concat!("Unsupported type: ", stringify!($t)));
};
}
fn main() {
let arr1 = create_array!(5, 0);
println!("Array: {:?}", arr1);
// This would cause a compile error:
// let arr2 = create_array!(0, 1);
let x = 42;
let y = expect_type!(x, i32);
let z = expect_type!(x, f64);
println!("y: {}, z: {}", y, z);
// This would cause a compile error:
// let w = expect_type!(x, String);
}
Expected Output:
Array: [0, 0, 0, 0, 0]
y: 42, z: 42
Advanced Patterns and Techniques
// Recursive macro for generating nested structures
macro_rules! nested_vec {
($item:expr) => {
$item
};
($item:expr; $($rest:expr);+) => {
vec![nested_vec!($item), nested_vec!($($rest);+)]
};
}
// Macro with internal helper rules
macro_rules! count_items {
() => { 0 };
($head:expr) => { 1 };
($head:expr, $($tail:expr),*) => { 1 + count_items!($($tail),*) };
}
// Macro that generates different code based on count
macro_rules! smart_print {
($single:expr) => {
println!("Single item: {}", $single);
};
($first:expr, $second:expr) => {
println!("Pair: {} and {}", $first, $second);
};
($($items:expr),*) => {
{
let count = count_items!($($items),*);
println!("Multiple items ({}): ", count);
$(
print!("{} ", $items);
)*
println!();
}
};
}
// Macro with optional trailing elements
macro_rules! flexible_vec {
($($x:expr),+ $(; default $default:expr)?) => {
{
let mut v = vec![$($x),*];
$(
if v.is_empty() {
v.push($default);
}
)?
v
}
};
}
fn main() {
// let nested = nested_vec![1; 2; 3];
// println!("Nested: {:?}", nested);
smart_print!(42);
smart_print!(1, 2);
smart_print!(1, 2, 3, 4, 5);
let v1 = flexible_vec![1, 2, 3];
let v2 = flexible_vec![10, 20; default 99];
println!("v1: {:?}", v1);
println!("v2: {:?}", v2);
}
Expected Output:
Single item: 42
Pair: 1 and 2
Multiple items (5): 1 2 3 4 5
v1: [1, 2, 3]
v2: [10, 20]
Macro Hygiene
Rust macros are hygienic, meaning they don't accidentally capture variables from the calling scope:
macro_rules! declare_variable {
($name:ident, $value:expr) => {
let $name = $value; // This creates a new binding
};
}
macro_rules! use_temp_var {
($expr:expr) => {
{
let temp = 100; // This temp won't conflict with outer temp
$expr + temp
}
};
}
fn main() {
let temp = 50;
declare_variable!(x, 42);
println!("x: {}", x);
let result = use_temp_var!(temp); // Uses outer temp (50) + inner temp (100)
println!("Result: {}", result); // 150
// The macro's temp variable doesn't leak out
println!("Outer temp: {}", temp); // Still 50
}
Expected Output:
x: 42
Result: 150
Outer temp: 50
Best Practices
1. Keep Macros Simple
- Prefer functions when possible
- Use macros for code generation and repetition
- Document macro behavior clearly
2. Handle Edge Cases
macro_rules! safe_division {
($a:expr, $b:expr) => {
{
let denominator = $b;
if denominator != 0 {
Some($a / denominator)
} else {
None
}
}
};
}
3. Use Appropriate Visibility
// Export macro from crate
#[macro_export]
macro_rules! public_macro {
() => { println!("This macro is public"); };
}
// Local macro (default)
macro_rules! private_macro {
() => { println!("This macro is private"); };
}
4. Test Your Macros
#[cfg(test)]
mod tests {
#[test]
fn test_sum_macro() {
assert_eq!(sum!(1, 2, 3), 6);
assert_eq!(sum!(), 0);
assert_eq!(sum!(42), 42);
}
}
Common Pitfalls
1. Multiple Evaluation
// Bad: evaluates expression multiple times
macro_rules! bad_max {
($a:expr, $b:expr) => {
if $a > $b { $a } else { $b }
};
}
// Good: evaluates once
macro_rules! good_max {
($a:expr, $b:expr) => {
{
let a_val = $a;
let b_val = $b;
if a_val > b_val { a_val } else { b_val }
}
};
}
Summary
Declarative macros provide powerful metaprogramming capabilities:
- Code generation: Write code that writes code at compile time
- Pattern matching: Match different token types and structures
- Repetition: Handle variable numbers of arguments elegantly
- Hygiene: Automatic scoping prevents variable capture issues
- Compile-time: All macro expansion happens before runtime
- Type safety: Generated code is still checked by Rust's type system
Use macros when you need to eliminate repetitive code patterns or create domain-specific syntax that would be impossible with functions alone.