Macros Procedurais — Criando Derive Macros e Atributos Customizados

Macros Procedurais — Criando Derive Macros e Atributos Customizados

Macro procedural roda durante a compilação de quem a usa, e por isso mora em crate própria. O texto percorre os três tipos, o trio syn, quote e proc_macro2, como produzir mensagem de erro que aponta a linha certa, e como testar o que só existe em tempo de compilação.
Rust

19 min de leitura

 

No artigo Macros — Escrevendo Código que Escreve Código exploramos macros declarativas com macro_rules! — padrões que transformam código em código. Macros procedurais são o próximo nível: programas Rust que recebem tokens como entrada e produzem tokens como saída, executando em tempo de compilação.

Quando você escreve #[derive(Serialize)] ou #[tokio::main], está usando macros procedurais. Elas permitem eliminar boilerplate que seria impossível com macro_rules!, gerar código com base na estrutura de tipos, e criar DSLs (Domain Specific Languages) que parecem parte natural da linguagem.

Os três tipos de macros procedurais

// 1. Derive macros — adicionam implementações a structs/enums
#[derive(MinhaDerive)]
struct MeuTipo { ... }

// 2. Attribute macros — transformam o item anotado
#[meu_atributo(opcao = "valor")]
fn minha_funcao() { ... }

// 3. Function-like macros — chamadas como funções mas com poder de macro
minha_macro!(conteudo arbitrario aqui)

Estrutura de um crate de macros procedurais

Macros procedurais precisam ficar em um crate separado com proc-macro = true:

meu_projeto/
├── Cargo.toml
├── src/
│   └── main.rs
└── meu_derive/           ← crate de macros procedurais
    ├── Cargo.toml
    └── src/
        └── lib.rs

meu_derive/Cargo.toml:

[package]
name = "meu_derive"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true  # obrigatório

[dependencies]
proc-macro2 = "1"  # versão melhorada de proc_macro
quote = "1"        # geração de código TokenStream
syn = { version = "2", features = ["full"] }  # parsing de AST Rust

Cargo.toml do projeto principal:

[dependencies]
meu_derive = { path = "./meu_derive" }

As ferramentas essenciais

Três crates formam o ecossistema de macros procedurais:

syn — parseia código Rust em uma AST (Abstract Syntax Tree). Transforma TokenStream em estruturas como DeriveInput, ItemFn, Type, que você pode inspecionar e transformar.

quote — gera código Rust a partir de templates. O macro quote! { ... } interpola variáveis Rust em código Rust, retornando um TokenStream.

proc-macro2 — versão melhorada do proc_macro da stdlib, com suporte a testes unitários e melhor ergonomia.

Derive macro: Descrever

Vamos criar uma derive macro que implementa automaticamente um método descrever() para qualquer struct:

meu_derive/src/lib.rs:

use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields, FieldsNamed};

