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_name

Practical 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 expand to 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

šŸ¦€ Rust Programming Tutorial

Learn from Zero to Advanced

Built with Next.js and Tailwind CSS • Open Source