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