#[proc_macro_derive(Descrever)]
pub fn derive_descrever(input: TokenStream) -> TokenStream {
    // Parseia o input como DeriveInput (struct/enum/union)
    let input = parse_macro_input!(input as DeriveInput);

    // Gera o código ou retorna erro
    match impl_descrever(&input) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn impl_descrever(input: &DeriveInput) -> syn::Result<TokenStream2> {
    let nome = &input.ident;

    // Obtém os campos da struct
    let campos = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(campos) => campos,
            _ => return Err(syn::Error::new_spanned(
                nome,
                "Descrever suporta apenas structs com campos nomeados"
            )),
        },
        _ => return Err(syn::Error::new_spanned(
            nome,
            "Descrever suporta apenas structs"
        )),
    };

    // Gera linhas de descrição para cada campo
    let linhas_campos = campos.named.iter().map(|campo| {
        let nome_campo = campo.ident.as_ref().unwrap();
        let nome_str = nome_campo.to_string();
        quote! {
            // Para cada campo, chama format! com seu valor
            partes.push(format!("  {}: {:?}", #nome_str, self.#nome_campo));
        }
    });

    let nome_str = nome.to_string();

    // Gera a implementação completa
    let codigo_gerado = quote! {
        impl #nome {
            pub fn descrever(&self) -> String {
                let mut partes = Vec::new();
                partes.push(format!("{}:", #nome_str));
                #(#linhas_campos)*
                partes.join("\n")
            }
        }
    };

    Ok(codigo_gerado)
}

Uso:

use meu_derive::Descrever;

#[derive(Descrever, Debug)]
struct Usuario {
    nome: String,
    email: String,
    idade: u32,
    ativo: bool,
}

#[derive(Descrever, Debug)]
struct Produto {
    id: u64,
    nome: String,
    preco: f64,
    estoque: i32,
}

fn main() {
    let usuario = Usuario {
        nome: "Ana Silva".to_string(),
        email: "ana@exemplo.com".to_string(),
        idade: 30,
        ativo: true,
    };

    println!("{}", usuario.descrever());
    // Usuario:
    //   nome: "Ana Silva"
    //   email: "ana@exemplo.com"
    //   idade: 30
    //   ativo: true

    let produto = Produto {
        id: 1,
        nome: "Teclado Mecânico".to_string(),
        preco: 350.99,
        estoque: 15,
    };

    println!("{}", produto.descrever());
}

Derive macro com atributos de campo

Vamos criar uma derive macro Builder que gera um builder pattern automaticamente, com suporte a atributos de configuração:

meu_derive/src/lib.rs (adição):

use syn::{Attribute, Lit, Meta, NameValue};

// Estrutura para guardar configuração de cada campo
struct ConfigCampo {
    nome: syn::Ident,
    tipo: syn::Type,
    obrigatorio: bool,
    valor_padrao: Option<proc_macro2::TokenStream>,
}

fn extrair_config_campo(campo: &syn::Field) -> ConfigCampo {
    let nome = campo.ident.clone().unwrap();
    let tipo = campo.ty.clone();
    let mut obrigatorio = true;
    let mut valor_padrao = None;

    // Procura atributo #[builder(padrao = "...")]
    for attr in &campo.attrs {
        if attr.path().is_ident("builder") {
            let _ = attr.parse_nested_meta(|meta| {
                if meta.path.is_ident("padrao") {
                    let valor: syn::LitStr = meta.value()?.parse()?;
                    valor_padrao = Some(valor.value().parse().unwrap());
                    obrigatorio = false;
                }
                if meta.path.is_ident("opcional") {
                    obrigatorio = false;
                }
                Ok(())
            });
        }
    }

    ConfigCampo { nome, tipo, obrigatorio, valor_padrao }
}

#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    match impl_builder(&input) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn impl_builder(input: &DeriveInput) -> syn::Result<TokenStream2> {
    let nome = &input.ident;
    let nome_builder = syn::Ident::new(
        &format!("{nome}Builder"),
        nome.span(),
    );

    let campos = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(f) => &f.named,
            _ => return Err(syn::Error::new_spanned(
                nome, "Builder requer campos nomeados"
            )),
        },
        _ => return Err(syn::Error::new_spanned(
            nome, "Builder requer struct"
        )),
    };

    let configs: Vec<ConfigCampo> = campos.iter()
        .map(extrair_config_campo)
        .collect();

    // Campos do Builder (todos Option<T>)
    let campos_builder = configs.iter().map(|c| {
        let nome = &c.nome;
        let tipo = &c.tipo;
        quote! { #nome: Option<#tipo> }
    });

    // Inicialização do Builder
    let init_campos = configs.iter().map(|c| {
        let nome = &c.nome;
        if let Some(ref padrao) = c.valor_padrao {
            quote! { #nome: Some(#padrao) }
        } else {
            quote! { #nome: None }
        }
    });

    // Métodos setter
    let setters = configs.iter().map(|c| {
        let nome = &c.nome;
        let tipo = &c.tipo;
        quote! {
            pub fn #nome(mut self, valor: #tipo) -> Self {
                self.#nome = Some(valor);
                self
            }
        }
    });

    // Método build() com validação
    let validacoes = configs.iter()
        .filter(|c| c.obrigatorio)
        .map(|c| {
            let nome = &c.nome;
            let nome_str = nome.to_string();
            quote! {
                let #nome = self.#nome.ok_or_else(||
                    format!("Campo obrigatório ausente: {}", #nome_str)
                )?;
            }
        });

    let campos_obrigatorios: Vec<_> = configs.iter()
        .filter(|c| c.obrigatorio)
        .map(|c| &c.nome)
        .collect();

    let campos_opcionais: Vec<_> = configs.iter()
        .filter(|c| !c.obrigatorio)
        .map(|c| &c.nome)
        .collect();

    let construcao_opcionais = configs.iter()
        .filter(|c| !c.obrigatorio)
        .map(|c| {
            let nome = &c.nome;
            if let Some(ref padrao) = c.valor_padrao {
                quote! { #nome: self.#nome.unwrap_or(#padrao) }
            } else {
                quote! { #nome: self.#nome.unwrap_or_default() }
            }
        });

    let codigo = quote! {
        pub struct #nome_builder {
            #(#campos_builder,)*
        }

        impl #nome_builder {
            pub fn novo() -> Self {
                #nome_builder {
                    #(#init_campos,)*
                }
            }

            #(#setters)*

            pub fn build(self) -> Result<#nome, String> {
                #(#validacoes)*

                Ok(#nome {
                    #(#campos_obrigatorios,)*
                    #(#construcao_opcionais,)*
                })
            }
        }

        impl #nome {
            pub fn builder() -> #nome_builder {
                #nome_builder::novo()
            }
        }
    };

    Ok(codigo)
}

