Macros procedurais em Rust: escreva seu próprio #[derive]
Toda vez que você escreve #[derive(Debug)] ou #[derive(Serialize)], um pequeno programa roda dentro do compilador. Ele olha para a sua struct e escreve um bloco impl para você. Esse pequeno programa é uma macro procedural (procedural macro), e você também pode escrever as suas.
Pense nela como um atendente muito prestativo. Você entrega um formulário (a sua struct), ele lê cada campo e devolve toda aquela papelada chata já preenchida. Na página de Macros você conheceu o macro_rules!, que funciona comparando padrões. As macros procedurais são diferentes: são funções Rust comuns que recebem código e devolvem código. Nesta página você vai construir uma de verdade, passo a passo, e ver a saída real.
Os três tipos de macros procedurais
Seção intitulada “Os três tipos de macros procedurais”Existem três tipos. Por dentro, todos funcionam do mesmo jeito, mas para quem usa eles têm cara diferente:
| Tipo | O que quem usa escreve | Exemplos famosos |
|---|---|---|
| Macro derive | #[derive(Serialize)] em cima de uma struct ou enum |
Serialize do serde, Error do thiserror, Parser do clap |
| Macro de atributo | #[route(GET, "/")] em cima de uma função ou outro item |
#[tokio::main], #[get("/")] do actix-web |
| Macro tipo função | sql!(SELECT * FROM users) |
query! do sqlx, html! do yew |
- Uma macro derive só acrescenta código ao lado do seu tipo. A sua struct continua exatamente como você escreveu.
- Uma macro de atributo substitui o item em que ela está. O
#[tokio::main]pega a suaasync fn maine transforma numafn mainnormal que inicia o runtime do tokio. - Uma macro tipo função parece uma chamada de
macro_rules!, mas quem decide o que a entrada significa é uma função Rust. É assim que osqlx::query!consegue conferir o seu SQL em tempo de compilação.
Você já usa macros derive de outros crates. Aqui estão duas delas fazendo trabalho de verdade. O Serialize escreve o código do JSON (veja JSON com Serde) e o Error escreve o código do Display a partir do texto em #[error(...)] (veja anyhow e thiserror):
use serde::Serialize;use thiserror::Error;
#[derive(Serialize)]struct Order { id: u32, item: String, quantity: u8,}
#[derive(Debug, Error)]enum ShopError { #[error("o item {0} está esgotado")] OutOfStock(String),}
fn main() { let order = Order { id: 42, item: "Bule".to_string(), quantity: 2 }; println!("{}", serde_json::to_string(&order).unwrap());
let err = ShopError::OutOfStock("Bule".to_string()); println!("{err}");}Saída:
{"id":42,"item":"Bule","quantity":2}o item Bule está esgotadoVocê nunca escreveu um impl Display nem uma linha de código JSON. Os derives escreveram tudo enquanto o seu programa compilava.
Como funcionam: entram tokens, saem tokens
Seção intitulada “Como funcionam: entram tokens, saem tokens”Primeiro, o compilador corta o seu código-fonte em tokens: nomes, pontuação, literais, parênteses e chaves. Uma macro procedural é uma função que recebe esses tokens como um TokenStream e devolve um TokenStream novo. Depois, o compilador compila o que você devolveu, como se você mesmo tivesse digitado.
Estes são os três formatos de função. Você marca cada um com um atributo, para o compilador saber de que tipo ele é:
use proc_macro::TokenStream;
// #[derive(HelloMacro)]: recebe a struct ou o enum e devolve código para ACRESCENTAR.#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { /* ... */ }
// #[route(GET, "/")]: recebe os tokens do próprio atributo e o item, e devolve um SUBSTITUTO.#[proc_macro_attribute]pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
// sql!(...): recebe tudo o que está entre os parênteses e devolve o substituto.#[proc_macro]pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }Um TokenStream cru é só uma lista de tokens, e trabalhar com isso é difícil. Por isso quase toda macro procedural usa três crates auxiliares:
- syn transforma os tokens numa árvore de sintaxe. Em vez de tokens, você recebe um
DeriveInputcom o nome do tipo, os genéricos e os campos. - quote transforma código com cara de Rust de volta em tokens. Dentro de
quote! { ... }você escreve Rust normal, e#namecola ali o valor de uma variável. - proc-macro2 é um invólucro em volta dos tipos
proc_macrodo compilador, e é ele que o syn e o quote usam. Ele também funciona fora do compilador, o que facilita funções auxiliares e testes.
Uma ideia importante: a sua macro roda em tempo de compilação. O Cargo compila primeiro o crate da macro, para o seu próprio computador, e o compilador executa a macro enquanto constrói o crate que a usa. Quando o programa finalmente roda, a macro já foi embora faz tempo. Só sobra o código que ela escreveu.
Montando o projeto
Seção intitulada “Montando o projeto”Uma macro procedural precisa morar no seu próprio crate, com um tipo de crate especial. Esse crate só pode exportar macros, não funções ou traits comuns. Por isso o formato de sempre são dois crates lado a lado, como serde e serde_derive: um para o trait e outro para o derive. Nós vamos acrescentar um terceiro crate, um programinha chamado shop que usa os dois. (Quer relembrar crates e workspaces? Veja Cargo e crates.)
mkdir procmacro-demo && cd procmacro-democargo new --lib hello_macrocargo new --lib hello_macro_derivecargo new shopEsta é a árvore de arquivos pronta:
procmacro-demo/├── Cargo.toml (o workspace)├── hello_macro/│ ├── Cargo.toml│ └── src/lib.rs (os traits e a reexportação dos derives)├── hello_macro_derive/│ ├── Cargo.toml (proc-macro = true)│ └── src/lib.rs (as macros em si)└── shop/ ├── Cargo.toml └── src/main.rs (um programa que usa #[derive(...)])O Cargo.toml da pasta principal junta os três crates num só workspace:
[workspace]members = ["hello_macro", "hello_macro_derive", "shop"]resolver = "2"O crate da macro é o único com algo especial: proc-macro = true. Ele também depende de syn, quote e proc-macro2:
[package]name = "hello_macro_derive"version = "0.1.0"edition = "2024"
[lib]proc-macro = true
[dependencies]proc-macro2 = "1"quote = "1"syn = "2"Os outros dois crates são normais. O hello_macro depende do crate do derive, e o shop só precisa do hello_macro:
[dependencies]hello_macro_derive = { version = "0.1.0", path = "../hello_macro_derive" }[dependencies]hello_macro = { version = "0.1.0", path = "../hello_macro" }Passo 1: #[derive(HelloMacro)]
Seção intitulada “Passo 1: #[derive(HelloMacro)]”Vamos começar bem pequeno. Queremos que todo tipo com #[derive(HelloMacro)] ganhe uma função hello_macro() que imprime o nome do tipo. Primeiro o trait, em hello_macro/src/lib.rs. A última linha reexporta as macros derive, assim quem usa só precisa deste crate. É exatamente o que o serde faz.
pub trait HelloMacro { fn hello_macro();}
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str];}
// Reexporta as macros derive, assim quem usa só precisa deste crate.pub use hello_macro_derive::{Describe, HelloMacro};(O Describe é para o passo 2.) Agora a macro em si, em hello_macro_derive/src/lib.rs:
use proc_macro::TokenStream;use quote::quote;use syn::{parse_macro_input, Data, DeriveInput, Fields}; // Data e Fields: passo 2
#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // 1. Transforma os tokens numa árvore de sintaxe que podemos examinar. let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident;
// 2. Monta o código novo com quote!. let generated = quote! { impl hello_macro::HelloMacro for #name { fn hello_macro() { println!("Hello, Macro! My name is {}!", stringify!(#name)); } } };
// 3. Devolve os tokens novos para o compilador. generated.into()}São três passos, e você vai ver os três em quase toda macro derive:
parse_macro_input!transforma os tokens numDeriveInput.ast.identé o nome do tipo, por exemploProduct. Se a análise falhar, essa macro já devolve um erro de compilação decente para você.quote!monta o blocoimpl.#namecola o nome ali.stringify!(#name)transforma o nome no texto"Product"..into()converte oproc_macro2::TokenStreamdo quote noproc_macro::TokenStreamdo compilador.
Repare no caminho completo hello_macro::HelloMacro. O código gerado é colado no arquivo de quem usa a macro, então ele não pode depender das linhas use dessa pessoa.
Quando alguém escreve #[derive(HelloMacro)] struct Product, o compilador acrescenta um código que funciona igualzinho a esta versão escrita à mão. Esta aqui roda no Playground, porque não tem nenhum crate de macro envolvido (deixamos o texto em inglês para bater com o projeto de exemplo):
trait HelloMacro { fn hello_macro();}
struct Product;
// É isto que o #[derive(HelloMacro)] escreve para você:impl HelloMacro for Product { fn hello_macro() { println!("Hello, Macro! My name is {}!", "Product"); }}
fn main() { Product::hello_macro();}Saída:
Hello, Macro! My name is Product!Escrever isso à mão uma vez não tem problema. Escrever para 40 tipos é quando um derive começa a valer a pena.
Passo 2: #[derive(Describe)] lê os campos
Seção intitulada “Passo 2: #[derive(Describe)] lê os campos”O HelloMacro só usava o nome do tipo. Derives de verdade, como os do serde, olham os campos. Vamos construir o Describe: ele lista os nomes dos campos de uma struct, o que é muito útil para o cabeçalho de um CSV. O syn entrega os campos por meio de ast.data:
Data::Struct,Data::EnumouData::Union: que espécie de tipo ele é.- Para uma struct,
Fields::Named(struct Product { id: u32 }),Fields::Unnamed(struct Meters(f64)) ouFields::Unit(struct Marker;).
Acrescente isto embaixo da primeira macro, em hello_macro_derive/src/lib.rs:
#[proc_macro_derive(Describe)]pub fn describe_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); expand_describe(&ast) .unwrap_or_else(|err| err.to_compile_error()) .into()}
fn expand_describe(ast: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> { let name = &ast.ident;
let Data::Struct(data) = &ast.data else { return Err(syn::Error::new_spanned( name, "Describe can only be derived for structs", )); };
let field_names: Vec<String> = match &data.fields { Fields::Named(fields) => fields .named .iter() .map(|field| field.ident.as_ref().unwrap().to_string()) .collect(), Fields::Unit => Vec::new(), Fields::Unnamed(_) => { return Err(syn::Error::new_spanned( name, "Describe needs named fields, like `struct Point { x: i32 }`", )); } };
Ok(quote! { impl hello_macro::Describe for #name { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] } } })}As peças novas:
- O trabalho de verdade acontece numa função auxiliar comum que retorna
syn::Result. Assim você pode usar?ereturn Err(...)como em qualquer outro código de tratamento de erros. A função da macro, lá em cima, só transforma um erro num erro de compilação. let ... elseresolve o caso “não é uma struct” logo de cara e mantém o caminho feliz sem aninhamento.#(#field_names),*é a repetição do quote. Ela cola cada string doVec, separadas por vírgulas. É a mesma ideia do$(...),*nomacro_rules!. CadaStringvira um literal de string, então isso fica&["id", "name", "price_cents"].field.ident.as_ref().unwrap()é seguro aqui: emFields::Named, todo campo tem nome.
Agora use os dois derives em shop/src/main.rs. Repare que use hello_macro::{Describe, HelloMacro} traz os traits e as macros derive. Eles têm o mesmo nome, mas vivem em namespaces diferentes, que é exatamente como o serde::Serialize funciona.
use hello_macro::{Describe, HelloMacro};
#[derive(HelloMacro, Describe)]struct Product { id: u32, name: String, price_cents: u64,}
#[derive(HelloMacro, Describe)]struct Customer { email: String, city: String,}
fn main() { Product::hello_macro(); Customer::hello_macro();
println!("{} has fields {:?}", Product::type_name(), Product::field_names());
// Um arquivo CSV: o cabeçalho vem do derive, as linhas vêm de valores reais. println!("{}", Customer::field_names().join(",")); println!("{},{}", ana.email, ana.city);
let teapot = Product { id: 7, name: "Teapot".to_string(), price_cents: 1999 }; println!("{} #{}: {} costs {} cents", Product::type_name(), teapot.id, teapot.name, teapot.price_cents);}Rode a partir da pasta do workspace:
cargo run -p shopSaída (numa compilação do zero):
Compiling proc-macro2 v1.0.107 Compiling unicode-ident v1.0.27 Compiling quote v1.0.47 Compiling syn v2.0.119 Compiling hello_macro_derive v0.1.0 (/home/you/procmacro-demo/hello_macro_derive) Compiling hello_macro v0.1.0 (/home/you/procmacro-demo/hello_macro) Compiling shop v0.1.0 (/home/you/procmacro-demo/shop) Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.45s Running `target/debug/shop`Hello, Macro! My name is Product!Hello, Macro! My name is Customer!Product has fields ["id", "name", "price_cents"]email,city[email protected],LisbonProduct #7: Teapot costs 1999 centsAs linhas Compiling, Finished e Running são do próprio Cargo, que sempre escreve em inglês. Repare na ordem: o Cargo compila o syn e o hello_macro_derive antes do shop, porque o compilador precisa executar a sua macro enquanto compila o shop. Se você acrescentar um campo em Customer, o cabeçalho do CSV se atualiza sozinho na próxima compilação.
Boas mensagens de erro
Seção intitulada “Boas mensagens de erro”O que acontece se alguém colocar #[derive(Describe)] em cima de um enum? Como expand_describe retorna um syn::Error, quem usa a macro recebe um erro do compilador com cara normal. (Colocamos os exemplos quebrados desta página em shop/src/bin/broken.rs; é por isso que esse caminho aparece.)
use hello_macro::Describe;
#[derive(Describe)]enum OrderStatus { Pending, Shipped,}error: Describe can only be derived for structs --> shop/src/bin/broken.rs:4:6 |4 | enum OrderStatus { | ^^^^^^^^^^^A mensagem é o texto que escrevemos na macro (“Describe só pode ser derivado para structs”). Duas coisas deixam esse erro bom:
syn::Error::new_spanned(name, ...)lembra onde o nome está no arquivo do usuário (o span dele, a posição no código). É por isso que os^^^^^apontam paraOrderStatus, e não para alguma linha dentro da sua macro.to_compile_error()transforma o erro numa chamadacompile_error!("..."). Aí o compilador mostra esse erro como qualquer outro. Você também pode usar essa mesma macrocompile_error!em código comum.
Uma tuple struct recebe a outra mensagem (“Describe precisa de campos com nome”), também apontando para o lugar certo:
error: Describe needs named fields, like `struct Point { x: i32 }` --> shop/src/bin/broken.rs:4:8 |4 | struct Meters(f64); | ^^^^^^Escreva as suas mensagens pensando na pessoa que usa a macro. Diga o que está errado e o que funcionaria no lugar.
Vendo o código gerado com cargo expand
Seção intitulada “Vendo o código gerado com cargo expand”Macros podem parecer mágica, e mágica é difícil de depurar. O cargo-expand mostra o seu código depois que todas as macros rodaram. Instale uma vez (em Ferramentas do Rust você encontra mais ferramentas úteis):
cargo install cargo-expandcargo expand -p shopParte do que ele imprime para o nosso projeto:
impl hello_macro::Describe for Product { fn type_name() -> &'static str { "Product" } fn field_names() -> &'static [&'static str] { &["id", "name", "price_cents"] }}impl hello_macro::Describe for Customer { fn type_name() -> &'static str { "Customer" } fn field_names() -> &'static [&'static str] { &["email", "city"] }}Aí está o seu bloco impl, com os nomes dos campos já preenchidos e o stringify!(#name) já transformado em "Product". Quando um derive não faz o que você espera, rode cargo expand primeiro. Ele também expande o println! e outras macros, então procure os seus próprios blocos impl.
Testando a sua macro
Seção intitulada “Testando a sua macro”Um crate de macro não consegue testar a si mesmo com facilidade, porque ele só pode exportar macros. A solução simples é escrever testes normais num crate que usa a macro. Coloque isto em hello_macro/tests/describe.rs e rode cargo test -p hello_macro:
use hello_macro::Describe;
#[allow(dead_code)]#[derive(Describe)]struct Product { id: u32, name: String,}
#[test]fn lists_field_names_in_order() { assert_eq!(Product::type_name(), "Product"); assert_eq!(Product::field_names(), ["id", "name"]);}running 1 testtest lists_field_names_in_order ... okMas como testar se o caso do enum dá um erro bom? Um teste normal não consegue, porque o arquivo de teste nem compilaria. É para isso que existe o crate trybuild. Você coloca pequenos arquivos que “devem falhar” numa pasta, e o trybuild compila cada um e compara o erro do compilador com um arquivo .stderr salvo:
#[test]fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui/*.rs");}A maioria dos crates de macro populares, incluindo o serde e o thiserror, testa as suas mensagens de erro desse jeito.
Quando vale a pena escrever uma?
Seção intitulada “Quando vale a pena escrever uma?”Macros procedurais são poderosas, mas têm um custo. Seja honesto com você mesmo antes de escrever uma:
- Tempo de compilação. O syn é um crate grande. A primeira compilação de todo projeto que usa a sua macro precisa compilá-lo. (As features padrão do syn bastam para derives. Você só precisa de
features = ["full"]para analisar coisas como o corpo de uma função, e isso deixa tudo mais lento.) - Complexidade. Agora você tem um crate a mais, código que escreve código e erros mais difíceis de acompanhar.
- Ferramentas. Os editores e o rust-analyzer entendem código comum melhor do que código que sai de uma macro.
Uma boa ordem para tentar:
- Uma função, um trait com um método padrão ou genéricos. A maioria dos problemas de “código repetitivo” termina aqui.
- Uma macro
macro_rules!(veja Macros em Rust). Ela não precisa de nenhum crate extra. - Uma macro procedural, quando o código depende da estrutura de um tipo (o nome, os campos ou as variantes), muitos tipos precisam dele e outras pessoas vão usar a macro. É por isso que o serde, o thiserror e o clap usam derives.
Para comparar, aqui está o Describe feito com macro_rules!. Funciona e não precisa de crate extra, mas a struct tem que ser escrita dentro da chamada da macro. Isso fica desajeitado bem rápido, por exemplo se você também quiser comentários de documentação, campos pub ou outros derives.
trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str];}
macro_rules! describe_struct { (struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => { struct $name { $($field: $ty),* }
impl Describe for $name { fn type_name() -> &'static str { stringify!($name) } fn field_names() -> &'static [&'static str] { &[$(stringify!($field)),*] } } };}
describe_struct!(struct Customer { email: String, city: String });
fn main() { println!("{} has fields {:?}", Customer::type_name(), Customer::field_names()); println!("{},{}", ana.email, ana.city);}Saída:
Customer has fields ["email", "city"][email protected],LisbonPara um ou dois tipos dentro do seu próprio programa, isso (ou simplesmente escrever o impl à mão) costuma ser a melhor escolha. Um derive é mais confortável para quem usa: a pessoa mantém a struct normal e só acrescenta uma linha.
Erros comuns
Seção intitulada “Erros comuns”Esquecer o proc-macro = true. Sem a seção [lib] no Cargo.toml do crate da macro, o crate é uma biblioteca comum, e o compilador se recusa:
error: the `#[proc_macro_derive]` attribute is only usable with crates of the `proc-macro` crate type --> hello_macro_derive/src/lib.rs:5:1 |5 | #[proc_macro_derive(HelloMacro)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0432]: unresolved import `proc_macro`O compilador está dizendo que #[proc_macro_derive] só pode ser usado em crates do tipo proc-macro, e por isso ele também não encontra o crate proc_macro.
Usar panic! ou unwrap() para erros de quem usa a macro. “Funciona”, mas o erro fica vago e aponta para o #[derive] inteiro, não para o problema:
error: proc-macro derive panicked --> shop/src/bin/broken.rs:3:10 |3 | #[derive(Describe)] | ^^^^^^^^ | = help: message: Describe can only be derived for structsAqui o compilador só diz que a macro derive entrou em panic e mostra a mensagem como uma dica. Em vez disso, retorne um syn::Error com new_spanned, como no passo 2.
Caminhos curtos no código gerado. Se você escrever impl Describe for #name em vez de impl hello_macro::Describe for #name, só funciona quando quem usa a macro, por acaso, importou o Describe. Quem escrever #[derive(hello_macro::Describe)] recebe isto:
error[E0405]: cannot find trait `Describe` in this scope --> shop/src/bin/broken.rs:1:10 |1 | #[derive(hello_macro::Describe)] | ^^^^^^^^^^^^^^^^^^^^^ not found in this scope | = note: this error originates in the derive macro `hello_macro::Describe`Ou seja: o compilador não encontra o trait Describe nesse escopo. Use sempre caminhos completos dentro do quote!: hello_macro::Describe, ::std::fmt::Display, ::core::option::Option.
Prática
Seção intitulada “Prática”Estes exercícios mexem no projeto de verdade, então não têm botão “Experimente você mesmo”. Monte o projeto desta página no seu próprio computador e rode cargo run -p shop.
1. Conte os campos
Seção intitulada “1. Conte os campos”Acrescente um terceiro método, fn field_count() -> usize, ao trait Describe e ao derive. Product::field_count() deve retornar 3. Dica: você já tem o vetor field_names. Guarde field_names.len() numa variável antes do quote! e cole com #.
Ver solução
Em hello_macro/src/lib.rs, acrescente o método ao trait:
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str]; fn field_count() -> usize;}Em expand_describe, conte os campos e acrescente o método ao impl gerado:
let count = field_names.len();
Ok(quote! { impl hello_macro::Describe for #name { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] }
fn field_count() -> usize { #count } } })Depois, println!("{} has {} fields", Product::type_name(), Product::field_count()); no main imprime:
Product has 3 fieldsA contagem acontece na sua macro, em tempo de compilação. O cargo expand mostra que o resultado é só fn field_count() -> usize { 3usize }, uma constante, sem nenhum trabalho sobrando para a hora da execução.
2. Aceite structs genéricas
Seção intitulada “2. Aceite structs genéricas”Experimente #[derive(Describe)] numa struct genérica como struct Page<T> { items: Vec<T>, number: u32 }. Dá erro: error[E0107]: missing generics for struct Page (faltam os genéricos), porque o código gerado diz impl hello_macro::Describe for Page e esquece o <T>. Conserte o derive para que structs genéricas funcionem. Dica: ast.generics.split_for_impl() devolve três pedaços para colar na linha do impl. Em Genéricos em Rust você relembra como fica um impl<T>.
Ver solução
Em expand_describe, separe os genéricos e use os três pedaços na linha do impl:
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
Ok(quote! { impl #impl_generics hello_macro::Describe for #name #ty_generics #where_clause { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] } } })Agora isto funciona no shop:
#[derive(Describe)]struct Page<T> { items: Vec<T>, number: u32,}
fn main() { let page = Page { items: vec!["a", "b"], number: 1 }; println!("{} {} {:?}", page.items.len(), page.number, Page::<String>::field_names());}2 1 ["items", "number"]impl_generics é <T> (com os bounds, se houver), ty_generics é <T> e where_clause é a parte where ..., se existir. Todo derive sério faz isso, então guarde o nome split_for_impl.
- Uma macro procedural é uma função Rust que roda em tempo de compilação: entra um
TokenStream, sai outroTokenStream. - Existem três tipos: derive (
#[derive(Serialize)]), de atributo (#[tokio::main]) e tipo função (sql!(...)). - Elas moram no seu próprio crate, com
proc-macro = true. Normalmente um crate principal as reexporta junto com os traits. - O syn analisa a entrada (
DeriveInput,Data,Fields) e o quote escreve a saída (#name,#(...),*). - Informe os erros de quem usa a macro com
syn::Error::new_spanned(...).to_compile_error(), confira o resultado comcargo expande teste os erros com o trybuild. - Tente primeiro uma função, um trait ou
macro_rules!. Uma macro procedural vale a pena quando muitos tipos precisam de código baseado na sua estrutura.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Quando o código de uma macro procedural roda?
2. Que tipo de macro procedural é o #[tokio::main]?
3. O que o syn e o quote fazem?
4. O seu derive deve recusar enums. Qual é o melhor jeito?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- Procedural Macros — The Rust Book (em inglês): o passo a passo oficial de um derive
HelloMacro, além das macros de atributo e tipo função. - Procedural Macros — The Rust Reference (em inglês): as regras exatas dos três tipos, os tipos de crate, os spans e a higiene.
- Documentação do syn (em inglês):
DeriveInput,Data,Fields,Errore as funções de análise usadas nesta página. - Documentação do quote (em inglês): como funcionam a interpolação com
#vare a repetição com#(...),*dentro doquote!. - proc-macro-workshop (em inglês): projetos práticos e gratuitos do David Tolnay (um derive builder, um derive de debug e mais), com testes que guiam você passo a passo.