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:
- Prefira funções genéricas quando possível — macros aumentam o tempo de compilação e o tamanho do binário
- Documente cada braço da macro com comentários claros
- Teste com
#[test]:
#[test]
fn test_meu_vec() {
let v = meu_vec![1, 2, 3];
assert_eq!(v.len(), 3);
}
- Evite complexidade excessiva — macros muito aninhadas são difíceis de depurar
- 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
- The Rust Reference: Macros By Example — Documentação oficial sobre a sintaxe completa de
macro_rules! - The Little Book of Rust Macros — Guia abrangente e aprofundado sobre macros em Rust, incluindo padrões avançados
- Rust by Example: Macros — Exemplos práticos e didáticos de macros declarativas
- Rust Cookbook: Macros — Receitas e padrões comuns para uso de macros
- Rust API Guidelines: Macros — Diretrizes oficiais para projetar macros bem comportadas e ergonômicas