Macros procedurais: derive, attribute e function-like
1. Introdução às Macros Procedurais
Macros procedurais representam um dos recursos mais poderosos do sistema de metaprogramação de Rust. Diferentemente das macros declarativas (macro_rules!), que operam com pattern matching sobre tokens, as macros procedurais são funções Rust que recebem um TokenStream de entrada e produzem um TokenStream de saída, executando código arbitrário em tempo de compilação.
Existem três tipos de macros procedurais:
- Derive macros: implementam traits automaticamente para structs e enums
- Attribute macros: transformam ou envolvem itens como funções, structs e módulos
- Function-like macros: são invocadas como funções, mas operam em tokens
A escolha entre macros procedurais e outras abordagens depende da complexidade da transformação necessária. Para transformações simples baseadas em padrões, macro_rules! é suficiente. Para geração de código complexa que requer análise semântica ou parsing avançado, macros procedurais são a escolha ideal.
2. Estrutura Básica de uma Macro Procedural
Para criar uma macro procedural, é necessário definir um crate separado com a configuração proc-macro = true no Cargo.toml:
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
O ecossistema de macros procedurais depende de três crates fundamentais:
- syn: faz o parsing de TokenStream em estruturas Rust tipadas
- quote: gera TokenStream a partir de templates com interpolação
- proc_macro2: wrapper sobre proc_macro que permite testes e composição
3. Derive Macros: Automatizando Traits
Derive macros são o tipo mais comum de macro procedural. Elas implementam traits automaticamente para tipos definidos pelo usuário. Vamos criar um derive MyDebug simplificado:
// lib.rs no crate proc-macro
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_derive(MyDebug, attributes(my_debug))]
pub fn derive_my_debug(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => {
let field_names = fields.named.iter().map(|f| &f.ident);
quote! {
#(stringify!(#field_names): {:?}, )*
}
}
_ => quote! {},
},
_ => quote! {},
};
let expanded = quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {{ {} }}", stringify!(#name), #fields)
}
}
};
expanded.into()
}
Uso do derive:
#[derive(MyDebug)]
struct Pessoa {
nome: String,
idade: u32,
}
let p = Pessoa { nome: "Ana".into(), idade: 30 };
println!("{:?}", p); // Pessoa { nome: "Ana", idade: 30 }
Para adicionar atributos auxiliares como #[my_debug(ignore)], é necessário processar os atributos customizados manualmente:
#[proc_macro_derive(MyDebug, attributes(my_debug))]
pub fn derive_my_debug(input: TokenStream) -> TokenStream {
// Parsing e processamento dos atributos my_debug
// ...
}
4. Attribute Macros: Transformando Itens
Attribute macros permitem transformar ou envolver itens completos. Vamos implementar um macro route para simular um framework web:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, LitStr, Meta, Expr};
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse do atributo: path = "/users"
let attr_str = attr.to_string();
let path = attr_str
.split('=')
.nth(1)
.map(|s| s.trim().trim_matches('"'))
.unwrap_or("/");
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_block = &input_fn.block;
let fn_args = &input_fn.sig.inputs;
let expanded = quote! {
fn #fn_name(#fn_args) -> String {
println!("Rota acessada: {}", #path);
#fn_block
}
};
expanded.into()
}
Uso:
#[route(path = "/users")]
fn listar_usuarios() -> String {
"Lista de usuários".to_string()
}
5. Function-like Macros: Invocando como Funções
Function-like macros são invocadas com sintaxe de função, mas operam em tokens arbitrários. Vamos criar um validador SQL em tempo de compilação:
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let sql_str = input.to_string();
// Validação básica em tempo de compilação
if !sql_str.to_uppercase().starts_with("SELECT") {
return quote! {
compile_error!("SQL inválido: deve começar com SELECT");
}.into();
}
quote! {
{
let query = #sql_str;
println!("Executando: {}", query);
query
}
}.into()
}
Uso:
let query = sql!(SELECT * FROM users WHERE active = true);
// Em tempo de compilação: query = "SELECT * FROM users WHERE active = true"
6. Ferramentas do Ecossistema: syn, quote e proc_macro2
O parsing avançado com syn permite trabalhar com estruturas complexas:
use syn::parse::{Parse, ParseStream};
use syn::{Token, punctuated::Punctuated};
struct SqlQuery {
table: syn::Ident,
columns: Vec<syn::Ident>,
}
impl Parse for SqlQuery {
fn parse(input: ParseStream) -> syn::Result<Self> {
input.parse::<Token![SELECT]>()?;
let columns = Punctuated::<syn::Ident, Token![,]>::parse_terminated(input)?;
input.parse::<Token![FROM]>()?;
let table = input.parse()?;
Ok(SqlQuery {
table,
columns: columns.into_iter().collect(),
})
}
}
A macro quote! oferece interpolação poderosa:
let name = "exemplo";
let tokens = quote! {
fn #name() {
println!("Função {}", stringify!(#name));
}
};
Para tratamento de spans e mensagens de erro precisas:
use proc_macro2::Span;
use syn::Error;
let span = Span::call_site();
let error = Error::new(span, "Erro específico na macro");
return error.to_compile_error().into();
7. Boas Práticas e Armadilhas Comuns
Limitações fundamentais: Macros procedurais operam apenas no nível de tokens, não no AST completo. Isso significa que não têm acesso a informações de tipo ou resolução de namespaces.
Performance: Macros procedurais são executadas durante a compilação e podem impactar o tempo de compilação. Use cargo expand para depurar o código gerado:
cargo install cargo-expand
cargo expand
Debugging: Para debugar macros, use eprintln! para imprimir tokens durante o desenvolvimento:
#[proc_macro_derive(MeuTrait)]
pub fn derive_meu_trait(input: TokenStream) -> TokenStream {
eprintln!("Input: {}", input);
// ... resto da implementação
}
Armadilhas comuns:
- Esquecer de converter TokenStream com .into() no retorno
- Não tratar erros adequadamente com syn::Error
- Ignorar spans, resultando em mensagens de erro pouco informativas
8. Casos de Uso Avançados e Padrões
Combinando múltiplos tipos de macros:
// Um crate pode exportar derive + attribute + function-like
#[proc_macro_derive(MeuTrait)]
pub fn derive_meu_trait(input: TokenStream) -> TokenStream { /* ... */ }
#[proc_macro_attribute]
pub fn meu_atributo(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
#[proc_macro]
pub fn minha_funcao(input: TokenStream) -> TokenStream { /* ... */ }
Macros com configuração externa: Use variáveis de ambiente ou arquivos de configuração lidos em tempo de compilação:
#[proc_macro]
pub fn config(input: TokenStream) -> TokenStream {
let config_path = std::env::var("MEU_CONFIG").unwrap_or("config.toml".into());
let config = std::fs::read_to_string(&config_path).unwrap();
// Gera código baseado na configuração
}
Integração com FFI: Macros derive podem gerar bindings seguros para código C:
#[derive(UnsafeBindings)]
#[repr(C)]
struct FFIStruct {
x: i32,
y: f64,
}
As macros procedurais são uma ferramenta essencial no ecossistema Rust, permitindo desde a automação de traits comuns até a criação de DSLs completas. Dominá-las abre possibilidades enormes para redução de boilerplate e garantia de correção em tempo de compilação.
Referências
- The Rust Reference: Procedural Macros — Documentação oficial sobre a sintaxe e funcionamento de macros procedurais em Rust
- The Little Book of Rust Macros — Guia abrangente sobre todos os tipos de macros em Rust, incluindo exemplos detalhados de macros procedurais
- syn crate documentation — Documentação completa do crate syn para parsing de tokens Rust
- quote crate documentation — Documentação do crate quote para geração de código Rust com templates
- Rust by Example: Macros — Exemplos práticos de diferentes tipos de macros, incluindo procedurais
- Proc Macro Workshop — Repositório com exercícios práticos para aprender a implementar macros procedurais passo a passo
- Rust API Guidelines: Macros — Boas práticas recomendadas para projetar e implementar macros em Rust