Macro procedurali in Rust: scrivi il tuo #[derive]
Ogni volta che scrivi #[derive(Debug)] o #[derive(Serialize)], dentro il compilatore gira un piccolo programma. Guarda la tua struct e scrive per te un blocco impl. Quel piccolo programma è una macro procedurale, e puoi scriverne una tua.
Immaginalo come un impiegato servizievole. Gli consegni un modulo (la tua struct), lui legge ogni campo e ti restituisce le pratiche noiose già compilate. Nella pagina sulle Macro hai conosciuto macro_rules!, che funziona riconoscendo dei pattern. Le macro procedurali sono diverse: sono normali funzioni Rust che ricevono codice e restituiscono codice. In questa pagina ne costruirai una vera, passo per passo, e vedrai l’output reale.
I tre tipi di macro procedurali
Sezione intitolata “I tre tipi di macro procedurali”Ce ne sono di tre tipi. Dentro funzionano tutti allo stesso modo, ma per chi li usa hanno un aspetto diverso:
| Tipo | Cosa scrive chi la usa | Esempi famosi |
|---|---|---|
| Macro derive | #[derive(Serialize)] su una struct o un enum |
Serialize di serde, Error di thiserror, Parser di clap |
| Macro attributo | #[route(GET, "/")] su una funzione o un elemento |
#[tokio::main], #[get("/")] di actix-web |
| Macro function-like | sql!(SELECT * FROM users) |
query! di sqlx, html! di yew |
- Una macro derive si limita ad aggiungere codice accanto al tuo tipo. La tua struct resta esattamente come l’hai scritta.
- Una macro attributo sostituisce l’elemento su cui si trova.
#[tokio::main]prende il tuoasync fn maine lo trasforma in un normalefn mainche avvia il runtime di tokio. - Una macro function-like (simile a una funzione) sembra una chiamata di
macro_rules!, ma è una funzione Rust a decidere cosa significa l’input. È così chesqlx::query!può controllare il tuo SQL in fase di compilazione.
Usi già le macro derive di altri crate. Eccone due al lavoro. Serialize scrive il codice per il JSON (vedi JSON con Serde), ed Error scrive il codice di Display a partire dal testo di #[error(...)] (vedi 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("l'articolo {0} è esaurito")] OutOfStock(String),}
fn main() { let order = Order { id: 42, item: "Teiera".to_string(), quantity: 2 }; println!("{}", serde_json::to_string(&order).unwrap());
let err = ShopError::OutOfStock("Teiera".to_string()); println!("{err}");}Output:
{"id":42,"item":"Teiera","quantity":2}l'articolo Teiera è esauritoNon hai mai scritto un’implementazione di Display né una riga di codice JSON. Le derive l’hanno scritto mentre il programma veniva compilato.
Come funzionano: token in entrata, token in uscita
Sezione intitolata “Come funzionano: token in entrata, token in uscita”Per prima cosa il compilatore taglia il tuo codice sorgente in token: nomi, punteggiatura, letterali, parentesi. Una macro procedurale è una funzione che riceve quei token come TokenStream e restituisce un nuovo TokenStream. Poi il compilatore compila quello che hai restituito, come se l’avessi scritto tu.
Ecco le tre forme di funzione. Ognuna si marca con un attributo, così il compilatore sa di che tipo è:
use proc_macro::TokenStream;
// #[derive(HelloMacro)]: riceve la struct o l'enum, restituisce codice da AGGIUNGERE.#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { /* ... */ }
// #[route(GET, "/")]: riceve i token dell'attributo e l'elemento, restituisce un SOSTITUTO.#[proc_macro_attribute]pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
// sql!(...): riceve tutto ciò che sta tra le parentesi, restituisce il sostituto.#[proc_macro]pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }Un TokenStream grezzo è solo una lista di token, ed è scomodo da maneggiare. Per questo quasi ogni macro procedurale usa tre crate di supporto:
- syn analizza (parsing) i token e li trasforma in un albero sintattico. Invece dei token ottieni un
DeriveInputcon il nome del tipo, i suoi generici e i suoi campi. - quote trasforma di nuovo in token del codice che sembra Rust. Dentro
quote! { ... }scrivi Rust normale, e#nameci incolla una variabile. - proc-macro2 è un involucro attorno ai tipi
proc_macrodel compilatore, usato da syn e quote. Funziona anche fuori dal compilatore, e questo rende più facili le funzioni di supporto e i test.
Un’idea importante: la tua macro gira in fase di compilazione. Cargo compila prima il crate della macro, per il tuo computer, e il compilatore la esegue mentre costruisce il crate che la usa. Quando il programma finalmente gira, della macro non c’è più traccia. Resta solo il codice che ha scritto.
Preparare il progetto
Sezione intitolata “Preparare il progetto”Una macro procedurale deve vivere in un crate tutto suo, con un tipo di crate speciale. Quel crate può esportare solo macro, non normali funzioni o trait. Quindi la struttura tipica è fatta di due crate affiancati, come serde e serde_derive: uno per il trait, uno per la derive. Noi aggiungiamo un terzo crate, un piccolo programma chiamato shop che li usa. (Ti serve un ripasso su crate e workspace? Vedi Cargo e crate.)
mkdir procmacro-demo && cd procmacro-democargo new --lib hello_macrocargo new --lib hello_macro_derivecargo new shopEcco l’albero dei file finito:
procmacro-demo/├── Cargo.toml (il workspace)├── hello_macro/│ ├── Cargo.toml│ └── src/lib.rs (i trait, e la riesportazione delle derive)├── hello_macro_derive/│ ├── Cargo.toml (proc-macro = true)│ └── src/lib.rs (le macro vere e proprie)└── shop/ ├── Cargo.toml └── src/main.rs (un programma che usa #[derive(...)])Il Cargo.toml principale lega i tre crate in un unico workspace:
[workspace]members = ["hello_macro", "hello_macro_derive", "shop"]resolver = "2"Il crate della macro è l’unico ad avere qualcosa di speciale: proc-macro = true. Inoltre dipende da 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"Gli altri due crate sono normali. hello_macro dipende dal crate della derive, e shop ha bisogno solo di 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)]
Sezione intitolata “Passo 1: #[derive(HelloMacro)]”Cominciamo in piccolo. Vogliamo che ogni tipo con #[derive(HelloMacro)] riceva una funzione hello_macro() che stampa il nome del tipo. Prima il trait, in hello_macro/src/lib.rs. L’ultima riga riesporta le macro derive, così chi le usa ha bisogno solo di questo crate. È esattamente quello che fa serde.
pub trait HelloMacro { fn hello_macro();}
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str];}
// Riesporta le macro derive, così chi le usa ha bisogno solo di questo crate.pub use hello_macro_derive::{Describe, HelloMacro};(Describe serve per il passo 2.) Ora la macro vera e propria, in 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. Trasforma i token in un albero sintattico da esaminare. let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident;
// 2. Costruisci il nuovo codice con quote!. let generated = quote! { impl hello_macro::HelloMacro for #name { fn hello_macro() { println!("Hello, Macro! My name is {}!", stringify!(#name)); } } };
// 3. Restituisci i nuovi token al compilatore. generated.into()}Tre passi, e li ritroverai in quasi tutte le macro derive:
parse_macro_input!trasforma i token in unDeriveInput.ast.identè il nome del tipo, per esempioProduct. Se il parsing fallisce, questa macro restituisce per te un vero errore di compilazione.quote!costruisce il bloccoimpl.#nameci incolla il nome.stringify!(#name)trasforma il nome nel testo"Product"..into()converte ilproc_macro2::TokenStreamdi quote nelproc_macro::TokenStreamdel compilatore.
Nota il percorso completo hello_macro::HelloMacro. Il codice generato viene incollato nel file di chi usa la macro, quindi non può contare sulle sue righe use.
Quando qualcuno scrive #[derive(HelloMacro)] struct Product, il compilatore aggiunge codice che funziona proprio come questa versione scritta a mano. Questa gira nel Playground, perché non c’è di mezzo nessun crate di macro (il testo stampato resta in inglese, identico a quello della macro del progetto):
trait HelloMacro { fn hello_macro();}
struct Product;
// Ecco cosa scrive per te #[derive(HelloMacro)]:impl HelloMacro for Product { fn hello_macro() { println!("Hello, Macro! My name is {}!", "Product"); }}
fn main() { Product::hello_macro();}Output:
Hello, Macro! My name is Product!Scriverlo a mano una volta va benissimo. Scriverlo per 40 tipi è il momento in cui una derive comincia a ripagarsi.
Passo 2: #[derive(Describe)] legge i campi
Sezione intitolata “Passo 2: #[derive(Describe)] legge i campi”HelloMacro usava solo il nome del tipo. Le derive vere, come quelle di serde, guardano i campi. Costruiamo Describe: elenca i nomi dei campi di una struct, comodo per l’intestazione di un CSV. syn ti dà i campi tramite ast.data:
Data::Struct,Data::EnumoData::Union: che genere di tipo è.- Per una struct,
Fields::Named(struct Product { id: u32 }),Fields::Unnamed(struct Meters(f64)) oFields::Unit(struct Marker;).
Aggiungi questo sotto la prima macro in 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),*] } } })}I pezzi nuovi:
- Il lavoro vero si fa in una normale funzione di supporto che restituisce
syn::Result. Così puoi usare?ereturn Err(...)come in qualsiasi altro codice di gestione degli errori. La funzione della macro, in alto, si limita a trasformare un errore in un errore di compilazione. let ... elsegestisce subito il caso «non è una struct» e mantiene piatto il percorso principale.#(#field_names),*è la ripetizione di quote. Incolla ogni stringa delVec, separata da virgole. È la stessa idea di$(...),*inmacro_rules!. OgniStringdiventa un letterale stringa, quindi questo si trasforma in&["id", "name", "price_cents"].field.ident.as_ref().unwrap()qui è sicuro: inFields::Namedogni campo ha un nome.
I messaggi di errore della macro restano in inglese, perché più avanti in questa pagina vedrai l’output reale del compilatore che li mostra.
Ora usa entrambe le derive in shop/src/main.rs. Nota che use hello_macro::{Describe, HelloMacro} importa i trait e le macro derive. Hanno lo stesso nome ma vivono in namespace diversi, ed è proprio così che funziona serde::Serialize.
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());
// Un file CSV: l'intestazione viene dalla derive, le righe da valori reali. 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);}Eseguilo dalla cartella del workspace:
cargo run -p shopOutput (con una build da 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 centsQuesto è l’output reale del progetto, quindi i testi stampati restano in inglese. Guarda l’ordine: Cargo compila syn e hello_macro_derive prima di shop, perché il compilatore deve eseguire la tua macro mentre compila shop. Se aggiungi un campo a Customer, l’intestazione del CSV si aggiorna da sola alla build successiva.
Buoni messaggi di errore
Sezione intitolata “Buoni messaggi di errore”Cosa succede se qualcuno mette #[derive(Describe)] su un enum? Visto che expand_describe restituisce un syn::Error, l’utente riceve un errore del compilatore dall’aspetto normale. (Abbiamo messo gli esempi sbagliati di questa pagina in shop/src/bin/broken.rs, ed è per questo che compare quel percorso.)
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 { | ^^^^^^^^^^^Il messaggio dice che Describe si può derivare solo per le struct. Due cose rendono buono questo errore:
syn::Error::new_spanned(name, ...)si ricorda dove si trova il nome nel file dell’utente (il suo span). Ecco perché i^^^^^puntano aOrderStatuse non a qualche riga dentro la tua macro.to_compile_error()trasforma l’errore in una chiamatacompile_error!("..."). Il compilatore poi lo segnala come qualsiasi altro errore. Puoi usare la stessa macrocompile_error!anche nel codice normale.
Una tuple struct riceve l’altro messaggio («servono campi con nome»), anche questo puntato nel posto giusto:
error: Describe needs named fields, like `struct Point { x: i32 }` --> shop/src/bin/broken.rs:4:8 |4 | struct Meters(f64); | ^^^^^^Scrivi i tuoi messaggi per la persona che usa la macro. Di’ cosa non va e cosa invece funzionerebbe.
Vedere il codice generato con cargo expand
Sezione intitolata “Vedere il codice generato con cargo expand”Le macro possono sembrare magia, e la magia è difficile da correggere. cargo-expand ti mostra il codice dopo che ogni macro è stata eseguita. Installalo una volta (vedi Strumenti e configurazione di Rust per altri strumenti utili):
cargo install cargo-expandcargo expand -p shopUna parte di quello che stampa per il nostro progetto:
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"] }}Ecco il tuo blocco impl, con i nomi dei campi già compilati e stringify!(#name) già trasformato in "Product". Quando una derive non fa quello che ti aspetti, esegui prima di tutto cargo expand. Espande anche println! e le altre macro, quindi cerca i tuoi blocchi impl.
Testare la macro
Sezione intitolata “Testare la macro”Un crate di macro non può testarsi facilmente da solo, perché può esportare solo macro. La soluzione semplice è scrivere normali test in un crate che usa la macro. Metti questo in hello_macro/tests/describe.rs ed esegui 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 ... okMa come verifichi che il caso dell’enum dia un buon errore? Un test normale non può, perché il file di test non compilerebbe. È a questo che serve il crate trybuild. Metti dei piccoli file «che devono fallire» in una cartella, e trybuild li compila uno per uno e confronta l’errore del compilatore con un file .stderr salvato:
#[test]fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui/*.rs");}La maggior parte dei crate di macro più popolari, compresi serde e thiserror, testa così i propri messaggi di errore.
Quando conviene scriverne una?
Sezione intitolata “Quando conviene scriverne una?”Le macro procedurali sono potenti, ma hanno un costo. Sii onesto con te stesso prima di scriverne una:
- Tempo di compilazione. syn è un crate grande. La prima build di ogni progetto che usa la tua macro deve compilarlo. (Le feature predefinite di syn bastano per le derive. Ti serve
features = ["full"]solo per analizzare cose come i corpi delle funzioni, e questo lo rende più lento.) - Complessità. Ora hai un crate in più, codice che scrive codice ed errori più difficili da seguire.
- Strumenti. Gli editor e rust-analyzer capiscono meglio il codice normale di quello generato da una macro.
Un buon ordine in cui provare le cose:
- Una funzione, un trait con un metodo predefinito, oppure i generici. La maggior parte dei problemi di «codice ripetitivo» finisce qui.
- Una macro
macro_rules!(vedi Macro). Non richiede nessun crate in più. - Una macro procedurale, quando il codice dipende dalla struttura di un tipo (il nome, i campi o le varianti), serve a molti tipi e la useranno altre persone. Ecco perché serde, thiserror e clap usano le derive.
Per confronto, ecco Describe fatto con macro_rules!. Funziona e non richiede crate in più, ma la struct va scritta dentro la chiamata della macro. Diventa scomodo in fretta, per esempio se vuoi anche commenti di documentazione, campi pub o altre derive.
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!("{} ha i campi {:?}", Customer::type_name(), Customer::field_names()); println!("{},{}", ana.email, ana.city);}Output:
Customer ha i campi ["email", "city"][email protected],LisbonaPer uno o due tipi nel tuo programma, questa soluzione (o anche solo scrivere l’impl a mano) è spesso la scelta migliore. Una derive è più comoda per chi la usa: tiene la sua struct normale e aggiunge una sola riga.
Errori comuni
Sezione intitolata “Errori comuni”Dimenticare proc-macro = true. Senza la sezione [lib] nel Cargo.toml del crate della macro, il crate è una normale libreria, e il compilatore si rifiuta:
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`Il compilatore dice che l’attributo #[proc_macro_derive] si può usare solo nei crate di tipo proc-macro, e di conseguenza non trova nemmeno proc_macro.
Usare panic! o unwrap() per gli errori dell’utente. «Funziona», ma l’errore è vago e punta all’intero #[derive], non al problema:
error: proc-macro derive panicked --> shop/src/bin/broken.rs:3:10 |3 | #[derive(Describe)] | ^^^^^^^^ | = help: message: Describe can only be derived for structsIl compilatore dice soltanto che la derive è andata in panic. Restituisci invece un syn::Error con new_spanned, come nel passo 2.
Percorsi brevi nel codice generato. Se scrivi impl Describe for #name invece di impl hello_macro::Describe for #name, funziona solo quando l’utente ha importato Describe per caso. Chi scrive #[derive(hello_macro::Describe)] ottiene questo:
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`Il compilatore non trova il trait Describe in questo scope. Usa sempre percorsi completi in quote!: hello_macro::Describe, ::std::fmt::Display, ::core::option::Option.
Pratica
Sezione intitolata “Pratica”Questi esercizi modificano il progetto vero, quindi non c’è il pulsante per provarli online. Costruisci il progetto di questa pagina sul tuo computer ed esegui cargo run -p shop.
1. Contare i campi
Sezione intitolata “1. Contare i campi”Aggiungi un terzo metodo, fn field_count() -> usize, al trait Describe e alla derive. Product::field_count() deve restituire 3. Suggerimento: hai già il vettore field_names. Salva field_names.len() in una variabile prima di quote! e incollala con #.
Mostra la soluzione
In hello_macro/src/lib.rs, aggiungi il metodo al trait:
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str]; fn field_count() -> usize;}In expand_describe, conta i campi e aggiungi il metodo all’impl generato:
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 } } })Poi println!("{} has {} fields", Product::type_name(), Product::field_count()); in main stampa:
Product has 3 fieldsIl conteggio avviene nella tua macro, in fase di compilazione. cargo expand mostra che il risultato è solo fn field_count() -> usize { 3usize }, una costante che non lascia nessun lavoro da fare in esecuzione.
2. Supportare le struct generiche
Sezione intitolata “2. Supportare le struct generiche”Prova #[derive(Describe)] su una struct generica come struct Page<T> { items: Vec<T>, number: u32 }. Fallisce con error[E0107]: missing generics for struct Page, perché il codice generato dice impl hello_macro::Describe for Page e dimentica il <T>. Correggi la derive in modo che le struct generiche funzionino. Suggerimento: ast.generics.split_for_impl() ti dà tre pezzi da incollare nella riga dell’impl. Vedi Generici per ricordarti com’è fatto impl<T>.
Mostra la soluzione
In expand_describe, dividi i generici e usa tutti e tre i pezzi nella riga dell’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),*] } } })Ora questo funziona in 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> (con gli eventuali vincoli), ty_generics è <T>, e where_clause è la parte where ..., se c’è. Ogni derive seria lo fa, quindi ricordati di split_for_impl.
Riepilogo
Sezione intitolata “Riepilogo”- Una macro procedurale è una funzione Rust che gira in fase di compilazione:
TokenStreamin entrata,TokenStreamin uscita. - Ce ne sono di tre tipi: derive (
#[derive(Serialize)]), attributo (#[tokio::main]) e function-like (sql!(...)). - Vivono in un crate tutto loro con
proc-macro = true. Di solito un crate principale le riesporta accanto ai trait. - syn analizza l’input (
DeriveInput,Data,Fields), e quote scrive l’output (#name,#(...),*). - Segnala gli errori dell’utente con
syn::Error::new_spanned(...).to_compile_error(), controlla il risultato concargo expande testa gli errori con trybuild. - Prova prima una funzione, un trait o
macro_rules!. Una macro procedurale conviene quando molti tipi hanno bisogno di codice basato sulla loro struttura.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Quando viene eseguito il codice di una macro procedurale?
2. Che tipo di macro procedurale è #[tokio::main]?
3. Cosa fanno syn e quote?
4. La tua derive deve rifiutare gli enum. Qual è il modo migliore?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Procedural Macros — The Rust Book (in inglese): la guida ufficiale passo per passo a una derive
HelloMacro, più le macro attributo e function-like. - Procedural Macros — The Rust Reference (in inglese): le regole esatte per i tre tipi, i tipi di crate, gli span e l’igiene.
- Documentazione di syn (in inglese):
DeriveInput,Data,Fields,Errore le funzioni di parsing usate in questa pagina. - Documentazione di quote (in inglese): come funzionano l’interpolazione
#vare la ripetizione#(...),*dentroquote!. - proc-macro-workshop (in inglese): i progetti pratici e gratuiti di David Tolnay (una derive builder, una derive debug e altro) con test che ti guidano passo per passo.