Macros Rust: Guia Completo macro_rules! | Rust Brasil

Guia de macros em Rust: macro_rules!, proc macros, derive e attribute macros. Quando usar macros vs funções e generics.

Macros são uma das ferramentas mais poderosas do Rust. Elas permitem escrever código que gera código — metaprogramação em tempo de compilação. Você usa macros o tempo todo em Rust (println!, vec!, derive), mas entender como criá-las abre um mundo de possibilidades. Neste artigo, vamos explorar tanto as macros declarativas (macro_rules!) quanto as procedurais (proc macros), com exemplos práticos e orientações sobre quando usá-las.

Macros vs Funções vs Generics

Antes de mergulhar em macros, é importante entender quando usá-las:

┌────────────────┬──────────────────┬──────────────────┬─────────────────┐
│ Característica │ Funções          │ Generics         │ Macros          │
├────────────────┼──────────────────┼──────────────────┼─────────────────┤
│ Tipo-safe      │ Sim              │ Sim              │ Depende         │
│ Variadica      │ Não              │ Não              │ Sim             │
│ Gera código    │ Não              │ Monomorfização   │ Sim             │
│ Acessa AST     │ Não              │ Não              │ Sim (proc)      │
│ Debug fácil    │ Sim              │ Razoável         │ Difícil         │
│ Performance    │ Boa              │ Ótima            │ Ótima           │
└────────────────┴──────────────────┴──────────────────┴─────────────────┘

Use macros quando:
- Precisa aceitar número variável de argumentos
- Quer gerar código repetitivo automaticamente
- Precisa manipular sintaxe (DSLs)
- Funções e generics não são suficientes

Macros Declarativas com macro_rules!

Macros declarativas são definidas com macro_rules! e funcionam por correspondência de padrões na sintaxe:

Sintaxe Básica

macro_rules! diga {
    () => {
        println!("Olá!");
    };
    ($nome:expr) => {
        println!("Olá, {}!", $nome);
    };
}

fn main() {
    diga!();           // Olá!
    diga!("Rust");     // Olá, Rust!
}

Designadores de Fragmento

Macros usam designadores para capturar diferentes tipos de sintaxe:

macro_rules! demonstrar_designadores {
    // $x:expr — qualquer expressão
    (expr: $x:expr) => { println!("Expressão: {:?}", $x) };

    // $x:ident — um identificador
    (ident: $x:ident) => { let $x = 42; println!("{} = {}", stringify!($x), $x) };

    // $x:ty — um tipo
    (tipo: $x:ty) => { println!("Tipo: {}", stringify!($x)) };

    // $x:literal — um literal
    (lit: $x:literal) => { println!("Literal: {}", $x) };

    // $x:pat — um pattern
    (pat: $x:pat) => { let $x = (1, 2); };

    // $x:tt — uma token tree (qualquer token)
    (tt: $($x:tt)*) => { println!("{}", stringify!($($x)*)) };
}

fn main() {
    demonstrar_designadores!(expr: 2 + 3);
    demonstrar_designadores!(ident: minha_var);
    demonstrar_designadores!(tipo: Vec<String>);
    demonstrar_designadores!(lit: "texto");
    demonstrar_designadores!(tt: qualquer coisa aqui);
}

Repetição com $(...)*

O recurso mais poderoso de macro_rules! é a repetição:

/// Cria um HashMap com sintaxe literal
macro_rules! mapa {
    ($($chave:expr => $valor:expr),* $(,)?) => {{
        let mut m = std::collections::HashMap::new();
        $(
            m.insert($chave, $valor);
        )*
        m
    }};
}

/// Implementa Display para múltiplos tipos de uma vez
macro_rules! impl_display {
    ($($tipo:ty => $formato:expr),* $(,)?) => {
        $(
            impl std::fmt::Display for $tipo {
                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                    write!(f, $formato, self.0)
                }
            }
        )*
    };
}

struct Celsius(f64);
struct Fahrenheit(f64);
struct Kelvin(f64);

