Bem-vindo à aula sobre derive macros e atributos em Rust. Nesta aula, vamos mergulhar em duas ferramentas essenciais que automatizam a implementação de código repetitivo e permitem adicionar metadados às declarações. As derive macros são uma forma de geração de código em tempo de compilação, enquanto os atributos são anotações que modificam o comportamento do compilador ou de macros.

Dominar esses recursos é fundamental para escrever código idiomático e produtivo em Rust, pois eles reduzem a quantidade de código manual e garantem consistência. Vamos começar explorando o atributo #[derive], depois os atributos mais comuns, quando usá-los e, por fim, uma visão geral das macros procedurais, que são a base das derive macros.

#[derive]

O atributo #[derive] é usado em structs, enums e unions para implementar automaticamente um ou mais traits fornecidos pelo compilador ou por bibliotecas externas. É uma das formas mais comuns de usar macros procedurais no Rust, pois o compilador gera a implementação do trait com base na definição do tipo.

Por exemplo, para habilitar a comparação de igualdade, clonagem, depuração e cópia, você pode simplesmente anotar seu tipo com #[derive(PartialEq, Clone, Debug, Copy)]. Isso evita escrever manualmente as implementações, que muitas vezes são mecânicas e propensas a erros.

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

fn main() {
    let p1 = Ponto { x: 1.0, y: 2.0 };
    let p2 = p1; // Copy
    let p3 = p1.clone(); // Clone
    println!("{:?}", p1); // Debug
    println!("{}", p1 == p2); // PartialEq
}

Os traits mais comuns que podem ser derivados incluem Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, e Serialize/Deserialize (via serde). É importante notar que nem todos os traits podem ser derivados; por exemplo, Display não pode ser derivado automaticamente porque requer formatação personalizada.

Atributos comuns

Além de #[derive], Rust possui vários atributos que controlam o comportamento do compilador, como #[allow], #[warn], #[deny], #[cfg], #[inline], e #[deprecated]. Esses atributos são usados para ajustar avisos, compilação condicional, otimizações e documentação.

Por exemplo, #[allow(dead_code)] suprime avisos sobre código não utilizado, enquanto #[cfg(target_os = "linux")] inclui o item apenas em sistemas Linux. O atributo #[inline] sugere ao compilador que a função deve ser expandida inline, e #[deprecated] marca um item como obsoleto, gerando avisos para quem o usar.

#[allow(dead_code)]
fn funcao_nao_usada() {}

#[cfg(target_os = "linux")]
fn so_linux() {
    println!("Estamos no Linux");
}

#[deprecated(note = "Use a nova função")]
fn antiga() {}

fn main() {
    // antiga(); // gera aviso de depreciação
}

Outros atributos importantes são #[derive] (já visto), #[repr] para controlar a representação em memória, e #[macro_use] para importar macros. Também existem atributos específicos de crates, como #[serde(rename_all = "camelCase")] usado com a crate serde para personalizar a serialização.

Quando usar

Usar #[derive] é recomendado sempre que você precisa implementar um trait que é puramente mecânico e baseado nos campos do tipo. Por exemplo, se você precisa comparar instâncias de uma struct por igualdade, derivar PartialEq é muito mais simples e menos propenso a erros do que escrever manualmente a implementação.

No entanto, nem sempre é apropriado. Se a implementação do trait requer lógica que não pode ser deduzida automaticamente, como Display ou From, você deve implementá-la manualmente. Além disso, traits como Default só podem ser derivados se todos os campos tiverem um valor padrão, o que nem sempre é o caso.

Uma boa prática é usar derive para traits simples e comuns, e reservar implementações manuais para comportamentos específicos. Isso mantém o código limpo e reduz a duplicação, mas também é importante não exagerar: se o tipo tem invariantes que precisam ser mantidos, a derivação pode quebrá-los silenciosamente.

Visão de macros procedurais

As derive macros são um tipo de macro procedural, que são funções que operam no AST (Árvore Sintática Abstrata) do código Rust e geram novo código. Elas são definidas em crates separadas e podem ser usadas por outras crates. Além de derive, existem macros procedurais de atributo e de função.

Uma macro procedural é escrita em Rust e usa o crate proc_macro para manipular tokens. Para criar uma derive macro, você define uma função anotada com #[proc_macro_derive] que recebe um TokenStream de entrada e retorna um TokenStream de saída. O código gerado é inserido no ponto onde a macro é usada.

