Rust

Macros — Escrevendo Código que Escreve Código Já leu

12 min de leitura

Macros — Escrevendo Código que Escreve Código
Desde o primeiro artigo usamos macros sem entender completamente o que são: println!, vec!, assert_eq!, derive. O ponto de exclamação no final de println! não é ornamento — é a marca de uma macro. Chegou a hora de entend

 

Desde o primeiro artigo usamos macros sem entender completamente o que são: println!, vec!, assert_eq!, derive. O ponto de exclamação no final de println! não é ornamento — é a marca de uma macro. Chegou a hora de entender o que são macros em Rust, por que existem, e como escrever as suas próprias.

Macros em Rust são um sistema de metaprogramação: código que gera código em tempo de compilação. São mais poderosas que macros de C (simples substituição de texto), mais seguras, e integradas ao sistema de tipos.


Por que macros existem

Macros resolvem problemas que funções não conseguem resolver. Há três razões principais:

Número variável de argumentos. println!("x={}, y={}", x, y) aceita qualquer número de argumentos. Funções Rust têm aridade fixa — você não pode criar uma função imprimir que aceite 0, 1, 2 ou 10 argumentos. Macros podem.

Geração de código repetitivo. #[derive(Debug, Clone, PartialEq)] gera implementações inteiras de traits automaticamente. Escrever isso manualmente para cada struct seria tedioso e propenso a erros.

DSLs (Domain Specific Languages). Macros permitem criar sintaxes especializadas embutidas no Rust — como a sintaxe de vec![1, 2, 3] que parece parte da linguagem mas é uma macro.


Dois tipos de macros

Rust tem dois sistemas de macros distintos:

Macros declarativas (macro_rules!) — definem padrões de correspondência. São como match para código: se o código de entrada corresponde ao padrão, é substituído pelo código de saída.

Macros procedurais — são funções Rust que operam sobre tokens do compilador. Mais poderosas e mais complexas. Incluem as macros derive, macros de atributo e macros de função.

Neste artigo focamos em macro_rules! — o ponto de entrada mais acessível e suficiente para a maioria dos casos de uso.


macro_rules! — macros declarativas

A sintaxe básica:

macro_rules! nome_da_macro {
    (padrão1) => {
        código_gerado1
    };
    (padrão2) => {
        código_gerado2
    };
}

Vamos começar com o exemplo mais simples — uma macro que não faz nada útil mas demonstra a sintaxe:

macro_rules! dizer_ola {
    () => {
        println!("Olá do macro!");
    };
}

fn main() {
    dizer_ola!();
    dizer_ola!();
}

Saída:

Olá do macro!
Olá do macro!

Capturando argumentos

Macros podem capturar argumentos com designadores de tipo:

macro_rules! inspecionar {
    ($valor:expr) => {
        println!("{} = {:?}", stringify!($valor), $valor);
    };
}

fn main() {
    let x = 42;
    let nome = "Rust";
    let lista = vec![1, 2, 3];

    inspecionar!(x);
    inspecionar!(nome);
    inspecionar!(lista);
    inspecionar!(2 + 2);
    inspecionar!(x * 3 + 1);
}

Saída:

x = 42
nome = "Rust"
lista = [1, 2, 3]
2 + 2 = 4
x * 3 + 1 = 127

$valor:expr captura uma expressão. Os designadores disponíveis incluem:

  • expr — qualquer expressão
  • ident — um identificador (nome de variável ou função)
  • ty — um tipo
  • literal — um literal (número, string)
  • pat — um padrão (como em match)
  • stmt — um statement
  • block — um bloco { ... }
  • tt — qualquer token tree (o mais genérico)

Múltiplos padrões

Macros podem ter múltiplos braços, como match:

