Por @zejuniortdr em Mai 12, 2026
Metaprogramação é a arte de escrever código que gera ou manipula outro código. Se você vem do Python, provavelmente já usou decoradores, metaclasses ou funções de geração de código para automatizar tarefas repetitivas. O Rust também oferece poderosas ferramentas de metaprogramação, mas com uma abordagem fundamentalmente diferente e mais segura: as macros.
Enquanto Python permite modificar o código em tempo de execução, Rust opera em tempo de compilação através de seu poderoso sistema de macros. Isso oferece vantagens significativas: verificação de tipos antecipada, maior segurança e melhor performance, já que todo o código é expandido antes do programa rodar.
Neste artigo, vamos explorar como as macros do Rust vão além do que Python permite, oferecendo um controle granular sobre a geração de código com segurança em tempo de compilação.
O Que São Macros e Por Que São Diferentes de Python
Em Python, a metaprogramação acontece principalmente em tempo de execução. Decoradores modificam funções, metaclasses controlam a criação de classes, e funções como eval() permitem executar strings como código. Isso é flexível, mas também pode levar a erros que só aparecem quando o programa está rodando.
Rust, por outro lado, realiza toda metaprogramação em tempo de compilação através de macros. Existem dois tipos principais:
- Macros declarativas (com
macro_rules!): Mais simples, funcionam como correspondência de padrões - Macros procedurais: Mais poderosas, permitem manipulação arbitrária de tokens
A principal diferença é que as macros Rust são higiênicas (não causam efeitos colaterais inesperados) e seguras (o compilador verifica todo o código gerado).
Macros Declarativas: Criando Seu Próprio print! 🛠️
Vamos começar com macros declarativas, que são mais fáceis de entender para iniciantes. Elas funcionam através do macro_rules! e permitem criar syntax extensions usando regras de correspondência de padrões.
Um Exemplo Básico: meu_println!
Em Python, estamos acostumados com print() sendo uma função simples. Em Rust, println! é na verdade uma macro! Vamos criar nossa própria versão simplificada:
// Define uma macro chamada meu_println macro_rules! meu_println { // Primeira regra: sem argumentos (apenas println vazio) () => { println!() // Chama a macro println! existente }; // Segunda regra: com um ou mais argumentos // $($arg:tt)* captura qualquer token válido em Rust (tt = token tree), // tornando a macro flexível para diferentes padrões de entrada ($($arg:tt)*) => { println!($($arg)*) // Repassa os argumentos para println! }; } fn main() { meu_println!(); // Imprime uma linha em branco meu_println!("Olá, {}!", "mundo"); // Imprime: Olá, mundo! meu_println!("Valores: {}, {}, {}", 1, 2, 3); // Múltiplos argumentos // ⚠️ Erro comum: esquecer o ! na chamada // meu_println("teste"); // Erro: função `meu_println` não encontrada // meu_println!("teste"); // ✅ Correto } O que está acontecendo aqui:
-
macro_rules!inicia a definição da macro -
()define uma regra para quando a macro é chamada sem argumentos -
$($arg:tt)*captura todos os tokens passados para a macro —tt(token tree) aceita qualquer token válido Rust, o que torna a macro extremamente flexível -
$($arg)*expande os tokens capturados na chamada paraprintln!
Comparação com Python
Em Python, poderíamos criar algo similar com uma função:
def meu_print(*args, **kwargs): if not args and not kwargs: print() else: print(*args, **kwargs) # Exemplo de uso meu_print() meu_print("Olá, {}!".format("mundo")) meu_print("Valores:", 1, 2, 3) A diferença crucial é que a versão Python roda em tempo de execução, enquanto a macro Rust é expandida durante a compilação. O código Rust resultante não tem overhead de chamada de função extra.
Um Exemplo Mais Útil: vetor!
Vamos criar uma macro mais útil que simplifica a criação de vetores:
macro_rules! vetor { // Caso vazio: cria um Vec vazio () => { Vec::new() }; // Caso com elementos: $x:expr captura uma expressão ($($x:expr),*) => { { let mut temp_vec = Vec::new(); $(temp_vec.push($x);)* // Expande para push() para cada expressão temp_vec } }; // Caso com repetição: usa std::iter::repeat para criar múltiplos elementos iguais. // Funciona para tipos Copy e Clone sem necessidade de .clone() explícito. ($x:expr; $n:expr) => { std::iter::repeat($x).take($n).collect::<Vec<_>>() }; } fn main() { let v1 = vetor!(); // Vec vazio let v2 = vetor!(1, 2, 3); // Vec com valores 1, 2, 3 let v3 = vetor!("a"; 5); // Vec com 5 strings "a" println!("{:?}", v1); // [] println!("{:?}", v2); // [1, 2, 3] println!("{:?}", v3); // ["a", "a", "a", "a", "a"] } Nota técnica: A versão anterior usava
.clone()explícito dentro de um loop, o que é desnecessário para tiposCopy(como inteiros) e verboso para os demais. Usarstd::iter::repeaté idiomático e deixa o compilador decidir a estratégia de cópia mais eficiente.
Em Python, teríamos que usar uma função normal:
def criar_vetor(*args): if len(args) == 0: return [] elif len(args) == 2 and isinstance(args[1], int): return [args[0]] * args[1] else: return list(args) # Exemplo de uso v1 = criar_vetor() v2 = criar_vetor(1, 2, 3) v3 = criar_vetor("a", 5) print(v1) # [] print(v2) # [1, 2, 3] print(v3) # ["a", "a", "a", "a", "a"] A versão Rust é mais poderosa porque:
- Verifica tipos em tempo de compilação
- Não tem overhead de runtime (a macro é expandida antes da execução)
- Oferece syntax mais limpa com múltiplos padrões
Macros Procedurais: O Poder Real da Metaprogramação 💪
⚠️ Atenção: Macros procedurais são um tópico avançado e requerem familiaridade com parsing de código Rust. Se você ainda está se familiarizando com a linguagem, comece com macros declarativas e volte aqui quando se sentir confortável com os fundamentos!
As macros procedurais são muito mais poderosas que as declarativas. Elas permitem manipular arbitrariamente o código fonte usando a API de tokens do Rust. Existem três tipos:
- Macros de Derive: Automatizam implementação de traits
- Macros de Atributo: Aplicam transformações a itens
- Macros Function-like: Parecidas com as declarativas, mas com poder total sobre os tokens
Configuração para Macros Procedurais
Para criar macros procedurais, precisamos de uma crate especial. Vamos configurar:
Cargo.toml:
[package] name = "meu_projeto" version = "0.1.0" edition = "2021" [lib] # Esta linha é crucial: define que esta crate pode exportar macros procedurais. # Macros procedurais requerem esta configuração especial porque o compilador # precisa carregar e executar sua crate durante a própria compilação do projeto. proc-macro = true [dependencies] syn = { version = "2.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" Macro de Derive: MinhaSerde
Vamos criar uma macro de derive simples que automaticamente gera serialização básica, similar ao que o Serde faz:
// main.rs use minha_serde::MinhaSerde; #[derive(MinhaSerde)] struct Pessoa { nome: String, idade: u32, ativo: bool, } fn main() { let pessoa = Pessoa { nome: "João".to_string(), idade: 30, ativo: true, }; let serializado = pessoa.serializar(); println!("{}", serializado); // {"nome": "João", "idade": 30, "ativo": true} } // lib.rs da crate minha_serde use proc_macro::TokenStream; use proc_macro2::Span; use quote::quote; use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(MinhaSerde)] pub fn minha_serde_derive(input: TokenStream) -> TokenStream { // Parse do input para AST let input = parse_macro_input!(input as DeriveInput); let nome = input.ident; // Extrai os campos da struct let campos = if let syn::Data::Struct(syn::DataStruct { fields: syn::Fields::Named(ref campos_nomeados), .. }) = input.data { &campos_nomeados.named } else { // ✅ Retorna um erro de compilação em vez de um panic em runtime return syn::Error::new( Span::call_site(), "MinhaSerde só funciona com structs com campos nomeados!" ).to_compile_error().into(); }; // Gera os nomes dos campos como strings e os identificadores para acesso let nomes_campos: Vec<String> = campos .iter() .map(|campo| campo.ident.as_ref().unwrap().to_string()) .collect(); let idents_campos: Vec<_> = campos .iter() .map(|campo| campo.ident.as_ref().unwrap()) .collect(); // Gera a implementação do trait. // Nota: usamos `{}` (Display) em vez de `{:?}` (Debug) para serialização. // Isso exige que cada campo implemente o trait `Display`. // Para um serializador real, use a crate `serde` com suporte a tipos arbitrários. let expandido = quote! { impl #nome { pub fn serializar(&self) -> String { let mut resultado = String::from("{"); #( resultado.push_str(&format!("\"{}\": {}, ", #nomes_campos, self.#idents_campos)); )* resultado.pop(); // Remove última vírgula resultado.pop(); // Remove último espaço resultado.push('}'); resultado } } }; TokenStream::from(expandido) } Nota técnica: Usamos
syn::Error::new(...).to_compile_error()em vez depanic!(). Isso garante que o erro apareça como um erro de compilação limpo e rastreável, em vez de um crash do compilador — que é o comportamento esperado em macros procedurais de produção.
Este exemplo mostra o poder real das macros procedurais: analisamos a estrutura da struct em tempo de compilação e geramos código específico baseado em seus campos.
Comparação com Python
Em Python, usaríamos decoradores de classe ou metaclasses para funcionalidade similar:
def minha_serde(cls): def serializar(self): campos = [] for nome, valor in self.__dict__.items(): campos.append(f'"{nome}": {repr(valor)}') return "{" + ", ".join(campos) + "}" cls.serializar = serializar return cls @minha_serde class Pessoa: def __init__(self, nome, idade, ativo): self.nome = nome self.idade = idade self.ativo = ativo # Exemplo de uso pessoa = Pessoa("João", 30, True) print(pessoa.serializar()) # {"nome": "João", "idade": 30, "ativo": True} As diferenças são fundamentais:
- A versão Rust verifica tipos em tempo de compilação
- O código Rust gerado é otimizado especificamente para cada struct
- Não há overhead de runtime para análise reflexiva
- Erros são detectados durante a compilação, não durante a execução
Macro Function-like Procedural
Além das macros de derive e atributo, existe o terceiro tipo: macros function-like procedurais. Elas têm a mesma sintaxe de chamada das declarativas (nome!(...)), mas com poder total sobre os tokens de entrada:
// lib.rs use proc_macro::TokenStream; #[proc_macro] pub fn meu_sql(input: TokenStream) -> TokenStream { // Aqui você pode validar SQL em tempo de compilação, // gerar structs de resultado, etc. let sql = input.to_string(); quote::quote! { compile_error!(concat!("SQL processado em compile-time: ", #sql)) }.into() } // Uso meu_sql!(SELECT * FROM usuarios WHERE ativo = true); Esse padrão é usado por crates como sqlx para validar queries SQL contra o banco de dados em tempo de compilação — algo simplesmente impossível em Python.
Macros de Atributo: Personalizando Comportamento
Macros de atributo permitem criar atributos personalizados que modificam itens. Vamos criar um atributo medir_tempo que automaticamente mede o tempo de execução de funções:
// main.rs use medir_tempo::medir_tempo; #[medir_tempo] fn funcao_lenta() { std::thread::sleep(std::time::Duration::from_secs(1)); println!("Função executada!"); } fn main() { funcao_lenta(); // Imprime: "funcao_lenta levou 1.003s" } // lib.rs da crate medir_tempo use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, ItemFn}; #[proc_macro_attribute] pub fn medir_tempo(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as ItemFn); let nome_funcao = &input.sig.ident; let blocos = &input.block; let visibilidade = &input.vis; let assinatura = &input.sig; let expandido = quote! { #visibilidade #assinatura { let inicio = std::time::Instant::now(); let resultado = (|| #blocos)(); let duracao = inicio.elapsed(); println!("{} levou {:.3}s", stringify!(#nome_funcao), duracao.as_secs_f64()); resultado } }; TokenStream::from(expandido) } Comparação com Python
Em Python, usaríamos um decorador normal:
import time def medir_tempo(funcao): def wrapper(*args, **kwargs): inicio = time.time() resultado = funcao(*args, **kwargs) duracao = time.time() - inicio print(f"{funcao.__name__} levou {duracao:.3f}s") return resultado return wrapper @medir_tempo def funcao_lenta(): time.sleep(1) print("Função executada!") funcao_lenta() # "funcao_lenta levou 1.003s" Novamente, a diferença fundamental está em quando a transformação acontece:
- Python: Em tempo de execução, com overhead de chamadas de função
- Rust: Em tempo de compilação, sem overhead de runtime
Debugando Macros com cargo-expand
Uma das partes mais desafiadoras de trabalhar com macros é entender o que elas realmente geram. A ferramenta cargo-expand resolve exatamente isso, mostrando o código final após todas as expansões:
# Instalação (necessária apenas uma vez) cargo install cargo-expand # Ver o código expandido do seu projeto cargo expand # Ver apenas um módulo específico cargo expand meu_modulo Por exemplo, ao rodar cargo expand em um projeto que usa #[derive(MinhaSerde)], você verá exatamente o código impl Pessoa { pub fn serializar(...) } gerado pela macro — o que é invaluável para debugging e aprendizado.
Quando Não Usar Macros
Antes de sair criando macros para tudo, é importante saber quando não usá-las:
- Dificultam o debugging: Erros em código gerado por macros podem ter mensagens confusas e stack traces difíceis de rastrear
- Reduzem a legibilidade: Um código com muitas macros pode ser difícil de entender para quem não as escreveu
- São complexas de manter: Macros procedurais especialmente exigem conhecimento de
syn,quotee a API de tokens do Rust - IDEs têm suporte limitado: Autocomplete e navegação de código funcionam melhor com código direto
Regra de ouro: Se uma função, trait ou genérico resolve o problema, prefira essas opções. Reserve macros para quando a sintaxe ou a geração de código em tempo de compilação for realmente necessária.
Erros Comuns de Pythonistas em Rust
- Esquecer o
!na chamada: Macros parecem funções, mas a exclamação é obrigatória.
meu_println("teste"); // ❌ Erro: função não encontrada meu_println!("teste"); // ✅ Correto Tentar usar macros como funções: Têm regras diferentes — não aceitam tipos genéricos da mesma forma.
Esquecer da higiene: Macros Rust são higiênicas (variáveis não vazam para o escopo externo), ao contrário de algumas técnicas de metaprogramação Python.
Subestimar a complexidade: Macros procedurais são mais complexas que decoradores Python. Use
cargo expandpara entender o que está sendo gerado.Usar
panic!em macros procedurais: Prefirasyn::Error::new(...).to_compile_error()para produzir erros de compilação adequados.
Quando Usar Macros no Rust
Use macros quando:
- Você precisa de boilerplate repetitivo que não pode ser abstraído com funções ou traits
- Quer criar APIs com syntax personalizada (como
vec![]ouprintln!) - Precisa de código gerado em tempo de compilação para performance
- Está implementando traits comuns para muitos tipos automaticamente
O Que Aprendemos
- Macros declarativas (
macro_rules!) permitem criação de syntax extensions através de regras de pattern matching - Macros procedurais oferecem poder total sobre manipulação de código em três variedades: derive, attribute e function-like
- Macros de derive automatizam implementação de traits com base na análise de estruturas em tempo de compilação
- Macros de atributo modificam itens anotados com comportamentos personalizados
- Macros function-like procedurais permitem processamento arbitrário de tokens, como validação de SQL em compile-time
-
cargo expandé a ferramenta essencial para debugar e entender o código gerado pelas macros - O processo de compilação do Rust expande macros antes da geração de código, oferecendo segurança e performance
- Comparação com Python: Enquanto Python usa runtime metaprogramming (decoradores, metaclasses), Rust faz compile-time metaprogramming com verificação de tipos — e isso faz toda a diferença
As macros são uma das características mais poderosas do Rust, permitindo criar código conciso, seguro e eficiente que seria impossível ou inseguro em muitas outras linguagens. Use-as com sabedoria, sempre preferindo a solução mais simples quando ela resolver o problema.
Este artigo é um material de apoio do livro Desbravando Rust, que explora em profundidade todos esses conceitos e muitos outros. Se você quer dominar Rust vindo de background Python, compre agora em desbravandorust.com.br e acelere sua jornada Rust!