Introdução
Macros são uma das ferramentas mais poderosas de Rust — elas permitem gerar código em tempo de compilação, eliminando boilerplate e criando abstrações impossíveis com funções normais. Se você já usou println!(), vec![], #[derive(Debug)] ou #[tokio::main], você já usou macros.
Em Rust, existem dois tipos principais: macros declarativas (macro_rules!) e macros procedurais (derive, attribute e function-like). Cada tipo tem seus casos de uso, vantagens e complexidades. Neste guia, vamos explorar ambos com exemplos práticos que você pode aplicar nos seus projetos.
Se você está começando com Rust, recomendamos primeiro nosso tutorial de primeiros passos e depois voltar a este artigo.
Macros Declarativas com macro_rules!
As macros declarativas usam pattern matching no código-fonte. Elas são definidas com macro_rules! e funcionam como um “match” sobre tokens de Rust. Para entender melhor pattern matching em Rust, confira nosso artigo sobre pattern matching avançado.
Exemplo Básico: vec! Simplificado
// Reimplementação simplificada do vec![]
macro_rules! meu_vec {
// Caso: lista de elementos — meu_vec![1, 2, 3]
( $( $elemento:expr ),* ) => {
{
let mut v = Vec::new();
$( v.push($elemento); )*
v
}
};
// Caso: repetição — meu_vec![0; 5] cria vec com 5 zeros
( $elemento:expr ; $contagem:expr ) => {
vec![$elemento; $contagem]
};
}
fn main() {
let numeros = meu_vec![1, 2, 3, 4, 5];
let zeros = meu_vec![0; 10];
println!("Números: {:?}", numeros); // [1, 2, 3, 4, 5]
println!("Zeros: {:?}", zeros); // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
Fragmentos de Macro (Designators)
Os designators definem que tipo de token a macro aceita:
| Designator | Aceita | Exemplo |
|---|---|---|
$x:expr | Expressão | 1 + 2, foo() |
$x:ident | Identificador | minha_var, MeuTipo |
$x:ty | Tipo | i32, Vec<String> |
$x:pat | Padrão | Some(x), _ |
$x:stmt | Declaração | let x = 5 |
$x:block | Bloco | { println!("oi"); } |
$x:tt | Token tree | Qualquer token |
$x:literal | Literal | 42, "texto" |
Exemplo Prático: Macro de HashMap
Uma macro que simplifica a criação de HashMaps — útil em qualquer projeto. Para mais sobre HashMaps, veja nosso guia da stdlib HashMap.
macro_rules! hashmap {
( $( $chave:expr => $valor:expr ),* $(,)? ) => {
{
let mut mapa = std::collections::HashMap::new();
$( mapa.insert($chave, $valor); )*
mapa
}
};
}
fn main() {
let config = hashmap! {
"host" => "localhost",
"porta" => "8080",
"debug" => "true",
};
for (chave, valor) in &config {
println!("{}: {}", chave, valor);
}
}
Macro Recursiva: DSL de SQL
Macros podem ser recursivas, permitindo criar mini-DSLs:
macro_rules! query {
// SELECT campos FROM tabela
(SELECT $( $campo:ident ),+ FROM $tabela:ident) => {
format!(
"SELECT {} FROM {}",
vec![ $( stringify!($campo) ),+ ].join(", "),
stringify!($tabela)
)
};
// SELECT campos FROM tabela WHERE condição
(SELECT $( $campo:ident ),+ FROM $tabela:ident WHERE $condicao:expr) => {
format!(
"SELECT {} FROM {} WHERE {}",
vec![ $( stringify!($campo) ),+ ].join(", "),
stringify!($tabela),
$condicao
)
};
}
fn main() {
let sql = query!(SELECT nome, email FROM usuarios);
println!("{}", sql);
// Output: SELECT nome, email FROM usuarios
let sql2 = query!(SELECT id, nome FROM produtos WHERE "preco > 100");
println!("{}", sql2);
// Output: SELECT id, nome FROM produtos WHERE preco > 100
}
Macros Procedurais
Macros procedurais são programas Rust que recebem código como entrada e geram código como saída. São mais poderosas que macro_rules! mas também mais complexas. Existem três tipos:
1. Derive Macros
As derive macros geram implementações automaticamente. Você já usa isso com #[derive(Debug, Clone, Serialize)]. Vamos criar uma derive macro que gera um método descricao():
// Em uma crate separada: minha_macro/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Descricao)]
pub fn derive_descricao(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let nome = &ast.ident;
let expandido = quote! {
impl #nome {
pub fn descricao() -> &'static str {
concat!("Struct: ", stringify!(#nome))
}
}
};
TokenStream::from(expandido)
}
// No código que usa a macro
use minha_macro::Descricao;
#[derive(Descricao, Debug)]
struct Usuario {
nome: String,
email: String,
}
fn main() {
println!("{}", Usuario::descricao());
// Output: Struct: Usuario
}
2. Attribute Macros
Attribute macros transformam o item anotado. O exemplo mais famoso é #[tokio::main]:
// Exemplo conceitual de como #[tokio::main] funciona
#[proc_macro_attribute]
pub fn meu_async_main(_attr: TokenStream, item: TokenStream) -> TokenStream {
let func = parse_macro_input!(item as syn::ItemFn);
let corpo = &func.block;
let expandido = quote! {
fn main() {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async #corpo)
}
};
TokenStream::from(expandido)
}
Para aprender mais sobre o runtime Tokio e async/await, confira nosso guia do ecossistema Tokio e o artigo sobre async Rust.
3. Function-like Macros
Parecem chamadas de função mas são macros procedurais:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let query = input.to_string();
// Validar SQL em tempo de compilação...
let expandido = quote! {
sqlx::query!(#query)
};
TokenStream::from(expandido)
}
// Uso:
// let resultado = sql!(SELECT * FROM usuarios WHERE id = $1);
Crates Essenciais: syn e quote
As crates syn e quote são fundamentais para macros procedurais:
- syn: Faz o parsing de código Rust em uma AST (Abstract Syntax Tree)
- quote: Gera código Rust a partir de templates com
quote!{}
# Cargo.toml da crate de macros
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
Exemplo Completo: Derive Builder
Um padrão muito útil é gerar builders automaticamente. Para entender o padrão Builder em Rust, veja nosso artigo sobre padrão Builder:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let nome = &ast.ident;
let builder_nome = syn::Ident::new(
&format!("{}Builder", nome),
nome.span(),
);
// Extrair campos da struct
let campos = match &ast.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => &fields.named,
_ => panic!("Builder só funciona com named fields"),
},
_ => panic!("Builder só funciona com structs"),
};
// Gerar campos do builder como Option<T>
let builder_campos = campos.iter().map(|f| {
let nome = &f.ident;
let tipo = &f.ty;
quote! { #nome: Option<#tipo> }
});
// Gerar setters
let setters = campos.iter().map(|f| {
let nome = &f.ident;
let tipo = &f.ty;
quote! {
pub fn #nome(mut self, val: #tipo) -> Self {
self.#nome = Some(val);
self
}
}
});
let expandido = quote! {
pub struct #builder_nome {
#( #builder_campos, )*
}
impl #nome {
pub fn builder() -> #builder_nome {
#builder_nome {
#( #(campos.iter().map(|f| &f.ident)): None, )*
}
}
}
impl #builder_nome {
#( #setters )*
}
};
TokenStream::from(expandido)
}
Quando Usar Macros vs Generics vs Traits
Uma dúvida comum é quando usar macros em vez de generics ou traits. Para um entendimento profundo de traits e generics, veja nosso tutorial de traits e generics.
| Use Macros Quando | Use Generics/Traits Quando |
|---|---|
| Precisa gerar código variável | O comportamento segue um padrão tipado |
| Quer criar uma DSL | A abstração é sobre tipos |
| Precisa de variadic arguments | Os parâmetros são fixos |
| Quer verificações em compile-time | Runtime dispatch é aceitável |
| Quer eliminar boilerplate repetitivo | A composição de traits resolve |
Casos Reais no Ecossistema
Macros são usadas extensivamente no ecossistema Rust. Alguns exemplos notáveis:
- serde:
#[derive(Serialize, Deserialize)]— veja nosso guia completo do Serde - thiserror:
#[derive(Error)]— como vimos no artigo sobre tratamento de erros - clap:
#[derive(Parser)]— para CLIs, veja nosso tutorial de CLI com Clap - tokio:
#[tokio::main]e#[tokio::test] - sqlx:
query!()com verificação SQL em compile-time
A crate Rayon também usa macros internamente para implementar traits de iteração paralela automaticamente.
Debugging de Macros
Depurar macros pode ser desafiador. Algumas ferramentas essenciais — que você pode integrar no seu fluxo de CI/CD:
# Expandir macros para ver o código gerado
cargo expand
# Expandir apenas um módulo específico
cargo expand nome_do_modulo
# Ver a expansão de uma macro específica
cargo expand --test nome_do_teste
O cargo expand requer instalação: cargo install cargo-expand. Para mais ferramentas do ecossistema Cargo, confira nosso artigo sobre Cargo e ferramentas essenciais.
// Outra técnica: compile_error! para debugging
macro_rules! debug_macro {
($($tokens:tt)*) => {
compile_error!(concat!(
"Tokens recebidos: ",
stringify!($($tokens)*)
));
};
}
Boas Práticas com Macros
Documente extensivamente — macros são mais difíceis de entender que funções. Use a convenção de documentação Rust.
Prefira funções e traits quando possível — macros devem ser o último recurso.
Use
$crate::para referenciar itens da sua crate dentro de macros exportadas.Teste com
trybuildpara macros procedurais — verifica erros de compilação esperados.Mantenha macros pequenas — extraia lógica complexa para funções helper.
// Boa prática: macro delega para função
macro_rules! log_evento {
($nivel:expr, $($arg:tt)*) => {
_log_evento_impl($nivel, &format!($($arg)*))
};
}
fn _log_evento_impl(nivel: &str, mensagem: &str) {
// Lógica complexa aqui — testável e debugável
println!("[{}] {}", nivel, mensagem);
}
Conclusão
Macros em Rust são uma ferramenta essencial que, quando usada com critério, pode transformar a qualidade do seu código. Use macro_rules! para padrões repetitivos e DSLs simples, e macros procedurais com syn/quote para geração de código sofisticada.
A regra de ouro é: se uma função ou trait resolve, use-os. Macros devem ser reservadas para casos onde a metaprogramação realmente agrega valor — eliminando boilerplate significativo ou criando APIs impossíveis de outra forma.
Para continuar aprendendo sobre técnicas avançadas, explore também Go Brasil para ver como Go lida com geração de código via go generate, e Zig Brasil para ver como comptime oferece uma abordagem diferente para metaprogramação.
Veja Também
- Closures em Rust — frequentemente usadas junto com macros
- Iteradores em Rust — macros que geram implementações de Iterator
- Smart Pointers — derive macros como
DerefeDerefMut - Top Projetos Open Source em Rust — muitos usam macros extensivamente