macro_rules! criar_vetor {
    // Sem argumentos — vetor vazio
    () => {
        Vec::new()
    };

    // Um único elemento
    ($elem:expr) => {
        {
            let mut v = Vec::new();
            v.push($elem);
            v
        }
    };

    // Elemento repetido N vezes
    ($elem:expr; $n:expr) => {
        {
            let mut v = Vec::with_capacity($n);
            for _ in 0..$n {
                v.push($elem.clone());
            }
            v
        }
    };

    // Lista de elementos
    ($($elem:expr),+ $(,)?) => {
        {
            let mut v = Vec::new();
            $(v.push($elem);)+
            v
        }
    };
}

fn main() {
    let v1: Vec<i32> = criar_vetor!();
    let v2 = criar_vetor!(42);
    let v3 = criar_vetor!(0; 5);
    let v4 = criar_vetor![1, 2, 3, 4, 5];
    let v5 = criar_vetor![10, 20, 30,]; // vírgula final aceita

    println!("{:?}", v1); // []
    println!("{:?}", v2); // [42]
    println!("{:?}", v3); // [0, 0, 0, 0, 0]
    println!("{:?}", v4); // [1, 2, 3, 4, 5]
    println!("{:?}", v5); // [10, 20, 30]
}

O padrão $($elem:expr),+ significa "um ou mais expr separados por vírgulas". O + indica "um ou mais", enquanto * indica "zero ou mais". O $(,)? no final aceita uma vírgula trailing opcional.


Recursão em macros

Macros podem se chamar recursivamente — útil para processar listas de argumentos:

macro_rules! somar {
    // Caso base: um único número
    ($x:expr) => ($x);

    // Caso recursivo: primeiro + resto
    ($x:expr, $($resto:expr),+) => {
        $x + somar!($($resto),+)
    };
}

macro_rules! maximo {
    ($x:expr) => ($x);

    ($x:expr, $($resto:expr),+) => {
        {
            let resto_max = maximo!($($resto),+);
            if $x > resto_max { $x } else { resto_max }
        }
    };
}

fn main() {
    println!("{}", somar!(1));               // 1
    println!("{}", somar!(1, 2, 3));         // 6
    println!("{}", somar!(10, 20, 30, 40));  // 100

    println!("{}", maximo!(5));              // 5
    println!("{}", maximo!(3, 7, 2, 9, 1)); // 9
}

Uma macro prática: hashmap!

A biblioteca padrão tem vec! mas não tem hashmap!. Vamos criar:

use std::collections::HashMap;

macro_rules! hashmap {
    // Vazio
    () => {
        HashMap::new()
    };

    // Com pares chave => valor
    ($($chave:expr => $valor:expr),+ $(,)?) => {
        {
            let mut mapa = HashMap::new();
            $(mapa.insert($chave, $valor);)+
            mapa
        }
    };
}

fn main() {
    let vazio: HashMap<&str, i32> = hashmap!();

    let capitais = hashmap! {
        "Brasil"    => "Brasília",
        "Argentina" => "Buenos Aires",
        "Chile"     => "Santiago",
        "Uruguai"   => "Montevidéu",
    };

    let notas = hashmap! {
        "Ana"    => 9.5,
        "Carlos" => 7.2,
        "Maria"  => 8.8,
    };

    println!("Vazio: {:?}", vazio);
    println!("
Capitais:");
    let mut pares: Vec<_> = capitais.iter().collect();
    pares.sort_by_key(|&(k, _)| k);
    for (pais, capital) in pares {
        println!("  {pais}: {capital}");
    }

    println!("
Notas:");
    let mut ranking: Vec<_> = notas.iter().collect();
    ranking.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap());
    for (aluno, nota) in ranking {
        println!("  {aluno}: {nota:.1}");
    }
}

Saída:

Vazio: {}

Capitais:
  Argentina: Buenos Aires
  Brasil: Brasília
  Chile: Santiago
  Uruguai: Montevidéu

Notas:
  Ana: 9.5
  Maria: 8.8
  Carlos: 7.2

Macros para geração de código repetitivo

Um dos usos mais poderosos de macros é eliminar código boilerplate. Veja um exemplo que gera implementações de conversão de unidades:

macro_rules! unidade {
    ($nome:ident, $simbolo:expr, $para_base:expr, $da_base:expr) => {
        #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
        struct $nome(f64);

        impl $nome {
            fn novo(valor: f64) -> Self {
                $nome(valor)
            }

            fn valor(&self) -> f64 {
                self.0
            }

            fn para_base(&self) -> f64 {
                $para_base(self.0)
            }

            fn da_base(base: f64) -> Self {
                $nome($da_base(base))
            }
        }

        impl std::fmt::Display for $nome {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(f, "{:.4} {}", self.0, $simbolo)
            }
        }
    };
}