impl_display! {
    Celsius => "{:.1}°C",
    Fahrenheit => "{:.1}°F",
    Kelvin => "{:.1}K",
}

fn main() {
    let capitais = mapa! {
        "Brasil" => "Brasília",
        "Argentina" => "Buenos Aires",
        "Chile" => "Santiago",
    };

    for (pais, capital) in &capitais {
        println!("{}: {}", pais, capital);
    }

    println!("{}", Celsius(100.0));     // 100.0°C
    println!("{}", Fahrenheit(212.0));  // 212.0°F
    println!("{}", Kelvin(373.15));     // 373.2K
}

Macro Recursiva

Macros podem chamar a si mesmas para processamento recursivo:

/// Conta o número de argumentos passados
macro_rules! contar {
    () => { 0usize };
    ($primeiro:tt $($resto:tt)*) => { 1usize + contar!($($resto)*) };
}

/// Cria um vetor com tamanho verificado em tempo de compilação
macro_rules! vec_fixo {
    ($($elemento:expr),* $(,)?) => {{
        const TAMANHO: usize = contar!($($elemento)*);
        let array: [_; TAMANHO] = [$($elemento),*];
        array
    }};
}

fn main() {
    let nums = vec_fixo![1, 2, 3, 4, 5];
    println!("Array de {} elementos: {:?}", nums.len(), nums);

    println!("Contagem: {}", contar!(a b c d)); // 4
}

Exemplo Prático: DSL para Testes

macro_rules! teste_tabela {
    ($nome_fn:ident, $fn_testar:expr, [$(($entrada:expr, $esperado:expr)),* $(,)?]) => {
        #[cfg(test)]
        mod $nome_fn {
            use super::*;

            $(
                paste::paste! {
                    // Cada caso gera uma função de teste separada
                }
            )*

            #[test]
            fn testar_todos() {
                let casos = vec![
                    $(($entrada, $esperado)),*
                ];

                for (i, (entrada, esperado)) in casos.iter().enumerate() {
                    let resultado = ($fn_testar)(*entrada);
                    assert_eq!(
                        resultado, *esperado,
                        "Caso {}: entrada={:?}, esperado={:?}, obtido={:?}",
                        i, entrada, esperado, resultado
                    );
                }
            }
        }
    };
}

fn dobro(x: i32) -> i32 {
    x * 2
}

teste_tabela!(testes_dobro, dobro, [
    (0, 0),
    (1, 2),
    (5, 10),
    (-3, -6),
]);

fn main() {
    println!("Dobro de 7: {}", dobro(7));
}

Macros Procedurais

Macros procedurais são programas Rust que manipulam tokens em tempo de compilação. Existem três tipos:

1. Derive Macros

A forma mais comum de proc macro. Gera implementações automaticamente:

// Em um crate separado: meu_derive/src/lib.rs
// Cargo.toml precisa de: [lib] proc-macro = true

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

/// Gera um método `descrever()` que retorna uma descrição dos campos
#[proc_macro_derive(Descrever)]
pub fn derive_descrever(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let nome = &input.ident;

    let descricao = format!("Struct '{}' com campos definidos pelo usuário", nome);

    let expanded = quote! {
        impl #nome {
            pub fn descrever() -> &'static str {
                #descricao
            }
        }
    };

    TokenStream::from(expanded)
}

Uso da derive macro:

// No crate que usa a macro
// use meu_derive::Descrever;

// #[derive(Descrever)]
// struct Produto {
//     nome: String,
//     preco: f64,
// }

// fn main() {
//     println!("{}", Produto::descrever());
//     // Struct 'Produto' com campos definidos pelo usuário
// }

2. Attribute Macros

Macros de atributo transformam o item que anotam:

// Em um crate proc-macro
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

