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çãotokio:#[tokio::main]transforma a função main para ser assíncronaclap:#[derive(Parser)]gera parsers de argumentos CLIsqlx:sqlx::query!()valida SQL em tempo de compilaçãothiserror:#[derive(Error)]gera implementações deError
// 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
- Pattern Matching Avançado em Rust — a base do
macro_rules!é pattern matching - Traits e Generics em Rust — alternativa a macros para muitos cenários
- Invocação de Macro Incorreta — erros comuns ao chamar macros
- Trait Objects vs Generics — generics frequentemente substituem macros
- Cheatsheet Rust — referência rápida da sintaxe de macros