// Temperatura — base: Kelvin
unidade!(Celsius,    "°C", |c| c + 273.15,      |k| k - 273.15);
unidade!(Fahrenheit, "°F", |f| (f + 459.67) * 5.0/9.0, |k| k * 9.0/5.0 - 459.67);
unidade!(Kelvin,     "K",  |k| k,               |k| k);

// Distância — base: metro
unidade!(Metro,      "m",  |m| m,         |m| m);
unidade!(Quilometro, "km", |k| k * 1000.0, |m| m / 1000.0);
unidade!(Milha,      "mi", |mi| mi * 1609.344, |m| m / 1609.344);

fn main() {
    // Temperaturas
    let fervura = Celsius::novo(100.0);
    let kelvin  = Kelvin::da_base(fervura.para_base());
    let fahrenheit = Fahrenheit::da_base(fervura.para_base());

    println!("Ponto de fervura da água:");
    println!("  {fervura}");
    println!("  {kelvin}");
    println!("  {fahrenheit}");

    // Distâncias
    let maratona = Quilometro::novo(42.195);
    let em_metros = Metro::da_base(maratona.para_base());
    let em_milhas = Milha::da_base(maratona.para_base());

    println!("
Maratona:");
    println!("  {maratona}");
    println!("  {em_metros}");
    println!("  {em_milhas}");
}

Saída:

Ponto de fervura da água:
  100.0000 °C
  373.1500 K
  212.0000 °F

Maratona:
  42.1950 km
  42195.0000 m
  26.2188 mi

A macro unidade! gerou cinco structs completas com implementações de Display, métodos de conversão e traits derivados — com uma única linha cada.


Macros procedurais — uma introdução

Macros procedurais são mais poderosas mas também mais complexas. Elas precisam estar em uma crate separada do tipo proc-macro. Vamos ver como usar as já existentes, que é o que você fará na maioria dos projetos.

derive macros — as mais usadas

Já usamos #[derive(...)] extensivamente. Elas geram implementações automáticas:

// Estas macros derive são da biblioteca padrão:
#[derive(
    Debug,      // implementa fmt::Debug
    Clone,      // implementa Clone
    Copy,       // implementa Copy (apenas se todos campos implementam)
    PartialEq,  // implementa == e !=
    Eq,         // igualdade total (requer PartialEq)
    PartialOrd, // implementa <, >, <=, >=
    Ord,        // ordenação total
    Hash,       // permite uso como chave em HashMap
    Default,    // implementa Default::default()
)]
struct Ponto {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Ponto { x: 1, y: 2 };
    let p2 = p1.clone();
    let p3 = Ponto::default(); // { x: 0, y: 0 }

    println!("{:?}", p1);
    println!("p1 == p2: {}", p1 == p2);
    println!("p1 > p3: {}", p1 > p3);
    println!("Default: {:?}", p3);
}

serde — serialização com macros

A crate serde é um dos exemplos mais impressionantes de macros procedurais em ação:

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct Usuario {
    id: u32,
    nome: String,
    email: String,
    #[serde(default)]
    ativo: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    telefone: Option<String>,
}