// Dentro de uma crate de macros (ex: my_macros)
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(MeuTrait)]
pub fn derive_meu_trait(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let nome = &input.ident;
    let expanded = quote! {
        impl MeuTrait for #nome {
            fn metodo() { println!("Implementado automaticamente"); }
        }
    };
    expanded.into()
}

Esse exemplo usa as crates syn e quote para parsear e gerar código. Embora as macros procedurais sejam poderosas, elas são avançadas e devem ser usadas com cuidado, pois podem tornar o código mais difícil de ler e depurar. Elas são a base de bibliotecas como serde, diesel e rocket.

Boas práticas e observações finais

Algumas boas práticas ao usar derive macros e atributos:

  • Prefira derivar traits padrão quando possível, em vez de implementar manualmente, para reduzir código e erros.
  • Use atributos como #[allow] e #[cfg] com moderação, para não esconder problemas reais.
  • Ao criar macros procedurais, documente bem e forneça testes, pois elas afetam a compilação de quem as usa.
  • Mantenha as implementações manuais de traits quando a lógica for específica, e não tente forçar a derivação.
  • Fique atento à compatibilidade: algumas derivações podem não funcionar com tipos genéricos ou com lifetimes complexos.

Essas ferramentas são essenciais no ecossistema Rust, e dominá-las eleva a qualidade e a manutenibilidade do seu código.

Referências

Exercícios

  1. Crie uma struct Pessoa com campos nome (String) e idade (u32). Derive os traits Debug, Clone, PartialEq e Eq. Escreva um programa que crie duas instâncias, clone uma, compare e imprima com {:?}.
  2. ✓ Resposta:
    #[derive(Debug, Clone, PartialEq, Eq)]
    struct Pessoa {
        nome: String,
        idade: u32,
    }
    
    fn main() {
        let p1 = Pessoa { nome: "Ana".to_string(), idade: 30 };
        let p2 = p1.clone();
        println!("{:?}", p1);
        println!("Iguais? {}", p1 == p2);
    }
  3. Explique por que Display não pode ser derivado automaticamente e dê um exemplo de implementação manual para a struct Pessoa.
  4. ✓ Resposta:Display não pode ser derivado porque a formatação é específica do tipo e não pode ser inferida dos campos. Implementação manual:
    use std::fmt;
    
    impl fmt::Display for Pessoa {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "{} ({} anos)", self.nome, self.idade)
        }
    }
  5. Use o atributo #[cfg] para criar uma função que só compila em sistemas Windows e outra que só compila em sistemas não-Windows. Imprima uma mensagem apropriada em cada.
  6. ✓ Resposta:
    #[cfg(target_os = "windows")]
    fn mensagem() {
        println!("Você está no Windows");
    }
    
    #[cfg(not(target_os = "windows"))]
    fn mensagem() {
        println!("Você não está no Windows");
    }
    
    fn main() {
        mensagem();
    }
  7. Pesquise e descreva o que são macros procedurais de atributo e dê um exemplo de uso (pode ser fictício) para um atributo #[route] que seria usado em um framework web.
  8. ✓ Resposta:Macros procedurais de atributo são usadas para modificar o item que elas anexam. Exemplo:
    #[route(GET, "/home")]
    fn home() { ... }
    A macro geraria código para registrar a rota em um framework web. Ela é definida com #[proc_macro_attribute] e recebe dois TokenStreams: um para os argumentos do atributo e outro para o item.
  9. Crie uma macro procedural simples (em um crate separado) que derive um trait Ola para qualquer struct, onde o trait tem um método ola() que imprime "Olá, mundo!". Use as crates syn e quote. (Dica: configure um crate de macros e um crate de teste).
  10. ✓ Resposta:Em Cargo.toml da crate de macros:
    [lib]
    proc-macro = true
    
    Defina a macro:
    use proc_macro::TokenStream;
    use quote::quote;
    use syn::{parse_macro_input, DeriveInput};
    
    #[proc_macro_derive(Ola)]
    pub fn derive_ola(input: TokenStream) -> TokenStream {
        let input = parse_macro_input!(input as DeriveInput);
        let nome = &input.ident;
        let expanded = quote! {
            impl Ola for #nome {
                fn ola() {
                    println!("Olá, mundo!");
                }
            }
        };
        expanded.into()
    }
    
    Na crate de teste, use #[derive(Ola)] e chame Ola::ola().