Advanced Level
Macros
Chapter 22: Macros š¦
Write code that generates code with Rust's powerful macro system.
Understanding Macros
Macros are a way of writing code that writes other code (metaprogramming). They allow you to extend Rust's syntax:
fn main() {
// Built-in macros
let v = vec![1, 2, 3];
println!("Hello, world!");
assert_eq!(2 + 2, 4);
// These are all macros that generate code
}Declarative Macros (macro_rules!)
Basic Macro Definition
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
fn main() {
say_hello!();
}Pattern Matching in Macros
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();
bar();
}Multiple Patterns
macro_rules! my_vec {
() => {
Vec::new()
};
($x:expr) => {
{
let mut temp_vec = Vec::new();
temp_vec.push($x);
temp_vec
}
};
($x:expr, $($y:expr),+) => {
{
let mut temp_vec = Vec::new();
temp_vec.push($x);
$(
temp_vec.push($y);
)+
temp_vec
}
};
}
fn main() {
let v1: Vec<i32> = my_vec![];
let v2 = my_vec![1];
let v3 = my_vec![1, 2, 3];
println!("{:?}, {:?}, {:?}", v1, v2, v3);
}Procedural Macros
Function-like Macros
// In Cargo.toml:
// [dependencies]
// proc-macro2 = "1.0"
// quote = "1.0"
// syn = "2.0"
// In lib.rs:
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
// Build the impl block
let gen = quote! {
// Implementation here
};
gen.into()
}Derive Macros
// In lib.rs:
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}Attribute-like Macros
// In lib.rs:
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as syn::ItemFn);
let gen = quote! {
#input
// Additional generated code here
};
gen.into()
}Macro Hygiene
Variable Scoping in Macros
macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
};
}
fn main() {
let a = 10;
let b = using_a!(a + 1); // This refers to the outer `a`
println!("b = {}", b); // 11, not 43
}Debugging Macros
Using cargo expand
# Install cargo-expand
cargo install cargo-expand
# Expand macros in your code
cargo expand
# Expand macros for a specific function
cargo expand --bin binary_name function_namePractical Examples
Example 1: Custom Logging Macro
macro_rules! log {
($level:expr, $msg:expr) => {
println!("[{}] {}", $level, $msg);
};
($level:expr, $fmt:expr, $($arg:tt)*) => {
println!(concat!("[{}] ", $fmt), $level, $($arg)*);
};
}
macro_rules! debug {
($msg:expr) => {
log!("DEBUG", $msg);
};
($fmt:expr, $($arg:tt)*) => {
log!("DEBUG", $fmt, $($arg)*);
};
}
macro_rules! error {
($msg:expr) => {
log!("ERROR", $msg);
};
($fmt:expr, $($arg:tt)*) => {
log!("ERROR", $fmt, $($arg)*);
};
}
fn main() {
debug!("Starting application");
error!("Failed to connect to database");
debug!("User {} logged in", "Alice");
error!("Error code: {}", 404);
}Example 2: Builder Pattern Macro
macro_rules! builder {
(
$builder_name:ident {
$($field_name:ident: $field_type:ty),* $(,)?
}
) => {
#[derive(Debug)]
struct $builder_name {
$($field_name: Option<$field_type>,)*
}
impl $builder_name {
pub fn new() -> Self {
Self {
$($field_name: None,)*
}
}
$(
pub fn $field_name(mut self, value: $field_type) -> Self {
self.$field_name = Some(value);
self
}
)*
pub fn build(self) -> Result<ConstructedType, &'static str> {
ConstructedType::new($($field_name: self.$field_name.ok_or(concat!("Missing field: ", stringify!($field_name)))?),*)
}
}
};
}
#[derive(Debug)]
struct ConstructedType {
name: String,
age: u32,
email: String,
}
impl ConstructedType {
fn new(name: String, age: u32, email: String) -> Result<Self, &'static str> {
if name.is_empty() {
return Err("Name cannot be empty");
}
if email.is_empty() {
return Err("Email cannot be empty");
}
Ok(ConstructedType { name, age, email })
}
}
builder!(PersonBuilder {
name: String,
age: u32,
email: String,
});
fn main() {
let person = PersonBuilder::new()
.name(String::from("Alice"))
.age(30)
.email(String::from("alice@example.com"))
.build();
match person {
Ok(p) => println!("{:?}", p),
Err(e) => println!("Error: {}", e),
}
}Common Mistakes
ā Forgetting Semicolons in Macro Definitions
macro_rules! bad_macro {
() => {
println!("Hello") // Missing semicolon
}
}
fn main() {
bad_macro!(); // Error: unexpected token
}ā Proper Macro Definition
macro_rules! good_macro {
() => {
println!("Hello");
};
}
fn main() {
good_macro!();
}ā Incorrect Repetition Patterns
macro_rules! bad_vec {
($($x:expr),*) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x)
)* // Missing semicolon here can cause issues
temp_vec
}
};
}ā Correct Repetition Patterns
macro_rules! good_vec {
($($x:expr),*) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}Key Takeaways
- ā Macros allow metaprogramming and code generation
- ā
Declarative macros use
macro_rules!and pattern matching - ā Procedural macros are more powerful and require separate crates
- ā Three types of procedural macros: function-like, derive, and attribute-like
- ā Macros follow hygiene rules to prevent variable name conflicts
- ā
Use
cargo expandto debug and understand macro expansion - ā
Follow Rust naming conventions (macro names often end with
!) - ā Macros are expanded at compile time, not runtime
Ready for Chapter 23? ā Memory Management Deep Dive