Macros em Rust: Declarativas e Procedurais — Guia Completo 2026

Aprenda macros em Rust: macro_rules!, derive macros, attribute macros e function-like. Exemplos práticos com syn, quote e casos reais de uso.

Introdução

Macros são uma das ferramentas mais poderosas de Rust — elas permitem gerar código em tempo de compilação, eliminando boilerplate e criando abstrações impossíveis com funções normais. Se você já usou println!(), vec![], #[derive(Debug)] ou #[tokio::main], você já usou macros.

Em Rust, existem dois tipos principais: macros declarativas (macro_rules!) e macros procedurais (derive, attribute e function-like). Cada tipo tem seus casos de uso, vantagens e complexidades. Neste guia, vamos explorar ambos com exemplos práticos que você pode aplicar nos seus projetos.

Se você está começando com Rust, recomendamos primeiro nosso tutorial de primeiros passos e depois voltar a este artigo.

Macros Declarativas com macro_rules!

As macros declarativas usam pattern matching no código-fonte. Elas são definidas com macro_rules! e funcionam como um “match” sobre tokens de Rust. Para entender melhor pattern matching em Rust, confira nosso artigo sobre pattern matching avançado.

Exemplo Básico: vec! Simplificado

// Reimplementação simplificada do vec![]
macro_rules! meu_vec {
    // Caso: lista de elementos — meu_vec![1, 2, 3]
    ( $( $elemento:expr ),* ) => {
        {
            let mut v = Vec::new();
            $( v.push($elemento); )*
            v
        }
    };
    // Caso: repetição — meu_vec![0; 5] cria vec com 5 zeros
    ( $elemento:expr ; $contagem:expr ) => {
        vec![$elemento; $contagem]
    };
}

fn main() {
    let numeros = meu_vec![1, 2, 3, 4, 5];
    let zeros = meu_vec![0; 10];
    println!("Números: {:?}", numeros); // [1, 2, 3, 4, 5]
    println!("Zeros: {:?}", zeros);     // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}

Fragmentos de Macro (Designators)

Os designators definem que tipo de token a macro aceita:

DesignatorAceitaExemplo
$x:exprExpressão1 + 2, foo()
$x:identIdentificadorminha_var, MeuTipo
$x:tyTipoi32, Vec<String>
$x:patPadrãoSome(x), _
$x:stmtDeclaraçãolet x = 5
$x:blockBloco{ println!("oi"); }
$x:ttToken treeQualquer token
$x:literalLiteral42, "texto"

Exemplo Prático: Macro de HashMap

Uma macro que simplifica a criação de HashMaps — útil em qualquer projeto. Para mais sobre HashMaps, veja nosso guia da stdlib HashMap.

macro_rules! hashmap {
    ( $( $chave:expr => $valor:expr ),* $(,)? ) => {
        {
            let mut mapa = std::collections::HashMap::new();
            $( mapa.insert($chave, $valor); )*
            mapa
        }
    };
}

fn main() {
    let config = hashmap! {
        "host" => "localhost",
        "porta" => "8080",
        "debug" => "true",
    };

    for (chave, valor) in &config {
        println!("{}: {}", chave, valor);
    }
}

Macro Recursiva: DSL de SQL

Macros podem ser recursivas, permitindo criar mini-DSLs:

macro_rules! query {
    // SELECT campos FROM tabela
    (SELECT $( $campo:ident ),+ FROM $tabela:ident) => {
        format!(
            "SELECT {} FROM {}",
            vec![ $( stringify!($campo) ),+ ].join(", "),
            stringify!($tabela)
        )
    };
    // SELECT campos FROM tabela WHERE condição
    (SELECT $( $campo:ident ),+ FROM $tabela:ident WHERE $condicao:expr) => {
        format!(
            "SELECT {} FROM {} WHERE {}",
            vec![ $( stringify!($campo) ),+ ].join(", "),
            stringify!($tabela),
            $condicao
        )
    };
}

fn main() {
    let sql = query!(SELECT nome, email FROM usuarios);
    println!("{}", sql);
    // Output: SELECT nome, email FROM usuarios

    let sql2 = query!(SELECT id, nome FROM produtos WHERE "preco > 100");
    println!("{}", sql2);
    // Output: SELECT id, nome FROM produtos WHERE preco > 100
}

Macros Procedurais

Macros procedurais são programas Rust que recebem código como entrada e geram código como saída. São mais poderosas que macro_rules! mas também mais complexas. Existem três tipos:

1. Derive Macros

As derive macros geram implementações automaticamente. Você já usa isso com #[derive(Debug, Clone, Serialize)]. Vamos criar uma derive macro que gera um método descricao():

// Em uma crate separada: minha_macro/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Descricao)]
pub fn derive_descricao(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let nome = &ast.ident;

    let expandido = quote! {
        impl #nome {
            pub fn descricao() -> &'static str {
                concat!("Struct: ", stringify!(#nome))
            }
        }
    };

    TokenStream::from(expandido)
}
// No código que usa a macro
use minha_macro::Descricao;

#[derive(Descricao, Debug)]
struct Usuario {
    nome: String,
    email: String,
}

fn main() {
    println!("{}", Usuario::descricao());
    // Output: Struct: Usuario
}

2. Attribute Macros

Attribute macros transformam o item anotado. O exemplo mais famoso é #[tokio::main]:

// Exemplo conceitual de como #[tokio::main] funciona
#[proc_macro_attribute]
pub fn meu_async_main(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let func = parse_macro_input!(item as syn::ItemFn);
    let corpo = &func.block;

    let expandido = quote! {
        fn main() {
            tokio::runtime::Runtime::new()
                .unwrap()
                .block_on(async #corpo)
        }
    };

    TokenStream::from(expandido)
}

Para aprender mais sobre o runtime Tokio e async/await, confira nosso guia do ecossistema Tokio e o artigo sobre async Rust.

3. Function-like Macros

Parecem chamadas de função mas são macros procedurais:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    let query = input.to_string();
    // Validar SQL em tempo de compilação...
    let expandido = quote! {
        sqlx::query!(#query)
    };
    TokenStream::from(expandido)
}

// Uso:
// let resultado = sql!(SELECT * FROM usuarios WHERE id = $1);

Crates Essenciais: syn e quote

As crates syn e quote são fundamentais para macros procedurais:

  • syn: Faz o parsing de código Rust em uma AST (Abstract Syntax Tree)
  • quote: Gera código Rust a partir de templates com quote!{}
# Cargo.toml da crate de macros
[lib]
proc-macro = true

[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"

Exemplo Completo: Derive Builder

Um padrão muito útil é gerar builders automaticamente. Para entender o padrão Builder em Rust, veja nosso artigo sobre padrão Builder:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};

#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let nome = &ast.ident;
    let builder_nome = syn::Ident::new(
        &format!("{}Builder", nome),
        nome.span(),
    );

    // Extrair campos da struct
    let campos = match &ast.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => panic!("Builder só funciona com named fields"),
        },
        _ => panic!("Builder só funciona com structs"),
    };

    // Gerar campos do builder como Option<T>
    let builder_campos = campos.iter().map(|f| {
        let nome = &f.ident;
        let tipo = &f.ty;
        quote! { #nome: Option<#tipo> }
    });

    // Gerar setters
    let setters = campos.iter().map(|f| {
        let nome = &f.ident;
        let tipo = &f.ty;
        quote! {
            pub fn #nome(mut self, val: #tipo) -> Self {
                self.#nome = Some(val);
                self
            }
        }
    });

    let expandido = quote! {
        pub struct #builder_nome {
            #( #builder_campos, )*
        }

        impl #nome {
            pub fn builder() -> #builder_nome {
                #builder_nome {
                    #( #(campos.iter().map(|f| &f.ident)): None, )*
                }
            }
        }

        impl #builder_nome {
            #( #setters )*
        }
    };

    TokenStream::from(expandido)
}

