Macros procedurais (introdução)
Esta aula introduz as macros procedurais em Rust, explicando os três tipos (derive, attribute e function-like), o crate proc_macro, as bibliotecas syn e quote, e cuidados importantes. Inclui exemplos práticos, referências e exercícios com respostas.
Macros procedurais são uma das ferramentas mais poderosas de metaprogramação em Rust. Diferentemente das macros declarativas (macro_rules!), que operam sobre padrões de tokens, as macros procedurais permitem que você escreva código Rust que recebe tokens de entrada e produz tokens de saída, possibilitando transformações complexas em tempo de compilação. Nesta aula, vamos explorar os fundamentos: os três tipos de macros proc, o crate proc_macro, as bibliotecas syn e quote, e os cuidados essenciais ao desenvolver suas próprias macros.
Se você já usou atributos como #[derive(Debug)] ou #[test], já viu macros procedurais em ação. Elas são amplamente utilizadas em crates populares como serde, tokio e diesel. Entender como elas funcionam abre portas para criar APIs elegantes e reduzir boilerplate.
Tipos de macros proc
Existem três tipos de macros procedurais em Rust, cada uma com um propósito específico:
- Macros derivadas (derive macros): são usadas com
#[derive(...)]para gerar implementações automáticas de traits. Por exemplo,#[derive(Clone)]gera a implementação deClonepara o tipo. Você pode criar suas próprias macros derive, como#[derive(MyTrait)]. - Macros de atributo (attribute macros): são usadas como atributos em itens (funções, structs, módulos, etc.) e podem modificar o item ou gerar código adicional. Por exemplo,
#[test]é uma macro de atributo que transforma uma função em um teste. - Macros do tipo função (function-like macros): são invocadas como funções, mas com sintaxe especial, como
my_macro!(...). Elas podem receber qualquer sequência de tokens e retornar qualquer código. Um exemplo éprintln!, embora este seja declarativo; function-like procedurais oferecem mais flexibilidade.
Para criar uma macro procedural, você precisa de um crate separado (ou um módulo com proc-macro crate type) que exporta funções marcadas com atributos específicos. Cada tipo tem um atributo correspondente: #[proc_macro_derive], #[proc_macro_attribute] e #[proc_macro].
Exemplo de declaração de uma macro derive simples:
use proc_macro::TokenStream;
#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
// implementação
input
}
proc_macro
O crate proc_macro é a base de todas as macros procedurais. Ele fornece o tipo TokenStream, que é uma sequência de tokens (identificadores, literais, pontuação, etc.) representando o código Rust. A função da macro recebe um TokenStream e retorna outro TokenStream.
Para usar macros procedurais, você deve configurar seu crate com o atributo proc-macro = true no Cargo.toml:
[lib]
proc-macro = true
Isso faz com que o crate seja compilado como uma biblioteca especial que só pode exportar macros procedurais. Você também pode usar proc_macro diretamente, mas é mais comum usar bibliotecas auxiliares como syn e quote para simplificar o processamento.
O tipo TokenStream pode ser convertido para strings e vice-versa, mas a manipulação direta é trabalhosa. Por isso, usamos syn para analisar a entrada em estruturas de dados tipadas e quote para gerar código de saída de forma ergonômica.
Exemplo de função que apenas repassa a entrada:
use proc_macro::TokenStream;
#[proc_macro]
pub fn passthrough(input: TokenStream) -> TokenStream {
input
}
syn e quote (visão geral)
syn é uma biblioteca para analisar (parse) tokens Rust em uma árvore sintática abstrata (AST). Ela fornece tipos como DeriveInput, ItemFn, Attribute, etc., que representam construções da linguagem. Com syn, você pode extrair informações como nomes de structs, campos, visibilidade, etc., de forma segura e tipada.
quote é uma biblioteca para gerar código Rust a partir de templates. A macro quote! permite escrever código quase como uma string, mas com interpolação de variáveis. Por exemplo, quote! { #ident } insere o valor da variável ident no código gerado. Isso torna a geração de código muito mais legível e menos propensa a erros.
Juntas, syn e quote formam o par padrão para desenvolvimento de macros procedurais. Você adiciona as dependências no Cargo.toml:
[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
Exemplo de uso de syn para analisar uma struct:
use syn::{parse_macro_input, DeriveInput};
use proc_macro::TokenStream;
#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
// ...
TokenStream::new()
}
Cuidados
Ao escrever macros procedurais, alguns cuidados são essenciais:
- Erros de compilação: Use
compile_error!ousyn::Errorpara reportar erros claros ao usuário. Se sua macro falhar, o compilador mostrará a mensagem na linha onde a macro é usada. - Higiene: Macros procedurais não são higiênicas por padrão, ou seja, podem introduzir nomes que colidem com itens do usuário. Use identificadores com prefixos ou use
quote::format_ident!para gerar nomes únicos. - Performance: Macros são executadas em tempo de compilação, então código ineficiente pode aumentar o tempo de compilação. Evite loops desnecessários e parse complexos.
- Compatibilidade: Lembre-se de que macros procedurais só podem ser usadas em crates que dependem do seu crate. Elas não podem ser exportadas de um crate binário.
- Documentação: Documente bem suas macros, pois elas são uma API para os usuários. Inclua exemplos e avisos sobre limitações.
Exemplo de geração de código com quote e tratamento de erros:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
if !matches!(ast.data, syn::Data::Struct(_)) {
return syn::Error::new_spanned(&ast, "MyTrait só pode ser derivado em structs")
.to_compile_error()
.into();
}
let expanded = quote! {
impl MyTrait for #name {
fn hello(&self) {
println!("Hello from {}", stringify!(#name));
}
}
};
expanded.into()
}
Boas práticas
Ao desenvolver macros procedurais, siga estas boas práticas:
- Use
synequoteem vez de manipularTokenStreamdiretamente, para evitar erros. - Valide a entrada o mais cedo possível e retorne erros claros.
- Teste suas macros com exemplos reais usando o crate
trybuildpara testes de compilação. - Mantenha a compatibilidade com versões anteriores se você publicar um crate.
- Considere o impacto no tempo de compilação; macros complexas podem tornar a compilação lenta.
Referências
- Referência de Macros Procedurais (Rust Reference)
- Documentação do crate proc_macro
- Documentação do syn
- Documentação do quote
- Capítulo 19 do Livro de Rust sobre Macros
- Proc Macro Workshop (exercícios)
Exercícios
-
Crie uma macro derive chamada
Helloque implementa um traitHellocom um métodohello()que imprime "Hello, <nome do tipo>!". A macro deve funcionar apenas em structs.✓ Resposta:use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput, Data}; #[proc_macro_derive(Hello)] pub fn hello_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident; if !matches!(ast.data, Data::Struct(_)) { return syn::Error::new_spanned(&ast, "Hello só pode ser derivado em structs") .to_compile_error() .into(); } let gen = quote! { impl Hello for #name { fn hello(&self) { println!("Hello, {}!", stringify!(#name)); } } }; gen.into() } -
Escreva uma macro de atributo chamada
logque, aplicada a uma função, imprime uma mensagem de log antes e depois da execução da função.✓ Resposta:use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, ItemFn, AttributeArgs}; #[proc_macro_attribute] pub fn log(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as ItemFn); let fn_name = &input.sig.ident; let block = &input.block; let gen = quote! { fn #fn_name() { println!("Iniciando {}...", stringify!(#fn_name)); #block println!("Finalizando {}.", stringify!(#fn_name)); } }; gen.into() } -
Implemente uma macro function-like chamada
make_answerque retorna uma funçãoanswer()que retorna o inteiro 42.✓ Resposta:use proc_macro::TokenStream; use quote::quote; #[proc_macro] pub fn make_answer(_input: TokenStream) -> TokenStream { quote! { fn answer() -> i32 { 42 } }.into() } -
Explique a diferença entre
TokenStreameproc_macro2::TokenStreame por quesynequoteusam o tipo do crateproc-macro2.✓ Resposta:proc_macro::TokenStreamé o tipo nativo do compilador, mas não pode ser usado fora de um contexto de macro (por exemplo, em bibliotecas que são usadas por macros).proc_macro2::TokenStreamé uma reimplementação que funciona em qualquer contexto, facilitando testes e reutilização.synequoteusamproc_macro2para que possam ser usados em código não-macro, e a conversão paraproc_macro::TokenStreamé feita no final via.into(). -
Escreva uma macro derive que adicione um método
to_json()que retorna uma string JSON simples com os campos da struct. Considere apenas structs com campos nomeados e use a bibliotecaserde_jsonpara gerar o JSON (não é necessário implementar a serialização, apenas gerar uma representação).✓ Resposta:use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput, Data, Fields}; #[proc_macro_derive(ToJson)] pub fn to_json_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident; let fields = match &ast.data { Data::Struct(data) => match &data.fields { Fields::Named(fields) => &fields.named, _ => return syn::Error::new_spanned(&ast, "ToJson requer campos nomeados").to_compile_error().into(), }, _ => return syn::Error::new_spanned(&ast, "ToJson só pode ser usado em structs").to_compile_error().into(), }; let field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect(); let gen = quote! { impl ToJson for #name { fn to_json(&self) -> String { let mut map = serde_json::Map::new(); #(map.insert(stringify!(#field_names).to_string(), serde_json::to_value(&self.#field_names).unwrap());)* serde_json::Value::Object(map).to_string() } } }; gen.into() }