Macros em Rust: Automatizando Código Além do que Python Permite

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:

  1. Macros declarativas (com macro_rules!): Mais simples, funcionam como correspondência de padrões
  2. 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:

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 tipos Copy (como inteiros) e verboso para os demais. Usar std::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:

  1. Verifica tipos em tempo de compilação
  2. Não tem overhead de runtime (a macro é expandida antes da execução)
  3. 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:

  1. Macros de Derive: Automatizam implementação de traits
  2. Macros de Atributo: Aplicam transformações a itens
  3. 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 de panic!(). 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:

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:

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:

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

  1. 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
    
  2. Tentar usar macros como funções: Têm regras diferentes — não aceitam tipos genéricos da mesma forma.

  3. 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.

  4. Subestimar a complexidade: Macros procedurais são mais complexas que decoradores Python. Use cargo expand para entender o que está sendo gerado.

  5. Usar panic! em macros procedurais: Prefira syn::Error::new(...).to_compile_error() para produzir erros de compilação adequados.

Quando Usar Macros no Rust

Use macros quando:

O Que Aprendemos

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!