Macros declarativas em Rust, definidas com macro_rules!, são uma ferramenta poderosa para metaprogramação e geração de código em tempo de compilação. Elas permitem que você escreva código que gera outros códigos, reduzindo repetição e criando abstrações que as funções comuns não conseguem. Nesta aula, exploraremos a sintaxe, os padrões de correspondência, a repetição, a higiene e exemplos práticos para que você domine esse recurso essencial.

Diferentemente de macros em C, as macros de Rust são higiênicas, o que significa que elas não vazam variáveis ou identificadores para o contexto de chamada, evitando colisões de nomes. Além disso, elas são expandidas antes da verificação de tipos, o que permite criar DSLs (linguagens de domínio específico) e reduzir boilerplate.

Sintaxe

A sintaxe básica de uma macro declarativa é: macro_rules! nome_da_macro { (padrão) => { expansão } }. O padrão define como a macro deve ser chamada, e a expansão é o código que será gerado. A macro pode ter múltiplas regras, cada uma com um padrão e uma expansão, e o compilador tenta corresponder a entrada com cada regra em ordem.

Os padrões usam uma sintaxe especial para capturar partes da entrada. Por exemplo, $ident:ident captura um identificador, $expr:expr captura uma expressão, e $ty:ty captura um tipo. Esses capturadores são chamados de fragmentos e são seguidos por um tipo de fragmento (como ident, expr, ty, etc.). A lista completa de fragmentos está na documentação oficial.

macro_rules! saudacao {
    ($nome:expr) => {
        println!("Olá, {}!", $nome);
    };
}

fn main() {
    saudacao!("Mundo");
}

Neste exemplo, a macro saudacao aceita uma expressão e imprime uma saudação. O padrão $nome:expr captura a expressão passada e a usa na expansão.

Padrões e repetição

Os padrões de macro podem incluir repetição usando $(...)* (zero ou mais vezes) ou $(...)+ (uma ou mais vezes). Isso permite que a macro aceite um número variável de argumentos, como em vec![1, 2, 3]. A repetição pode ser usada tanto no padrão quanto na expansão, e deve haver correspondência entre as variáveis repetidas.

Por exemplo, uma macro que soma vários números pode ser definida assim:

macro_rules! soma {
    ($($num:expr),*) => {
        {
            let mut total = 0;
            $( total += $num; )*
            total
        }
    };
}

fn main() {
    let resultado = soma!(1, 2, 3, 4);
    println!("{}", resultado); // 10
}

No padrão, $( $num:expr ),* captura zero ou mais expressões separadas por vírgulas. Na expansão, $( total += $num; )* repete o bloco para cada expressão capturada. Note que a repetição na expansão deve usar as mesmas variáveis repetidas do padrão.

Higiene

Higiene é uma propriedade das macros de Rust que garante que os identificadores introduzidos pela macro não entrem em conflito com os do contexto de chamada. Isso significa que uma macro pode usar nomes como temp sem se preocupar em sobrescrever uma variável temp do chamador. Internamente, o compilador atribui um contexto único a cada identificador criado pela macro, tornando-os distintos.

No entanto, há exceções: se a macro receber um identificador como argumento (usando ident), ela pode usá-lo no código gerado, e isso pode causar conflitos se o chamador já tiver uma variável com esse nome. Para evitar isso, é possível usar o operador paste ou técnicas de concatenação, mas a higiene básica já resolve a maioria dos casos.

macro_rules! cria_var {
    ($nome:ident) => {
        let $nome = 42;
    };
}

fn main() {
    let x = 10;
    cria_var!(x);
    println!("{}", x); // 42, mas não sobrescreve a variável original? Na verdade, isso causa erro de compilação, pois x é redefinido no mesmo escopo.
}

Este exemplo ilustra um caso em que a higiene não protege totalmente, pois o identificador x é passado para a macro e usado como nome de variável, resultando em uma redefinição no mesmo escopo. Para evitar isso, você pode usar um bloco { } para isolar o escopo.

Exemplos