Quando Usar Macros vs Generics vs Traits

Uma dúvida comum é quando usar macros em vez de generics ou traits. Para um entendimento profundo de traits e generics, veja nosso tutorial de traits e generics.

Use Macros QuandoUse Generics/Traits Quando
Precisa gerar código variávelO comportamento segue um padrão tipado
Quer criar uma DSLA abstração é sobre tipos
Precisa de variadic argumentsOs parâmetros são fixos
Quer verificações em compile-timeRuntime dispatch é aceitável
Quer eliminar boilerplate repetitivoA composição de traits resolve

Casos Reais no Ecossistema

Macros são usadas extensivamente no ecossistema Rust. Alguns exemplos notáveis:

A crate Rayon também usa macros internamente para implementar traits de iteração paralela automaticamente.

Debugging de Macros

Depurar macros pode ser desafiador. Algumas ferramentas essenciais — que você pode integrar no seu fluxo de CI/CD:

# Expandir macros para ver o código gerado
cargo expand

# Expandir apenas um módulo específico
cargo expand nome_do_modulo

# Ver a expansão de uma macro específica
cargo expand --test nome_do_teste

O cargo expand requer instalação: cargo install cargo-expand. Para mais ferramentas do ecossistema Cargo, confira nosso artigo sobre Cargo e ferramentas essenciais.

// Outra técnica: compile_error! para debugging
macro_rules! debug_macro {
    ($($tokens:tt)*) => {
        compile_error!(concat!(
            "Tokens recebidos: ",
            stringify!($($tokens)*)
        ));
    };
}

Boas Práticas com Macros

  1. Documente extensivamente — macros são mais difíceis de entender que funções. Use a convenção de documentação Rust.

  2. Prefira funções e traits quando possível — macros devem ser o último recurso.

  3. Use $crate:: para referenciar itens da sua crate dentro de macros exportadas.

  4. Teste com trybuild para macros procedurais — verifica erros de compilação esperados.

  5. Mantenha macros pequenas — extraia lógica complexa para funções helper.

// Boa prática: macro delega para função
macro_rules! log_evento {
    ($nivel:expr, $($arg:tt)*) => {
        _log_evento_impl($nivel, &format!($($arg)*))
    };
}

fn _log_evento_impl(nivel: &str, mensagem: &str) {
    // Lógica complexa aqui — testável e debugável
    println!("[{}] {}", nivel, mensagem);
}

Conclusão

Macros em Rust são uma ferramenta essencial que, quando usada com critério, pode transformar a qualidade do seu código. Use macro_rules! para padrões repetitivos e DSLs simples, e macros procedurais com syn/quote para geração de código sofisticada.

A regra de ouro é: se uma função ou trait resolve, use-os. Macros devem ser reservadas para casos onde a metaprogramação realmente agrega valor — eliminando boilerplate significativo ou criando APIs impossíveis de outra forma.

Para continuar aprendendo sobre técnicas avançadas, explore também Go Brasil para ver como Go lida com geração de código via go generate, e Zig Brasil para ver como comptime oferece uma abordagem diferente para metaprogramação.

Veja Também