Uso:

use meu_derive::Builder;

#[derive(Builder, Debug)]
struct ConfiguracaoServidor {
    // Campos obrigatórios
    host: String,
    porta: u16,

    // Campos com valores padrão
    #[builder(padrao = "10")]
    max_conexoes: usize,

    #[builder(padrao = "30")]
    timeout_segundos: u64,

    #[builder(padrao = "false")]
    debug: bool,
}

fn main() {
    // Uso fluente do builder gerado
    let config = ConfiguracaoServidor::builder()
        .host("0.0.0.0".to_string())
        .porta(8080)
        .max_conexoes(100)
        .debug(true)
        .build()
        .unwrap();

    println!("{:?}", config);

    // Sem campos opcionais — usa os padrões
    let config_minima = ConfiguracaoServidor::builder()
        .host("localhost".to_string())
        .porta(3000)
        .build()
        .unwrap();

    println!("{:?}", config_minima);

    // Erro — campo obrigatório ausente
    let resultado = ConfiguracaoServidor::builder()
        .porta(8080)
        .build();

    println!("Sem host: {:?}", resultado);
    // Err("Campo obrigatório ausente: host")
}

Attribute macro: cronometrar

Attribute macros transformam o item que anotam. Vamos criar #[cronometrar] que envolve qualquer função com medição de tempo:

meu_derive/src/lib.rs (adição):

use syn::{ItemFn, parse_macro_input};

#[proc_macro_attribute]
pub fn cronometrar(args: TokenStream, input: TokenStream) -> TokenStream {
    let funcao = parse_macro_input!(input as ItemFn);

    match impl_cronometrar(args.into(), funcao) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn impl_cronometrar(
    _args: TokenStream2,
    funcao: ItemFn,
) -> syn::Result<TokenStream2> {
    let nome = &funcao.sig.ident;
    let nome_str = nome.to_string();
    let visibilidade = &funcao.vis;
    let assinatura = &funcao.sig;
    let corpo = &funcao.block;
    let atributos = &funcao.attrs;

    // Verifica se é async
    let e_async = funcao.sig.asyncness.is_some();

    let codigo_cronometrado = if e_async {
        quote! {
            #(#atributos)*
            #visibilidade #assinatura {
                let __inicio = ::std::time::Instant::now();
                let __resultado = async move #corpo .await;
                let __duracao = __inicio.elapsed();
                println!(
                    "[cronometrar] {} concluída em {:?}",
                    #nome_str,
                    __duracao
                );
                __resultado
            }
        }
    } else {
        quote! {
            #(#atributos)*
            #visibilidade #assinatura {
                let __inicio = ::std::time::Instant::now();
                let __resultado = (|| #corpo)();
                let __duracao = __inicio.elapsed();
                println!(
                    "[cronometrar] {} concluída em {:?}",
                    #nome_str,
                    __duracao
                );
                __resultado
            }
        }
    };

    Ok(codigo_cronometrado)
}

Uso:

use meu_derive::cronometrar;