Vamos ver alguns exemplos práticos de macros declarativas. O primeiro é uma macro para criar funções getter/setter para structs, reduzindo boilerplate. O segundo é uma macro para definir testes comuns.

Macro para getters e setters

macro_rules! getter_setter {
    ($struct_name:ident, $field:ident, $type:ty) => {
        impl $struct_name {
            pub fn get_$field(&self) -> $type {
                self.$field.clone()
            }
            pub fn set_$field(&mut self, valor: $type) {
                self.$field = valor;
            }
        }
    };
}

struct Pessoa {
    nome: String,
    idade: u32,
}

getter_setter!(Pessoa, nome, String);
getter_setter!(Pessoa, idade, u32);

fn main() {
    let mut p = Pessoa { nome: "Ana".to_string(), idade: 30 };
    println!("{}", p.get_nome());
    p.set_idade(31);
    println!("{}", p.get_idade());
}

Macro para testes

macro_rules! test_igual {
    ($esquerda:expr, $direita:expr) => {
        #[test]
        fn test_igual() {
            assert_eq!($esquerda, $direita);
        }
    };
}

test_igual!(2 + 2, 4);
test_igual!(5 * 5, 25);

Esses exemplos mostram como as macros podem gerar código repetitivo de forma concisa e segura.

Boas práticas

  • Use macros apenas quando necessário; funções e genéricos são mais legíveis e verificáveis.
  • Documente bem suas macros com comentários e exemplos, pois a expansão pode ser difícil de depurar.
  • Evite macros que escondam fluxos de controle complexos; prefira implementações explícitas.
  • Teste macros extensivamente, incluindo casos extremos de entrada.
  • Considere usar a macro macro_rules! para DSLs pequenas, mas avalie se uma solução com traits não seria mais idiomática.

Referências

Exercícios

  1. Crie uma macro max! que aceite dois ou mais números e retorne o maior deles. Use repetição para aceitar múltiplos argumentos.

    ✓ Resposta:
    macro_rules! max {
        ($primeiro:expr $(, $resto:expr)*) => {
            {
                let mut maior = $primeiro;
                $( if $resto > maior { maior = $resto; } )*
                maior
            }
        };
    }
    
    fn main() {
        println!("{}", max!(3, 5, 2, 8)); // 8
    }
  2. Escreva uma macro cria_struct! que receba o nome da struct e uma lista de campos (nome e tipo) e gere a definição da struct com esses campos.

    ✓ Resposta:
    macro_rules! cria_struct {
        ($nome:ident { $($campo:ident: $tipo:ty),* }) => {
            struct $nome {
                $( $campo: $tipo ),*
            }
        };
    }
    
    cria_struct!(Ponto { x: f64, y: f64 });
    
    fn main() {
        let p = Ponto { x: 1.0, y: 2.0 };
        println!("({}, {})", p.x, p.y);
    }
  3. Implemente uma macro calcula! que aceite uma expressão e imprima o resultado com uma mensagem. Use a macro println! dentro da expansão.

    ✓ Resposta:
    macro_rules! calcula {
        ($expr:expr) => {
            println!("Resultado: {}", $expr);
        };
    }
    
    fn main() {
        calcula!(10 + 5);
    }
  4. Crie uma macro vetor_de! que aceite um valor e uma quantidade, e gere um vetor com o valor repetido a quantidade de vezes. Use repetição e concatenação se necessário.

    ✓ Resposta:
    macro_rules! vetor_de {
        ($valor:expr; $n:expr) => {
            {
                let mut v = Vec::new();
                for _ in 0..$n {
                    v.push($valor);
                }
                v
            }
        };
    }
    
    fn main() {
        let v = vetor_de![7; 3];
        println!("{:?}", v); // [7, 7, 7]
    }
  5. Escreva uma macro imprime_pares! que aceite uma lista de números e imprima apenas os pares. Use repetição e condição.

    ✓ Resposta:
    macro_rules! imprime_pares {
        ($($num:expr),*) => {
            $( if $num % 2 == 0 { println!("{}", $num); } )*
        };
    }
    
    fn main() {
        imprime_pares!(1, 2, 3, 4, 5, 6); // imprime 2, 4, 6
    }