fn main() {
    let usuario = Usuario {
        id: 1,
        nome: String::from("Ana Silva"),
        email: String::from("ana@exemplo.com"),
        ativo: true,
        telefone: Some(String::from("+55 11 99999-0000")),
    };

    // Serialização: Rust → JSON
    let json = serde_json::to_string_pretty(&usuario).unwrap();
    println!("JSON:
{json}");

    // Desserialização: JSON → Rust
    let json_entrada = r#"
    {
        "id": 2,
        "nome": "Carlos",
        "email": "carlos@exemplo.com",
        "ativo": false
    }
    "#;

    let usuario2: Usuario = serde_json::from_str(json_entrada).unwrap();
    println!("
Desserializado: {:?}", usuario2);
}

Saída:

JSON:
{
  "id": 1,
  "nome": "Ana Silva",
  "email": "ana@exemplo.com",
  "ativo": true,
  "telefone": "+55 11 99999-0000"
}

Desserializado: Usuario { id: 2, nome: "Carlos", email: "carlos@exemplo.com",
    ativo: false, telefone: None }

Tudo isso gerado por #[derive(Serialize, Deserialize)] — sem uma linha de código de serialização escrito manualmente.


Um programa completo: framework de testes próprio

Vamos usar macros para criar um mini framework de testes com relatório colorido:

macro_rules! suite {
    ($nome:expr, { $($corpo:tt)* }) => {
        println!("
╔══ Suite: {} ══╗", $nome);
        $($corpo)*
        println!("╚{}╝
", "═".repeat($nome.len() + 12));
    };
}

macro_rules! teste {
    ($nome:expr, $corpo:block) => {
        let resultado = std::panic::catch_unwind(|| $corpo);
        match resultado {
            Ok(_)  => println!("  ✓ {}", $nome),
            Err(_) => println!("  ✗ {} [FALHOU]", $nome),
        }
    };
}

macro_rules! verificar {
    ($cond:expr) => {
        if !($cond) {
            panic!("Verificação falhou: {}", stringify!($cond));
        }
    };

    ($cond:expr, $msg:expr) => {
        if !($cond) {
            panic!("Verificação falhou: {} — {}", stringify!($cond), $msg);
        }
    };
}

macro_rules! verificar_eq {
    ($esq:expr, $dir:expr) => {
        {
            let esq = $esq;
            let dir = $dir;
            if esq != dir {
                panic!(
                    "Valores diferentes:
  esquerda: {:?}
  direita:  {:?}",
                    esq, dir
                );
            }
        }
    };
}

// Código a ser testado
fn fatorial(n: u64) -> u64 {
    match n {
        0 | 1 => 1,
        _ => n * fatorial(n - 1),
    }
}

fn e_palindromo(s: &str) -> bool {
    let chars: Vec<char> = s.chars().collect();
    let n = chars.len();
    (0..n / 2).all(|i| chars[i] == chars[n - 1 - i])
}

fn celsius_para_fahrenheit(c: f64) -> f64 {
    c * 9.0 / 5.0 + 32.0
}

fn main() {
    suite!("Fatorial", {
        teste!("fatorial de 0 é 1", {
            verificar_eq!(fatorial(0), 1);
        });

        teste!("fatorial de 1 é 1", {
            verificar_eq!(fatorial(1), 1);
        });

        teste!("fatorial de 5 é 120", {
            verificar_eq!(fatorial(5), 120);
        });

        teste!("fatorial de 10 é 3628800", {
            verificar_eq!(fatorial(10), 3_628_800);
        });

        teste!("fatorial é crescente", {
            verificar!(fatorial(5) < fatorial(6), "5! deve ser menor que 6!");
        });
    });

    suite!("Palíndromo", {
        teste!("'radar' é palíndromo", {
            verificar!(e_palindromo("radar"));
        });

        teste!("'rust' não é palíndromo", {
            verificar!(!e_palindromo("rust"));
        });

        teste!("string vazia é palíndromo", {
            verificar!(e_palindromo(""));
        });

        teste!("'a' é palíndromo", {
            verificar!(e_palindromo("a"));
        });

        teste!("'abba' é palíndromo", {
            verificar!(e_palindromo("abba"));
        });
    });

    suite!("Temperatura", {
        teste!("0°C = 32°F", {
            verificar_eq!(celsius_para_fahrenheit(0.0), 32.0);
        });

        teste!("100°C = 212°F", {
            verificar_eq!(celsius_para_fahrenheit(100.0), 212.0);
        });

        teste!("-40°C = -40°F", {
            verificar_eq!(celsius_para_fahrenheit(-40.0), -40.0);
        });

        teste!("37°C ≈ 98.6°F", {
            let resultado = celsius_para_fahrenheit(37.0);
            let esperado = 98.6f64;
            verificar!(
                (resultado - esperado).abs() < 0.01,
                "diferença maior que 0.01"
            );
        });
    });
}

Saída:

╔══ Suite: Fatorial ══╗
  ✓ fatorial de 0 é 1
  ✓ fatorial de 1 é 1
  ✓ fatorial de 5 é 120
  ✓ fatorial de 10 é 3628800
  ✓ fatorial é crescente
╚═══════════════════════╝

╔══ Suite: Palíndromo ══╗
  ✓ 'radar' é palíndromo
  ✓ 'rust' não é palíndromo
  ✓ string vazia é palíndromo
  ✓ 'a' é palíndromo
  ✓ 'abba' é palíndromo
╚════════════════════════╝

╔══ Suite: Temperatura ══╗
  ✓ 0°C = 32°F
  ✓ 100°C = 212°F
  ✓ -40°C = -40°F
  ✓ 37°C ≈ 98.6°F
╚═════════════════════════╝

Depurando macros

Quando uma macro se comporta de forma inesperada, o compilador pode expandir o código gerado. Use a flag +nightly e cargo expand (crate cargo-expand):

cargo install cargo-expand
cargo expand

Isso mostra o código Rust real gerado pelas macros — inestimável para debug.

Outra técnica: comece simples. Macros complexas devem ser construídas incrementalmente — adicione um padrão de cada vez e teste.


Quando usar macros vs funções

Macros são poderosas mas têm custos: são mais difíceis de escrever, ler e depurar do que funções. Use macros quando:

Você precisa de número variável de argumentos — funções não podem.

Você quer geração de código em tempo de compilação — structs, implementações de traits, código repetitivo.

Você está criando uma DSL — uma sintaxe especializada que simplifica um domínio.

Use funções para todo o resto — são mais simples, têm erros mais claros, e se beneficiam de todas as ferramentas de análise estática.

A regra de ouro: se você pode resolver com uma função genérica, use uma função. Macros são o último recurso expressivo, não o primeiro.


Fontes e leituras recomendadas

  • The Rust Programming Language, Cap. 19.5Macros — https://doc.rust-lang.org/book/ch19-06-macros.html
  • Rust by Example — Macros — https://doc.rust-lang.org/rust-by-example/macros.html
  • "The Little Book of Rust Macros" — guia completo sobre macro_rules! — https://veykril.github.io/tlborm/
  • cargo-expand — ferramenta para expandir macros — https://github.com/dtolnay/cargo-expand
  • serde documentation — o exemplo mais impressionante de macros procedurais — https://serde.rs
  • "Procedural Macros Workshop" — David Tolnay — tutorial prático de macros procedurais — https://github.com/dtolnay/proc-macro-workshop
  • Rustlings — não tem seção de macros, mas os exemplos anteriores ajudam a consolidar — https://github.com/rust-lang/rustlings
Comentários

Mais em Rust

Tratamento de Erros com Result — Erros como Valores, não Exceções
Tratamento de Erros com Result — Erros como Valores, não Exceções

&nbsp; Em linguagens como Java, Python e C#, erros s&atilde;o tratados com e...

FFI — Integrando Rust com C e Outras Linguagens
FFI — Integrando Rust com C e Outras Linguagens

O mundo real raramente é monolítico. Sistemas legados em C, bibliotecas de al...

Compiladores e Interpretadores — Construindo uma Linguagem com Rust
Compiladores e Interpretadores — Construindo uma Linguagem com Rust

Rust — Artigo #44 Compiladores e Interpretadores — Construindo uma Linguagem...