#[cronometrar]
fn calcular_primos(limite: u64) -> Vec<u64> {
    (2..=limite)
        .filter(|&n| (2..=(n as f64).sqrt() as u64)
            .all(|i| n % i != 0))
        .collect()
}

#[cronometrar]
fn ordenar_grande(mut dados: Vec<i32>) -> Vec<i32> {
    dados.sort_unstable();
    dados
}

// Também funciona com funções async
#[cronometrar]
async fn buscar_dados() -> String {
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    "dados retornados".to_string()
}

fn main() {
    let primos = calcular_primos(100_000);
    println!("Encontrados {} primos", primos.len());
    // [cronometrar] calcular_primos concluída em 15.234ms

    let dados: Vec<i32> = (0..100_000).rev().collect();
    let ordenado = ordenar_grande(dados);
    println!("Ordenados {} elementos", ordenado.len());
    // [cronometrar] ordenar_grande concluída em 4.891ms
}

Function-like macro: sql!

Macros function-like têm a mesma sintaxe de chamada que macros declarativas mas com o poder das procedurais:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    let tokens: Vec<_> = input.into_iter().collect();

    // Extrai o SQL como string literal
    let sql_str = match tokens.first() {
        Some(proc_macro::TokenTree::Literal(lit)) => {
            let s = lit.to_string();
            // Remove aspas
            s.trim_matches('"').to_string()
        }
        _ => {
            return syn::Error::new(
                proc_macro2::Span::call_site(),
                "sql! espera uma string literal"
            )
            .to_compile_error()
            .into()
        }
    };

    // Validações em tempo de compilação
    let sql_upper = sql_str.to_uppercase();

    // Verifica palavras-chave perigosas
    let avisos: Vec<&str> = [
        ("DROP TABLE", "Operação destrutiva: DROP TABLE detectada"),
        ("DELETE FROM", "DELETE sem WHERE pode remover todos os dados"),
        ("TRUNCATE", "TRUNCATE remove todos os dados da tabela"),
    ]
    .iter()
    .filter(|(palavra, _)| sql_upper.contains(palavra))
    .map(|(_, aviso)| *aviso)
    .collect();

    // Contagem de parâmetros ($1, $2, ...)
    let mut num_params = 0usize;
    let mut i = 1;
    while sql_str.contains(&format!("${i}")) {
        num_params = i;
        i += 1;
    }

    let avisos_code = avisos.iter().map(|aviso| {
        quote! { compile_error!(#aviso); }
    });

    let codigo = quote! {
        {
            #(#avisos_code)*

            struct ConsultaValidada {
                sql: &'static str,
                num_parametros: usize,
            }

            ConsultaValidada {
                sql: #sql_str,
                num_parametros: #num_params,
            }
        }
    };

    codigo.into()
}

Uso:

use meu_derive::sql;

fn main() {
    let consulta = sql!("SELECT id, nome FROM usuarios WHERE id = $1");
    println!("SQL: {}", consulta.sql);
    println!("Parâmetros: {}", consulta.num_parametros);

    // Erro em tempo de compilação:
    // let perigosa = sql!("DROP TABLE usuarios");
    // error: Operação destrutiva: DROP TABLE detectada
}

Testando macros procedurais

# Em meu_derive/Cargo.toml
[dev-dependencies]
trybuild = "1"         # testa erros de compilação
prettyplease = "0.2"  # formata código gerado para legibilidade
// meu_derive/tests/testes_builder.rs
#[test]
fn builder_campos_obrigatorios() {
    // trybuild verifica que o código compila/falha como esperado
    let t = trybuild::TestCases::new();
    t.pass("tests/casos/builder_basico.rs");
    t.compile_fail("tests/casos/builder_sem_obrigatorio.rs");
}

// tests/casos/builder_basico.rs
use meu_derive::Builder;

#[derive(Builder, Debug)]
struct Ponto {
    x: f64,
    y: f64,
}

fn main() {
    let p = Ponto::builder()
        .x(1.0)
        .y(2.0)
        .build()
        .unwrap();

    assert_eq!(p.x, 1.0);
    assert_eq!(p.y, 2.0);
}

// tests/casos/builder_sem_obrigatorio.rs
use meu_derive::Builder;

#[derive(Builder, Debug)]
struct Ponto {
    x: f64,
    y: f64,
}

fn main() {
    // Deve resultar em Err em tempo de execução, não compilação
    // mas o tipo do erro deve ser verificável
    let resultado = Ponto::builder().x(1.0).build();
    assert!(resultado.is_err());
}

Inspecionar código gerado com cargo expand

cargo install cargo-expand

# Expande todas as macros e mostra o código resultante
cargo expand

# Expande apenas um módulo específico
cargo expand meu_modulo

Saída para #[derive(Builder)] struct Config { ... }:

// Código gerado e expandido:
pub struct ConfigBuilder {
    host: Option<String>,
    porta: Option<u16>,
    max_conexoes: Option<usize>,
    // ...
}

impl ConfigBuilder {
    pub fn novo() -> Self {
        ConfigBuilder {
            host: None,
            porta: None,
            max_conexoes: Some(10usize),
            // ...
        }
    }

    pub fn host(mut self, valor: String) -> Self {
        self.host = Some(valor);
        self
    }
    // ...

    pub fn build(self) -> Result<Config, String> {
        let host = self.host.ok_or_else(|| {
            format!("Campo obrigatório ausente: {}", "host")
        })?;
        // ...
    }
}

Um exemplo completo: derive Validar

Vamos criar uma macro que gera validação automática baseada em atributos de campo:

// meu_derive/src/lib.rs — macro Validar

#[proc_macro_derive(Validar, attributes(validar))]
pub fn derive_validar(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    match impl_validar(&input) {
        Ok(tokens) => tokens.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

struct RegraValidacao {
    campo: syn::Ident,
    tipo: syn::Type,
    regras: Vec<Regra>,
}

enum Regra {
    NaoVazio,
    MinLen(usize),
    MaxLen(usize),
    Min(f64),
    Max(f64),
    Email,
    Regex(String),
}

fn extrair_regras(campo: &syn::Field) -> RegraValidacao {
    let nome = campo.ident.clone().unwrap();
    let tipo = campo.ty.clone();
    let mut regras = Vec::new();

    for attr in &campo.attrs {
        if !attr.path().is_ident("validar") {
            continue;
        }

        let _ = attr.parse_nested_meta(|meta| {
            if meta.path.is_ident("nao_vazio") {
                regras.push(Regra::NaoVazio);
            } else if meta.path.is_ident("email") {
                regras.push(Regra::Email);
            } else if meta.path.is_ident("min_len") {
                let valor: syn::LitInt = meta.value()?.parse()?;
                regras.push(Regra::MinLen(valor.base10_parse()?));
            } else if meta.path.is_ident("max_len") {
                let valor: syn::LitInt = meta.value()?.parse()?;
                regras.push(Regra::MaxLen(valor.base10_parse()?));
            } else if meta.path.is_ident("min") {
                let valor: syn::LitFloat = meta.value()?.parse()?;
                regras.push(Regra::Min(valor.base10_parse()?));
            } else if meta.path.is_ident("max") {
                let valor: syn::LitFloat = meta.value()?.parse()?;
                regras.push(Regra::Max(valor.base10_parse()?));
            }
            Ok(())
        });
    }

    RegraValidacao { campo: nome, tipo, regras }
}

fn gerar_verificacao(regra: &RegraValidacao) -> TokenStream2 {
    let campo = &regra.campo;
    let campo_str = campo.to_string();

    let verificacoes = regra.regras.iter().map(|r| match r {
        Regra::NaoVazio => quote! {
            if self.#campo.is_empty() {
                erros.push(format!("{}: não pode ser vazio", #campo_str));
            }
        },
        Regra::MinLen(n) => quote! {
            if self.#campo.len() < #n {
                erros.push(format!(
                    "{}: mínimo {} caracteres (tem {})",
                    #campo_str, #n, self.#campo.len()
                ));
            }
        },
        Regra::MaxLen(n) => quote! {
            if self.#campo.len() > #n {
                erros.push(format!(
                    "{}: máximo {} caracteres (tem {})",
                    #campo_str, #n, self.#campo.len()
                ));
            }
        },
        Regra::Min(n) => quote! {
            if (self.#campo as f64) < #n {
                erros.push(format!(
                    "{}: valor mínimo é {} (tem {})",
                    #campo_str, #n, self.#campo
                ));
            }
        },
        Regra::Max(n) => quote! {
            if (self.#campo as f64) > #n {
                erros.push(format!(
                    "{}: valor máximo é {} (tem {})",
                    #campo_str, #n, self.#campo
                ));
            }
        },
        Regra::Email => quote! {
            if !self.#campo.contains('@') || !self.#campo.contains('.') {
                erros.push(format!("{}: email inválido", #campo_str));
            }
        },
        Regra::Regex(_) => quote! {},
    });

    quote! { #(#verificacoes)* }
}

fn impl_validar(input: &DeriveInput) -> syn::Result<TokenStream2> {
    let nome = &input.ident;

    let campos = match &input.data {
        Data::Struct(d) => match &d.fields {
            Fields::Named(f) => &f.named,
            _ => return Err(syn::Error::new_spanned(
                nome, "Validar requer campos nomeados"
            )),
        },
        _ => return Err(syn::Error::new_spanned(
            nome, "Validar requer struct"
        )),
    };

    let regras: Vec<_> = campos.iter()
        .map(extrair_regras)
        .collect();

    let verificacoes = regras.iter().map(gerar_verificacao);

    let codigo = quote! {
        impl #nome {
            pub fn validar(&self) -> Result<(), Vec<String>> {
                let mut erros: Vec<String> = Vec::new();

                #(#verificacoes)*

                if erros.is_empty() {
                    Ok(())
                } else {
                    Err(erros)
                }
            }

            pub fn e_valido(&self) -> bool {
                self.validar().is_ok()
            }
        }
    };

    Ok(codigo)
}

Uso:

use meu_derive::Validar;

#[derive(Validar, Debug)]
struct FormularioCadastro {
    #[validar(nao_vazio, min_len = 2, max_len = 100)]
    nome: String,

    #[validar(nao_vazio, email)]
    email: String,

    #[validar(min_len = 8, max_len = 72)]
    senha: String,

    #[validar(min = 18.0, max = 120.0)]
    idade: u32,
}

fn main() {
    let formulario_valido = FormularioCadastro {
        nome: "Ana Silva".to_string(),
        email: "ana@exemplo.com".to_string(),
        senha: "MinhaS3nh@".to_string(),
        idade: 30,
    };

    match formulario_valido.validar() {
        Ok(())    => println!("✓ Formulário válido"),
        Err(erros) => println!("✗ Erros: {:?}", erros),
    }

    let formulario_invalido = FormularioCadastro {
        nome: "A".to_string(),                    // muito curto
        email: "email_invalido".to_string(),       // sem @
        senha: "123".to_string(),                  // muito curta
        idade: 15,                                 // menor de 18
    };

    match formulario_invalido.validar() {
        Ok(())     => println!("✓ Válido"),
        Err(erros) => {
            println!("✗ {} erro(s) encontrado(s):", erros.len());
            for erro in &erros {
                println!("  • {erro}");
            }
        }
    }
    // ✗ 4 erro(s) encontrado(s):
    //   • nome: mínimo 2 caracteres (tem 1)
    //   • email: email inválido
    //   • senha: mínimo 8 caracteres (tem 3)
    //   • idade: valor mínimo é 18 (tem 15)
}

Mensagens de erro de qualidade

Macros procedurais bem feitas produzem erros compreensíveis:

// Erro com span preciso — aponta para o local exato do problema
fn verificar_tipo_compativel(
    campo: &syn::Field
) -> syn::Result<()> {
    let tipo = &campo.ty;

    // Verifica se o tipo é String ou &str para validar .is_empty()
    let tipo_str = quote!(#tipo).to_string();
    if !["String", "str"].iter().any(|t| tipo_str.contains(t)) {
        return Err(syn::Error::new_spanned(
            tipo,
            format!(
                "Validar(nao_vazio) requer tipo String ou &str, mas encontrou: {}",
                tipo_str
            )
        ));
    }

    Ok(())
}

// O erro aparece assim no compilador:
// error: Validar(nao_vazio) requer tipo String ou &str, mas encontrou: u32
//  --> src/main.rs:8:5
//   |
// 8 |     #[validar(nao_vazio)]
//   |     ^^^^^^^^^^^^^^^^^^^^^
// 9 |     idade: u32,
//   |            ^^^

Quando usar macros procedurais

Macros procedurais são ferramentas poderosas mas com custo real: aumentam o tempo de compilação, tornam o código mais difícil de depurar, e adicionam complexidade ao projeto.

Use-as quando você precisa eliminar boilerplate repetitivo que traits ou generics não conseguem resolver, quando quer fazer verificações semânticas em tempo de compilação, ou quando está criando uma DSL que melhora significativamente a expressividade.

Não use quando um trait bem projetado resolve o problema, quando generics são suficientes, ou quando a macro economizaria apenas algumas linhas — o custo de manutenção raramente vale para economias pequenas.

Exercícios

Exercício 1

Quais são os três tipos de macro procedural e o que cada um permite?

Ver resposta

✓ Resposta: Derive (#[derive(MinhaMacro)]) gera código a partir de uma struct ou enum, sem alterá-los — é como Debug e Clone funcionam. Attribute (#[minha_macro]) recebe o item anotado e pode substituí-lo, sendo o mecanismo por trás de #[tokio::main] e das rotas de frameworks web. Function-like (minha_macro!(...)) parece uma macro comum, mas processa os tokens em Rust, permitindo validar sintaxe própria em tempo de compilação, como faz o sqlx::query!.

Exercício 2

Por que uma macro procedural precisa morar em crate separada?

Ver resposta

✓ Resposta: Porque ela roda durante a compilação de quem a usa: precisa ser compilada antes e carregada pelo compilador como plugin. Isso exige proc-macro = true no Cargo.toml, e uma crate assim só pode exportar macros — nada mais. Por isso o padrão de duas crates: minha_lib e minha_lib_derive, com a primeira reexportando a macro para que o usuário importe de um lugar só.

Exercício 3

Qual o papel de syn, quote e proc_macro2?

Ver resposta

✓ Resposta: syn converte o fluxo de tokens recebido numa árvore sintática navegável — é o que permite perguntar pelos campos de uma struct em vez de manipular texto. quote faz o inverso: monta tokens a partir de um modelo, com interpolação por #variavel. proc_macro2 é uma versão dos tipos de token que funciona fora do contexto de macro, o que torna a lógica testável em teste comum — sem ela, testar exigiria compilar um crate de verdade a cada caso.

Exercício 4

Como produzir uma mensagem de erro útil numa macro procedural?

Ver resposta

✓ Resposta: Com syn::Error ancorado no span do item problemático, convertido com to_compile_error():

return syn::Error::new_spanned(
    campo,
    "este atributo só vale para campos do tipo String",
).to_compile_error().into();

O span é o que faz o compilador apontar a linha e a coluna exatas no código de quem usou a macro, com sublinhado no lugar certo. Sem ele — usando panic!, por exemplo — a mensagem aparece sem localização, e quem recebe não sabe qual campo causou o problema.

Exercício 5

Como testar uma macro procedural?

Ver resposta

✓ Resposta: Em duas camadas. A lógica interna, que recebe e devolve proc_macro2::TokenStream, é testada em teste unitário comum, comparando a saída esperada. O comportamento visível de fora é testado com trybuild: arquivos de exemplo que devem compilar e outros que devem falhar, com a mensagem de erro esperada gravada em arquivo .stderr e comparada a cada execução. É essa segunda camada que evita a regressão mais comum — a macro continuar funcionando mas passar a errar a mensagem quando o uso está incorreto.

Fontes e leituras recomendadas

 

Comentários

Mais em Rust

Contribuindo com o Ecossistema Rust — Open Source, RFCs e a Comunidade
Contribuindo com o Ecossistema Rust — Open Source, RFCs e a Comunidade

Contribuir com código aberto começa antes do código: ler o guia do projeto…

Programação de Sistemas — Arquivos, Processos e Sinais
Programação de Sistemas — Arquivos, Processos e Sinais

Descer ao nível do sistema operacional traz de volta arquivos, processos e…

Sistemas Distribuídos — Consenso, Mensageria e Tolerância a Falhas
Sistemas Distribuídos — Consenso, Mensageria e Tolerância a Falhas

O que torna o distribuído difícil não é a concorrência, é não conseguir…