Macros declarativas: macro_rules!

1. Introdução às Macros Declarativas

Macros em Rust são uma poderosa ferramenta de metaprogramação que permite gerar código em tempo de compilação. Diferente de funções comuns, macros operam sobre a árvore sintática abstrata (AST) do código, possibilitando transformações que vão além do que funções ou genéricos podem oferecer.

Existem dois tipos principais de macros em Rust: as declarativas (macro_rules!) e as procedurais. As macros declarativas funcionam através de casamento de padrões — você define um padrão de entrada e uma expansão que será gerada quando esse padrão for correspondido. As procedurais, por outro lado, são funções Rust que manipulam tokens diretamente e são mais complexas.

A sintaxe básica de macro_rules! é:

macro_rules! nome_da_macro {
    ($padrao) => {
        $expansao
    };
}

Cada braço da macro define um padrão que, quando correspondido, gera o código especificado na expansão.

2. Padrões e Capturas (Patterns e Metavariables)

O coração das macros declarativas são as metavariables. Elas permitem capturar partes do código de entrada e reutilizá-las na expansão. Cada metavariable é declarada com $nome:tipo, onde tipo especifica que tipo de token será capturado.

Os principais tipos de metavariables são:

  • $expr — captura uma expressão
  • $ty — captura um tipo
  • $ident — captura um identificador
  • $stmt — captura uma declaração
  • $pat — captura um padrão
  • $block — captura um bloco de código
  • $item — captura um item (fn, struct, impl)
  • $meta — captura um atributo
  • $lifetime — captura uma lifetime

Exemplo simples:

macro_rules! saudacao {
    ($nome:ident) => {
        println!("Olá, {}!", stringify!($nome));
    };
}

fn main() {
    saudacao!(Maria); // Imprime: Olá, Maria!
}

3. Repetição e Expansão com $()

A repetição é uma das características mais poderosas das macros. A sintaxe $($padrao)separador* permite capturar sequências de tokens repetidos.

  • $()* — zero ou mais repetições
  • $()+ — uma ou mais repetições
  • $()? — zero ou uma repetição

Vamos implementar uma versão simplificada da macro vec!:

macro_rules! meu_vec {
    ($($elemento:expr),*) => {
        {
            let mut v = Vec::new();
            $(
                v.push($elemento);
            )*
            v
        }
    };
}

fn main() {
    let numeros = meu_vec![1, 2, 3, 4, 5];
    println!("{:?}", numeros); // [1, 2, 3, 4, 5]
}

O separador , indica que os elementos são separados por vírgula. Poderíamos usar outros separadores, como ; ou espaço.

4. Macros Recursivas e Design Patterns

Macros declarativas podem ser recursivas, permitindo processar estruturas complexas. Um padrão comum é o TT muncher (Token Tree muncher), onde a macro consome tokens progressivamente.

Exemplo de uma macro json! simplificada:

macro_rules! json {
    (null) => {
        JsonValue::Null
    };
    ($valor:expr) => {
        JsonValue::Number($valor)
    };
    ({ $($chave:ident : $valor:expr),* $(,)? }) => {
        JsonValue::Object(vec![
            $(
                (stringify!($chave).to_string(), json!($valor)),
            )*
        ])
    };
}

enum JsonValue {
    Null,
    Number(i32),
    Object(Vec<(String, JsonValue)>),
}

Limitações: Rust impõe um limite de recursão (geralmente 128 níveis) para evitar loops infinitos. Macros muito complexas podem atingir esse limite.

5. Higiene de Macros e Escopo

Rust implementa higiene em macros, o que significa que os identificadores criados dentro da macro não entram em conflito com identificadores do código externo. Isso evita bugs sutis.

macro_rules! criar_variavel {
    () => {
        let x = 10;
    };
}

fn main() {
    let x = 5;
    criar_variavel!();
    println!("{}", x); // Imprime 5, não 10
}

Para referenciar itens do crate atual de forma segura, use $crate:

macro_rules! minha_macro {
    () => {
        $crate::minha_funcao()
    };
}

Em casos raros, você pode precisar escapar da higiene usando unsafe ou identificadores específicos, mas isso geralmente é desnecessário.

6. Tratamento de Erros e Debugging

Para gerar erros de compilação personalizados, use compile_error!:

macro_rules! assert_tamanho {
    ($v:expr, $tamanho:expr) => {
        if $v.len() != $tamanho {
            compile_error!("Tamanho do vetor não corresponde ao esperado");
        }
    };
}

Para depuração, stringify! e concat! são úteis:

macro_rules! debug_macro {
    ($expr:expr) => {
        eprintln!("Expressão: {}", stringify!($expr));
        eprintln!("Resultado: {:?}", $expr);
    };
}

Ferramentas essenciais:
- cargo expand — mostra o código gerado pela macro
- cargo rustc -- -Z trace-macros — rastreia a expansão passo a passo

Instale o expansor com: cargo install cargo-expand

7. Casos de Uso Avançados e Boas Práticas

Macros declarativas são excelentes para:

  • Geração de boilerplate: Implementar traits repetitivos
macro_rules! impl_display {
    ($tipo:ty, $campo:ident) => {
        impl std::fmt::Display for $tipo {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "{}", self.$campo)
            }
        }
    };
}

struct Pessoa {
    nome: String,
}

impl_display!(Pessoa, nome);
  • DSLs simples: Criar linguagens específicas de domínio
macro_rules! roteador {
    ($($metodo:ident => $caminho:expr => $handler:expr),*) => {
        $(
            if request.method() == stringify!($metodo) && request.path() == $caminho {
                $handler(request);
            }
        )*
    };
}

Boas práticas:

  1. Prefira funções genéricas quando possível — macros aumentam o tempo de compilação e o tamanho do binário
  2. Documente cada braço da macro com comentários claros
  3. Teste com #[test]:
#[test]
fn test_meu_vec() {
    let v = meu_vec![1, 2, 3];
    assert_eq!(v.len(), 3);
}
  1. Evite complexidade excessiva — macros muito aninhadas são difíceis de depurar
  2. Use nomes descritivos para as macros e suas metavariables

Macros declarativas são uma ferramenta poderosa, mas com grande poder vem grande responsabilidade. Use-as com moderação e sempre priorize soluções mais simples quando possível.

Referências