Salta ai contenuti

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.

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 tuo async fn main e lo trasforma in un normale fn main che 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ì che sqlx::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 è esaurito
▶ Provalo tu

Non 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 DeriveInput con 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 #name ci incolla una variabile.
  • proc-macro2 è un involucro attorno ai tipi proc_macro del 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.

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.)

Terminal window
mkdir procmacro-demo && cd procmacro-demo
cargo new --lib hello_macro
cargo new --lib hello_macro_derive
cargo new shop

Ecco 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:

hello_macro_derive/Cargo.toml
[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:

hello_macro/Cargo.toml
[dependencies]
hello_macro_derive = { version = "0.1.0", path = "../hello_macro_derive" }
shop/Cargo.toml
[dependencies]
hello_macro = { version = "0.1.0", path = "../hello_macro" }

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.

hello_macro/src/lib.rs
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:

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:

  1. parse_macro_input! trasforma i token in un DeriveInput. ast.ident è il nome del tipo, per esempio Product. Se il parsing fallisce, questa macro restituisce per te un vero errore di compilazione.
  2. quote! costruisce il blocco impl. #name ci incolla il nome. stringify!(#name) trasforma il nome nel testo "Product".
  3. .into() converte il proc_macro2::TokenStream di quote nel proc_macro::TokenStream del 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!
▶ Provalo tu

Scriverlo a mano una volta va benissimo. Scriverlo per 40 tipi è il momento in cui una derive comincia a ripagarsi.

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::Enum o Data::Union: che genere di tipo è.
  • Per una struct, Fields::Named (struct Product { id: u32 }), Fields::Unnamed (struct Meters(f64)) o Fields::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 ? e return 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 ... else gestisce subito il caso «non è una struct» e mantiene piatto il percorso principale.
  • #(#field_names),* è la ripetizione di quote. Incolla ogni stringa del Vec, separata da virgole. È la stessa idea di $(...),* in macro_rules!. Ogni String diventa un letterale stringa, quindi questo si trasforma in &["id", "name", "price_cents"].
  • field.ident.as_ref().unwrap() qui è sicuro: in Fields::Named ogni 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.

shop/src/main.rs
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.
let ana = Customer { email: "[email protected]".to_string(), city: "Lisbon".to_string() };
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:

Terminal window
cargo run -p shop

Output (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
Product #7: Teapot costs 1999 cents

Questo è 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.

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 a OrderStatus e non a qualche riga dentro la tua macro.
  • to_compile_error() trasforma l’errore in una chiamata compile_error!("..."). Il compilatore poi lo segnala come qualsiasi altro errore. Puoi usare la stessa macro compile_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.

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):

Terminal window
cargo install cargo-expand
cargo expand -p shop

Una 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.

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 test
test lists_field_names_in_order ... ok

Ma 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.

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:

  1. Una funzione, un trait con un metodo predefinito, oppure i generici. La maggior parte dei problemi di «codice ripetitivo» finisce qui.
  2. Una macro macro_rules! (vedi Macro). Non richiede nessun crate in più.
  3. 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() {
let ana = Customer { email: "[email protected]".to_string(), city: "Lisbona".to_string() };
println!("{} ha i campi {:?}", Customer::type_name(), Customer::field_names());
println!("{},{}", ana.email, ana.city);
}

Output:

Customer ha i campi ["email", "city"]
▶ Provalo tu

Per 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.

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 structs

Il 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.

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.

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 fields

Il 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.

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.

  • Una macro procedurale è una funzione Rust che gira in fase di compilazione: TokenStream in entrata, TokenStream in 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 con cargo expand e 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

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