Loading...
Loading...

Rust Macros: Code Generation and Metaprogramming

Macros in Rust are powerful metaprogramming tools that generate code at compile time. Unlike functions, macros can take a variable number of arguments, operate on code syntax, and generate repetitive implementations automatically while maintaining type safety.

1. Declarative Macros (macro_rules!)

Declarative macros are the most common form of Rust macros, using pattern matching to generate code. They're defined with macro_rules! and work like match statements for code.

// Basic macro definition
macro_rules! say_hello {
    () => {  // Matches empty input
        println!("Hello!");
    };
}

// Macro with pattern matching
macro_rules! create_function {
    ($func_name:ident) => {  // Captures an identifier
        fn $func_name() {  // Uses captured name in function
            println!("Called: {}", stringify!($func_name));
        }
    };
}

fn main() {
    say_hello!();  // Expands to println!("Hello!");
    
    create_function!(foo); // Creates fn foo()
    foo();  // Prints "Called: foo"
}

Key Points: The ident specifies we expect an identifier. stringify! converts the token to a string literal. Macros can generate any valid Rust code.

2. Macro Pattern Matching

Advanced pattern matching allows macros to handle different input patterns. This is how Rust's vec! macro works internally.

macro_rules! vec {
    // Handle vec![1, 2, 3] - comma separated elements
    ($($element:expr),*) => {
        {
            let mut temp_vec = Vec::new();
            $(temp_vec.push($element);)*  // Repeat for each element
            temp_vec
        }
    };
    
    // Handle vec![0; 5] - element and count
    ($element:expr; $count:expr) => {
        {
            let mut temp_vec = Vec::with_capacity($count);
            for _ in 0..$count {
                temp_vec.push($element.clone());
            }
            temp_vec
        }
    };
}

fn main() {
    let v = vec![1, 2, 3];  // Uses first pattern
    let zeros = vec![0; 5];  // Uses second pattern
}

Repetition Patterns: The $(...)* syntax repeats the enclosed code for each match. expr matches any expression. Macros can have multiple arms like match statements.

3. Procedural Macros

Procedural macros are more powerful but complex. They accept TokenStream input and generate TokenStream output, operating like functions that transform code.

// In a separate crate with proc-macro = true in Cargo.toml
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Parse input into AST
    let ast = parse_macro_input!(input as DeriveInput);
    let name = &ast.ident;  // Get struct name
    
    // Generate implementation
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello from {}!", stringify!(#name));
            }
        }
    };
    
    gen.into()  // Convert back to TokenStream
}

// Usage in another crate:
#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro(); // Prints "Hello from Pancakes!"
}

Key Components: syn parses Rust code into AST, quote turns Rust syntax back into code. Procedural macros must be in their own crate.

4. Attribute-like Macros

Attribute macros create custom attributes that transform the item they annotate. Commonly used in web frameworks for route definitions.

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
    // attr: #[route(GET, "/")] - the attribute contents
    // item: fn handler() {...} - the annotated item
    
    // Parse attributes and generate code
    let output = quote! {
        #[rocket::get(#path)]  // Transform into framework attribute
        #item  // Keep original function
    };
    
    output.into()
}

// Usage:
#[route(GET, "/")]
fn index() -> &'static str {
    "Hello, world!"
}

Real-world Use: This pattern is used in Rocket and Actix for route declarations. The macro validates routes at compile time.

5. Function-like Procedural Macros

Function-like macros look like regular function calls but operate at compile time. Useful for DSLs (Domain Specific Languages).

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    // Parse and validate SQL at compile time
    let sql = parse_sql(input);
    let validation = validate_sql(&sql);
    
    quote! {
        {
            #validation  // Include any validation checks
            QueryBuilder::new(#sql)  // Generate builder
        }
    }.into()
}

// Usage:
let query = sql!(SELECT * FROM users WHERE id = 1);

Benefits: Catches SQL errors during compilation rather than runtime. The macro can optimize queries before they execute.

6. Debugging Macros

Debugging macros requires special techniques since they operate at compile time.

// 1. Show expanded macros
cargo rustc -- -Z unstable-options --pretty=expanded

// 2. Debug print in macros
macro_rules! debug_print {
    ($($arg:tt)*) => {  // tt = token tree (any token)
        {
            println!("[DEBUG] {}:{}", file!(), line!());
            println!($($arg)*);
        }
    };
}

// 3. For procedural macros, use log::debug!
#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
    log::debug!("Input: {:?}", input);
    // ...
}

Debugging Tips: Use cargo expand (third-party) for better macro inspection. Start simple and gradually add complexity.

7. Useful Macro Patterns

Common patterns solve repetitive problems in Rust code generation.

// Builder pattern macro
macro_rules! builder {
    ($struct_name:ident { $($field:ident: $ty:ty),* }) => {
        impl $struct_name {
            pub fn builder() -> ${struct_name}Builder {
                ${struct_name}Builder::new()
            }
        }
        
        struct ${struct_name}Builder {
            $($field: Option<$ty>),*
        }
        
        impl ${struct_name}Builder {
            fn new() -> Self {
                Self { $($field: None),* }
            }
            
            $(pub fn $field(&mut self, value: $ty) -> &mut Self {
                self.$field = Some(value);
                self
            })*
            
            pub fn build(&self) -> Result<$struct_name, String> {
                Ok($struct_name {
                    $($field: self.$field.clone().ok_or(
                        format!("{} must be set", stringify!($field))
                    ?),*
                })
            }
        }
    };
}

// Usage:
builder!(User {
    name: String,
    age: u32
});

Pattern Benefits: Automates repetitive builder pattern implementation. Generates type-safe builders with compile-time checked required fields.

0 Interaction
0 Views
Views
0 Likes
×
×
×
🍪 CookieConsent@Ptutorials:~

Welcome to Ptutorials

$ Allow cookies on this site ? (y/n)

top-home