/// Mede o tempo de execução de uma função
#[proc_macro_attribute]
pub fn medir_tempo(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let func = parse_macro_input!(item as ItemFn);
    let nome = &func.sig.ident;
    let bloco = &func.block;
    let vis = &func.vis;
    let sig = &func.sig;

    let expanded = quote! {
        #vis #sig {
            let inicio = std::time::Instant::now();
            let resultado = (|| #bloco)();
            let duracao = inicio.elapsed();
            println!("[{}] executou em {:?}", stringify!(#nome), duracao);
            resultado
        }
    };

    TokenStream::from(expanded)
}

Uso:

// #[medir_tempo]
// fn calcular_fibonacci(n: u64) -> u64 {
//     if n <= 1 { return n; }
//     calcular_fibonacci(n - 1) + calcular_fibonacci(n - 2)
// }

3. Function-like Macros

Parecem chamadas de função, mas processam tokens arbitrários:

// Em um crate proc-macro
use proc_macro::TokenStream;

/// Cria SQL type-checked (exemplo simplificado)
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    let query = input.to_string();
    // Aqui você poderia validar a SQL em tempo de compilação
    let expanded = format!(
        r#"{{ static QUERY: &str = "{}"; QUERY }}"#,
        query.trim_matches('"')
    );
    expanded.parse().unwrap()
}

Macros Declarativas Avançadas

Macro com Múltiplas Regras (Overloading)

macro_rules! criar_struct {
    // Forma 1: struct simples com campos públicos
    (pub struct $nome:ident { $($campo:ident : $tipo:ty),* $(,)? }) => {
        #[derive(Debug, Clone)]
        pub struct $nome {
            $(pub $campo: $tipo),*
        }

        impl $nome {
            pub fn novo($($campo: $tipo),*) -> Self {
                Self { $($campo),* }
            }
        }
    };

    // Forma 2: struct com valores padrão
    (struct $nome:ident {
        $($campo:ident : $tipo:ty = $padrao:expr),* $(,)?
    }) => {
        #[derive(Debug, Clone)]
        pub struct $nome {
            $(pub $campo: $tipo),*
        }

        impl Default for $nome {
            fn default() -> Self {
                Self {
                    $($campo: $padrao),*
                }
            }
        }
    };
}

criar_struct! {
    pub struct Ponto {
        x: f64,
        y: f64,
    }
}

criar_struct! {
    struct Config {
        porta: u16 = 8080,
        host: String = String::from("localhost"),
        max_conexoes: usize = 100,
    }
}

fn main() {
    let ponto = Ponto::novo(3.0, 4.0);
    println!("{:?}", ponto);

    let config = Config::default();
    println!("{:?}", config);
}

Macro para Builder Pattern

macro_rules! builder {
    ($nome:ident {
        $($campo:ident : $tipo:ty),* $(,)?
    }) => {
        #[derive(Debug, Clone)]
        pub struct $nome {
            $($campo: $tipo),*
        }

        paste::paste! {
            pub struct [<$nome Builder>] {
                $($campo: Option<$tipo>),*
            }

            impl [<$nome Builder>] {
                pub fn novo() -> Self {
                    Self {
                        $($campo: None),*
                    }
                }

                $(
                    pub fn $campo(mut self, valor: $tipo) -> Self {
                        self.$campo = Some(valor);
                        self
                    }
                )*

                pub fn build(self) -> Result<$nome, String> {
                    Ok($nome {
                        $(
                            $campo: self.$campo.ok_or_else(||
                                format!("Campo '{}' não definido", stringify!($campo))
                            )?
                        ),*
                    })
                }
            }
        }
    };
}

// Note: este exemplo usa o crate `paste` para concatenar identificadores.
// Na prática, defina a struct e o builder manualmente ou use o crate `derive_builder`.

Depuração de Macros

Depurar macros pode ser desafiador. Aqui estão técnicas úteis:

1. cargo expand

Instale com cargo install cargo-expand e veja o código gerado:

# Expande todas as macros do projeto
cargo expand

# Expande apenas uma função específica
cargo expand main

2. stringify! para inspecionar tokens

macro_rules! debug_macro {
    ($($tokens:tt)*) => {
        println!("Tokens recebidos: {}", stringify!($($tokens)*));
        $($tokens)*
    };
}

fn main() {
    debug_macro!(let x = 42; println!("{}", x););
}

3. compile_error! para mensagens de erro

macro_rules! apenas_numeros {
    ($x:literal) => {
        {
            // Verifica se é numérico em tempo de compilação (limitado)
            const _: () = {
                let _ = 0i128 + $x; // falha se não for numérico
            };
            $x
        }
    };
    ($($outros:tt)*) => {
        compile_error!("Esta macro aceita apenas literais numéricos")
    };
}

fn main() {
    let n = apenas_numeros!(42);
    println!("{}", n);
    // let s = apenas_numeros!("texto"); // ERRO de compilação
}

Erros Comuns com Macros

1. Ordem das regras importa

macro_rules! exemplo {
    // ERRADO: esta regra captura TUDO antes que as específicas sejam testadas
    // ($($t:tt)*) => { ... };
    // ($x:expr) => { ... }; // nunca alcançada!

    // CORRETO: regras mais específicas primeiro
    ($x:expr) => { println!("Expressão: {}", $x) };
    ($($t:tt)*) => { println!("Tokens: {}", stringify!($($t)*)) };
}

fn main() {
    exemplo!(42);
}

2. Higiene de macros

Macros declarativas em Rust são higiênicas — variáveis definidas dentro da macro não conflitam com variáveis externas:

macro_rules! criar_var {
    () => {
        let x = 42; // este 'x' é diferente de qualquer 'x' externo
        println!("Dentro da macro: {}", x);
    };
}

fn main() {
    let x = 100;
    criar_var!();
    println!("Fora da macro: {}", x); // ainda é 100
}

Para mais detalhes sobre erros com macros, veja Invocação de Macro Incorreta.

Quando Usar Cada Tipo de Macro

Preciso de metaprogramação?
│
├─ Preciso de número variável de argumentos?
│  └─ macro_rules! (ex: vec!, println!)
│
├─ Preciso gerar impls automaticamente?
│  └─ Derive macro (ex: #[derive(Debug)])
│
├─ Preciso transformar uma função/struct?
│  └─ Attribute macro (ex: #[tokio::main])
│
├─ Preciso de uma DSL customizada?
│  └─ Function-like proc macro (ex: sql!(...))
│
└─ Nenhum dos acima?
   └─ Use funções ou generics — são mais simples

Aplicações no Mundo Real

Macros são fundamentais no ecossistema Rust:

  • serde: #[derive(Serialize, Deserialize)] gera código de serialização
  • tokio: #[tokio::main] transforma a função main para ser assíncrona
  • clap: #[derive(Parser)] gera parsers de argumentos CLI
  • sqlx: sqlx::query!() valida SQL em tempo de compilação
  • thiserror: #[derive(Error)] gera implementações de Error
// Exemplo real: macro para criar um enum de erros
macro_rules! definir_erros {
    ($(
        $variante:ident => $msg:expr
    ),* $(,)?) => {
        #[derive(Debug)]
        enum AppError {
            $($variante),*
        }

        impl std::fmt::Display for AppError {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                match self {
                    $(AppError::$variante => write!(f, $msg)),*
                }
            }
        }

        impl std::error::Error for AppError {}
    };
}

definir_erros! {
    NaoEncontrado => "Recurso não encontrado",
    NaoAutorizado => "Acesso não autorizado",
    ErroInterno => "Erro interno do servidor",
    Timeout => "Tempo limite excedido",
}

fn buscar_usuario(id: u32) -> Result<String, AppError> {
    match id {
        1 => Ok("Ana".into()),
        _ => Err(AppError::NaoEncontrado),
    }
}

fn main() {
    match buscar_usuario(99) {
        Ok(nome) => println!("Encontrado: {}", nome),
        Err(e) => println!("Erro: {}", e),
    }
}

Veja Também