No Artigo #21 exploramos macros declarativas com macro_rules! — padrões que transformam código em código. Macros procedurais são o próximo nível: programas Rust que recebem tokens como entrada e produzem tokens como saída, executando em tempo de compilação.
Quando você escreve #[derive(Serialize)] ou #[tokio::main], está usando macros procedurais. Elas permitem eliminar boilerplate que seria impossível com macro_rules!, gerar código com base na estrutura de tipos, e criar DSLs (Domain Specific Languages) que parecem parte natural da linguagem.
Os três tipos de macros procedurais
// 1. Derive macros — adicionam implementações a structs/enums
#[derive(MinhaDerive)]
struct MeuTipo { ... }
// 2. Attribute macros — transformam o item anotado
#[meu_atributo(opcao = "valor")]
fn minha_funcao() { ... }
// 3. Function-like macros — chamadas como funções mas com poder de macro
minha_macro!(conteudo arbitrario aqui)
Estrutura de um crate de macros procedurais
Macros procedurais precisam ficar em um crate separado com proc-macro = true:
meu_projeto/
├── Cargo.toml
├── src/
│ └── main.rs
└── meu_derive/ ← crate de macros procedurais
├── Cargo.toml
└── src/
└── lib.rs
meu_derive/Cargo.toml:
[package]
name = "meu_derive"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true # obrigatório
[dependencies]
proc-macro2 = "1" # versão melhorada de proc_macro
quote = "1" # geração de código TokenStream
syn = { version = "2", features = ["full"] } # parsing de AST Rust
Cargo.toml do projeto principal:
[dependencies]
meu_derive = { path = "./meu_derive" }
As ferramentas essenciais
Três crates formam o ecossistema de macros procedurais:
syn — parseia código Rust em uma AST (Abstract Syntax Tree). Transforma TokenStream em estruturas como DeriveInput, ItemFn, Type, que você pode inspecionar e transformar.
quote — gera código Rust a partir de templates. O macro quote! { ... } interpola variáveis Rust em código Rust, retornando um TokenStream.
proc-macro2 — versão melhorada do proc_macro da stdlib, com suporte a testes unitários e melhor ergonomia.
Derive macro: Descrever
Vamos criar uma derive macro que implementa automaticamente um método descrever() para qualquer struct:
meu_derive/src/lib.rs:
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields, FieldsNamed};
#[proc_macro_derive(Descrever)]
pub fn derive_descrever(input: TokenStream) -> TokenStream {
// Parseia o input como DeriveInput (struct/enum/union)
let input = parse_macro_input!(input as DeriveInput);
// Gera o código ou retorna erro
match impl_descrever(&input) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn impl_descrever(input: &DeriveInput) -> syn::Result<TokenStream2> {
let nome = &input.ident;
// Obtém os campos da struct
let campos = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(campos) => campos,
_ => return Err(syn::Error::new_spanned(
nome,
"Descrever suporta apenas structs com campos nomeados"
)),
},
_ => return Err(syn::Error::new_spanned(
nome,
"Descrever suporta apenas structs"
)),
};
// Gera linhas de descrição para cada campo
let linhas_campos = campos.named.iter().map(|campo| {
let nome_campo = campo.ident.as_ref().unwrap();
let nome_str = nome_campo.to_string();
quote! {
// Para cada campo, chama format! com seu valor
partes.push(format!(" {}: {:?}", #nome_str, self.#nome_campo));
}
});
let nome_str = nome.to_string();
// Gera a implementação completa
let codigo_gerado = quote! {
impl #nome {
pub fn descrever(&self) -> String {
let mut partes = Vec::new();
partes.push(format!("{}:", #nome_str));
#(#linhas_campos)*
partes.join("
")
}
}
};
Ok(codigo_gerado)
}
Uso:
use meu_derive::Descrever;
#[derive(Descrever, Debug)]
struct Usuario {
nome: String,
email: String,
idade: u32,
ativo: bool,
}
#[derive(Descrever, Debug)]
struct Produto {
id: u64,
nome: String,
preco: f64,
estoque: i32,
}
fn main() {
let usuario = Usuario {
nome: "Ana Silva".to_string(),
email: "ana@exemplo.com".to_string(),
idade: 30,
ativo: true,
};
println!("{}", usuario.descrever());
// Usuario:
// nome: "Ana Silva"
// email: "ana@exemplo.com"
// idade: 30
// ativo: true
let produto = Produto {
id: 1,
nome: "Teclado Mecânico".to_string(),
preco: 350.99,
estoque: 15,
};
println!("{}", produto.descrever());
}
Derive macro com atributos de campo
Vamos criar uma derive macro Builder que gera um builder pattern automaticamente, com suporte a atributos de configuração:
meu_derive/src/lib.rs (adição):
use syn::{Attribute, Lit, Meta, NameValue};
// Estrutura para guardar configuração de cada campo
struct ConfigCampo {
nome: syn::Ident,
tipo: syn::Type,
obrigatorio: bool,
valor_padrao: Option<proc_macro2::TokenStream>,
}
fn extrair_config_campo(campo: &syn::Field) -> ConfigCampo {
let nome = campo.ident.clone().unwrap();
let tipo = campo.ty.clone();
let mut obrigatorio = true;
let mut valor_padrao = None;
// Procura atributo #[builder(padrao = "...")]
for attr in &campo.attrs {
if attr.path().is_ident("builder") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("padrao") {
let valor: syn::LitStr = meta.value()?.parse()?;
valor_padrao = Some(valor.value().parse().unwrap());
obrigatorio = false;
}
if meta.path.is_ident("opcional") {
obrigatorio = false;
}
Ok(())
});
}
}
ConfigCampo { nome, tipo, obrigatorio, valor_padrao }
}
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match impl_builder(&input) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn impl_builder(input: &DeriveInput) -> syn::Result<TokenStream2> {
let nome = &input.ident;
let nome_builder = syn::Ident::new(
&format!("{nome}Builder"),
nome.span(),
);
let campos = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(f) => &f.named,
_ => return Err(syn::Error::new_spanned(
nome, "Builder requer campos nomeados"
)),
},
_ => return Err(syn::Error::new_spanned(
nome, "Builder requer struct"
)),
};
let configs: Vec<ConfigCampo> = campos.iter()
.map(extrair_config_campo)
.collect();
// Campos do Builder (todos Option<T>)
let campos_builder = configs.iter().map(|c| {
let nome = &c.nome;
let tipo = &c.tipo;
quote! { #nome: Option<#tipo> }
});
// Inicialização do Builder
let init_campos = configs.iter().map(|c| {
let nome = &c.nome;
if let Some(ref padrao) = c.valor_padrao {
quote! { #nome: Some(#padrao) }
} else {
quote! { #nome: None }
}
});
// Métodos setter
let setters = configs.iter().map(|c| {
let nome = &c.nome;
let tipo = &c.tipo;
quote! {
pub fn #nome(mut self, valor: #tipo) -> Self {
self.#nome = Some(valor);
self
}
}
});
// Método build() com validação
let validacoes = configs.iter()
.filter(|c| c.obrigatorio)
.map(|c| {
let nome = &c.nome;
let nome_str = nome.to_string();
quote! {
let #nome = self.#nome.ok_or_else(||
format!("Campo obrigatório ausente: {}", #nome_str)
)?;
}
});
let campos_obrigatorios: Vec<_> = configs.iter()
.filter(|c| c.obrigatorio)
.map(|c| &c.nome)
.collect();
let campos_opcionais: Vec<_> = configs.iter()
.filter(|c| !c.obrigatorio)
.map(|c| &c.nome)
.collect();
let construcao_opcionais = configs.iter()
.filter(|c| !c.obrigatorio)
.map(|c| {
let nome = &c.nome;
if let Some(ref padrao) = c.valor_padrao {
quote! { #nome: self.#nome.unwrap_or(#padrao) }
} else {
quote! { #nome: self.#nome.unwrap_or_default() }
}
});
let codigo = quote! {
pub struct #nome_builder {
#(#campos_builder,)*
}
impl #nome_builder {
pub fn novo() -> Self {
#nome_builder {
#(#init_campos,)*
}
}
#(#setters)*
pub fn build(self) -> Result<#nome, String> {
#(#validacoes)*
Ok(#nome {
#(#campos_obrigatorios,)*
#(#construcao_opcionais,)*
})
}
}
impl #nome {
pub fn builder() -> #nome_builder {
#nome_builder::novo()
}
}
};
Ok(codigo)
}
Uso:
use meu_derive::Builder;
#[derive(Builder, Debug)]
struct ConfiguracaoServidor {
// Campos obrigatórios
host: String,
porta: u16,
// Campos com valores padrão
#[builder(padrao = "10")]
max_conexoes: usize,
#[builder(padrao = "30")]
timeout_segundos: u64,
#[builder(padrao = "false")]
debug: bool,
}
fn main() {
// Uso fluente do builder gerado
let config = ConfiguracaoServidor::builder()
.host("0.0.0.0".to_string())
.porta(8080)
.max_conexoes(100)
.debug(true)
.build()
.unwrap();
println!("{:?}", config);
// Sem campos opcionais — usa os padrões
let config_minima = ConfiguracaoServidor::builder()
.host("localhost".to_string())
.porta(3000)
.build()
.unwrap();
println!("{:?}", config_minima);
// Erro — campo obrigatório ausente
let resultado = ConfiguracaoServidor::builder()
.porta(8080)
.build();
println!("Sem host: {:?}", resultado);
// Err("Campo obrigatório ausente: host")
}
Attribute macro: cronometrar
Attribute macros transformam o item que anotam. Vamos criar #[cronometrar] que envolve qualquer função com medição de tempo:
meu_derive/src/lib.rs (adição):
use syn::{ItemFn, parse_macro_input};
#[proc_macro_attribute]
pub fn cronometrar(args: TokenStream, input: TokenStream) -> TokenStream {
let funcao = parse_macro_input!(input as ItemFn);
match impl_cronometrar(args.into(), funcao) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn impl_cronometrar(
_args: TokenStream2,
funcao: ItemFn,
) -> syn::Result<TokenStream2> {
let nome = &funcao.sig.ident;
let nome_str = nome.to_string();
let visibilidade = &funcao.vis;
let assinatura = &funcao.sig;
let corpo = &funcao.block;
let atributos = &funcao.attrs;
// Verifica se é async
let e_async = funcao.sig.asyncness.is_some();
let codigo_cronometrado = if e_async {
quote! {
#(#atributos)*
#visibilidade #assinatura {
let __inicio = ::std::time::Instant::now();
let __resultado = async move #corpo .await;
let __duracao = __inicio.elapsed();
println!(
"[cronometrar] {} concluída em {:?}",
#nome_str,
__duracao
);
__resultado
}
}
} else {
quote! {
#(#atributos)*
#visibilidade #assinatura {
let __inicio = ::std::time::Instant::now();
let __resultado = (|| #corpo)();
let __duracao = __inicio.elapsed();
println!(
"[cronometrar] {} concluída em {:?}",
#nome_str,
__duracao
);
__resultado
}
}
};
Ok(codigo_cronometrado)
}
Uso:
use meu_derive::cronometrar;
#[cronometrar]
fn calcular_primos(limite: u64) -> Vec<u64> {
(2..=limite)
.filter(|&n| (2..=(n as f64).sqrt() as u64)
.all(|i| n % i != 0))
.collect()
}
#[cronometrar]
fn ordenar_grande(mut dados: Vec<i32>) -> Vec<i32> {
dados.sort_unstable();
dados
}
// Também funciona com funções async
#[cronometrar]
async fn buscar_dados() -> String {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
"dados retornados".to_string()
}
fn main() {
let primos = calcular_primos(100_000);
println!("Encontrados {} primos", primos.len());
// [cronometrar] calcular_primos concluída em 15.234ms
let dados: Vec<i32> = (0..100_000).rev().collect();
let ordenado = ordenar_grande(dados);
println!("Ordenados {} elementos", ordenado.len());
// [cronometrar] ordenar_grande concluída em 4.891ms
}
Function-like macro: sql!
Macros function-like têm a mesma sintaxe de chamada que macros declarativas mas com o poder das procedurais:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let tokens: Vec<_> = input.into_iter().collect();
// Extrai o SQL como string literal
let sql_str = match tokens.first() {
Some(proc_macro::TokenTree::Literal(lit)) => {
let s = lit.to_string();
// Remove aspas
s.trim_matches('"').to_string()
}
_ => {
return syn::Error::new(
proc_macro2::Span::call_site(),
"sql! espera uma string literal"
)
.to_compile_error()
.into()
}
};
// Validações em tempo de compilação
let sql_upper = sql_str.to_uppercase();
// Verifica palavras-chave perigosas
let avisos: Vec<&str> = [
("DROP TABLE", "Operação destrutiva: DROP TABLE detectada"),
("DELETE FROM", "DELETE sem WHERE pode remover todos os dados"),
("TRUNCATE", "TRUNCATE remove todos os dados da tabela"),
]
.iter()
.filter(|(palavra, _)| sql_upper.contains(palavra))
.map(|(_, aviso)| *aviso)
.collect();
// Contagem de parâmetros ($1, $2, ...)
let mut num_params = 0usize;
let mut i = 1;
while sql_str.contains(&format!("${i}")) {
num_params = i;
i += 1;
}
let avisos_code = avisos.iter().map(|aviso| {
quote! { compile_error!(#aviso); }
});
let codigo = quote! {
{
#(#avisos_code)*
struct ConsultaValidada {
sql: &'static str,
num_parametros: usize,
}
ConsultaValidada {
sql: #sql_str,
num_parametros: #num_params,
}
}
};
codigo.into()
}
Uso:
use meu_derive::sql;
fn main() {
let consulta = sql!("SELECT id, nome FROM usuarios WHERE id = $1");
println!("SQL: {}", consulta.sql);
println!("Parâmetros: {}", consulta.num_parametros);
// Erro em tempo de compilação:
// let perigosa = sql!("DROP TABLE usuarios");
// error: Operação destrutiva: DROP TABLE detectada
}
Testando macros procedurais
# Em meu_derive/Cargo.toml
[dev-dependencies]
trybuild = "1" # testa erros de compilação
prettyplease = "0.2" # formata código gerado para legibilidade
// meu_derive/tests/testes_builder.rs
#[test]
fn builder_campos_obrigatorios() {
// trybuild verifica que o código compila/falha como esperado
let t = trybuild::TestCases::new();
t.pass("tests/casos/builder_basico.rs");
t.compile_fail("tests/casos/builder_sem_obrigatorio.rs");
}
// tests/casos/builder_basico.rs
use meu_derive::Builder;
#[derive(Builder, Debug)]
struct Ponto {
x: f64,
y: f64,
}
fn main() {
let p = Ponto::builder()
.x(1.0)
.y(2.0)
.build()
.unwrap();
assert_eq!(p.x, 1.0);
assert_eq!(p.y, 2.0);
}
// tests/casos/builder_sem_obrigatorio.rs
use meu_derive::Builder;
#[derive(Builder, Debug)]
struct Ponto {
x: f64,
y: f64,
}
fn main() {
// Deve resultar em Err em tempo de execução, não compilação
// mas o tipo do erro deve ser verificável
let resultado = Ponto::builder().x(1.0).build();
assert!(resultado.is_err());
}
Inspecionar código gerado com cargo expand
cargo install cargo-expand
# Expande todas as macros e mostra o código resultante
cargo expand
# Expande apenas um módulo específico
cargo expand meu_modulo
Saída para #[derive(Builder)] struct Config { ... }:
// Código gerado e expandido:
pub struct ConfigBuilder {
host: Option<String>,
porta: Option<u16>,
max_conexoes: Option<usize>,
// ...
}
impl ConfigBuilder {
pub fn novo() -> Self {
ConfigBuilder {
host: None,
porta: None,
max_conexoes: Some(10usize),
// ...
}
}
pub fn host(mut self, valor: String) -> Self {
self.host = Some(valor);
self
}
// ...
pub fn build(self) -> Result<Config, String> {
let host = self.host.ok_or_else(|| {
format!("Campo obrigatório ausente: {}", "host")
})?;
// ...
}
}
Um exemplo completo: derive Validar
Vamos criar uma macro que gera validação automática baseada em atributos de campo:
// meu_derive/src/lib.rs — macro Validar
#[proc_macro_derive(Validar, attributes(validar))]
pub fn derive_validar(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match impl_validar(&input) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
struct RegraValidacao {
campo: syn::Ident,
tipo: syn::Type,
regras: Vec<Regra>,
}
enum Regra {
NaoVazio,
MinLen(usize),
MaxLen(usize),
Min(f64),
Max(f64),
Email,
Regex(String),
}
fn extrair_regras(campo: &syn::Field) -> RegraValidacao {
let nome = campo.ident.clone().unwrap();
let tipo = campo.ty.clone();
let mut regras = Vec::new();
for attr in &campo.attrs {
if !attr.path().is_ident("validar") {
continue;
}
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("nao_vazio") {
regras.push(Regra::NaoVazio);
} else if meta.path.is_ident("email") {
regras.push(Regra::Email);
} else if meta.path.is_ident("min_len") {
let valor: syn::LitInt = meta.value()?.parse()?;
regras.push(Regra::MinLen(valor.base10_parse()?));
} else if meta.path.is_ident("max_len") {
let valor: syn::LitInt = meta.value()?.parse()?;
regras.push(Regra::MaxLen(valor.base10_parse()?));
} else if meta.path.is_ident("min") {
let valor: syn::LitFloat = meta.value()?.parse()?;
regras.push(Regra::Min(valor.base10_parse()?));
} else if meta.path.is_ident("max") {
let valor: syn::LitFloat = meta.value()?.parse()?;
regras.push(Regra::Max(valor.base10_parse()?));
}
Ok(())
});
}
RegraValidacao { campo: nome, tipo, regras }
}
fn gerar_verificacao(regra: &RegraValidacao) -> TokenStream2 {
let campo = ®ra.campo;
let campo_str = campo.to_string();
let verificacoes = regra.regras.iter().map(|r| match r {
Regra::NaoVazio => quote! {
if self.#campo.is_empty() {
erros.push(format!("{}: não pode ser vazio", #campo_str));
}
},
Regra::MinLen(n) => quote! {
if self.#campo.len() < #n {
erros.push(format!(
"{}: mínimo {} caracteres (tem {})",
#campo_str, #n, self.#campo.len()
));
}
},
Regra::MaxLen(n) => quote! {
if self.#campo.len() > #n {
erros.push(format!(
"{}: máximo {} caracteres (tem {})",
#campo_str, #n, self.#campo.len()
));
}
},
Regra::Min(n) => quote! {
if (self.#campo as f64) < #n {
erros.push(format!(
"{}: valor mínimo é {} (tem {})",
#campo_str, #n, self.#campo
));
}
},
Regra::Max(n) => quote! {
if (self.#campo as f64) > #n {
erros.push(format!(
"{}: valor máximo é {} (tem {})",
#campo_str, #n, self.#campo
));
}
},
Regra::Email => quote! {
if !self.#campo.contains('@') || !self.#campo.contains('.') {
erros.push(format!("{}: email inválido", #campo_str));
}
},
Regra::Regex(_) => quote! {},
});
quote! { #(#verificacoes)* }
}
fn impl_validar(input: &DeriveInput) -> syn::Result<TokenStream2> {
let nome = &input.ident;
let campos = match &input.data {
Data::Struct(d) => match &d.fields {
Fields::Named(f) => &f.named,
_ => return Err(syn::Error::new_spanned(
nome, "Validar requer campos nomeados"
)),
},
_ => return Err(syn::Error::new_spanned(
nome, "Validar requer struct"
)),
};
let regras: Vec<_> = campos.iter()
.map(extrair_regras)
.collect();
let verificacoes = regras.iter().map(gerar_verificacao);
let codigo = quote! {
impl #nome {
pub fn validar(&self) -> Result<(), Vec<String>> {
let mut erros: Vec<String> = Vec::new();
#(#verificacoes)*
if erros.is_empty() {
Ok(())
} else {
Err(erros)
}
}
pub fn e_valido(&self) -> bool {
self.validar().is_ok()
}
}
};
Ok(codigo)
}
Uso:
use meu_derive::Validar;
#[derive(Validar, Debug)]
struct FormularioCadastro {
#[validar(nao_vazio, min_len = 2, max_len = 100)]
nome: String,
#[validar(nao_vazio, email)]
email: String,
#[validar(min_len = 8, max_len = 72)]
senha: String,
#[validar(min = 18.0, max = 120.0)]
idade: u32,
}
fn main() {
let formulario_valido = FormularioCadastro {
nome: "Ana Silva".to_string(),
email: "ana@exemplo.com".to_string(),
senha: "MinhaS3nh@".to_string(),
idade: 30,
};
match formulario_valido.validar() {
Ok(()) => println!("✓ Formulário válido"),
Err(erros) => println!("✗ Erros: {:?}", erros),
}
let formulario_invalido = FormularioCadastro {
nome: "A".to_string(), // muito curto
email: "email_invalido".to_string(), // sem @
senha: "123".to_string(), // muito curta
idade: 15, // menor de 18
};
match formulario_invalido.validar() {
Ok(()) => println!("✓ Válido"),
Err(erros) => {
println!("✗ {} erro(s) encontrado(s):", erros.len());
for erro in &erros {
println!(" • {erro}");
}
}
}
// ✗ 4 erro(s) encontrado(s):
// • nome: mínimo 2 caracteres (tem 1)
// • email: email inválido
// • senha: mínimo 8 caracteres (tem 3)
// • idade: valor mínimo é 18 (tem 15)
}
Mensagens de erro de qualidade
Macros procedurais bem feitas produzem erros compreensíveis:
// Erro com span preciso — aponta para o local exato do problema
fn verificar_tipo_compativel(
campo: &syn::Field
) -> syn::Result<()> {
let tipo = &campo.ty;
// Verifica se o tipo é String ou &str para validar .is_empty()
let tipo_str = quote!(#tipo).to_string();
if !["String", "str"].iter().any(|t| tipo_str.contains(t)) {
return Err(syn::Error::new_spanned(
tipo,
format!(
"Validar(nao_vazio) requer tipo String ou &str, mas encontrou: {}",
tipo_str
)
));
}
Ok(())
}
// O erro aparece assim no compilador:
// error: Validar(nao_vazio) requer tipo String ou &str, mas encontrou: u32
// --> src/main.rs:8:5
// |
// 8 | #[validar(nao_vazio)]
// | ^^^^^^^^^^^^^^^^^^^^^
// 9 | idade: u32,
// | ^^^
Quando usar macros procedurais
Macros procedurais são ferramentas poderosas mas com custo real: aumentam o tempo de compilação, tornam o código mais difícil de depurar, e adicionam complexidade ao projeto.
Use-as quando você precisa eliminar boilerplate repetitivo que traits ou generics não conseguem resolver, quando quer fazer verificações semânticas em tempo de compilação, ou quando está criando uma DSL que melhora significativamente a expressividade.
Não use quando um trait bem projetado resolve o problema, quando generics são suficientes, ou quando a macro economizaria apenas algumas linhas — o custo de manutenção raramente vale para economias pequenas.
Fontes e leituras recomendadas
- "Procedural Macros" — The Rust Reference — especificação oficial — https://doc.rust-lang.org/reference/procedural-macros.html
syndocumentation — parsing de AST Rust — https://docs.rs/synquotedocumentation — geração de código — https://docs.rs/quoteproc-macro2documentation — https://docs.rs/proc-macro2- "proc-macro-workshop" — David Tolnay — exercícios práticos excelentes — https://github.com/dtolnay/proc-macro-workshop
trybuildcrate — testes de macros com casos de erro — https://docs.rs/trybuild- "Macros in Rust: A tutorial with examples" — LogRocket — https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/
- Serde source code — a macro procedural mais estudada do ecossistema — https://github.com/serde-rs/serde