Les macros procédurales en Rust : écrire votre propre #[derive]
Chaque fois que vous écrivez #[derive(Debug)] ou #[derive(Serialize)], un petit programme s’exécute à l’intérieur du compilateur. Il regarde votre struct et écrit un bloc impl à votre place. Ce petit programme est une macro procédurale (procedural macro), et vous pouvez aussi écrire les vôtres.
Imaginez un employé de bureau très serviable. Vous lui tendez un formulaire (votre struct), il lit chaque champ et vous rend toute la paperasse ennuyeuse déjà remplie. Sur la page Les macros, vous avez découvert macro_rules!, qui fonctionne en comparant des motifs. Les macros procédurales sont différentes : ce sont des fonctions Rust ordinaires qui reçoivent du code et renvoient du code. Sur cette page, vous allez en construire une vraie, pas à pas, et voir la vraie sortie.
Les trois sortes de macros procédurales
Section intitulée « Les trois sortes de macros procédurales »Il en existe trois sortes. À l’intérieur, elles fonctionnent toutes de la même façon, mais elles ne se présentent pas pareil pour la personne qui les utilise :
| Sorte | Ce qu’écrit l’utilisateur | Exemples célèbres |
|---|---|---|
| Macro derive | #[derive(Serialize)] sur une struct ou un enum |
Serialize de serde, Error de thiserror, Parser de clap |
| Macro d’attribut | #[route(GET, "/")] sur une fonction ou un autre élément |
#[tokio::main], #[get("/")] d’actix-web |
| Macro de type fonction | sql!(SELECT * FROM users) |
query! de sqlx, html! de yew |
- Une macro derive se contente d’ajouter du code à côté de votre type. Votre struct reste exactement telle que vous l’avez écrite.
- Une macro d’attribut remplace l’élément sur lequel elle est placée.
#[tokio::main]prend votreasync fn mainet la transforme en unefn mainnormale qui démarre le runtime de tokio. - Une macro de type fonction ressemble à un appel de
macro_rules!, mais c’est une fonction Rust qui décide de ce que l’entrée veut dire. C’est comme ça quesqlx::query!peut vérifier votre SQL à la compilation.
Vous utilisez déjà des macros derive venues d’autres crates. En voici deux qui font un vrai travail. Serialize écrit le code pour le JSON (voir JSON avec Serde) et Error écrit le code de Display à partir du texte de #[error(...)] (voir anyhow et thiserror) :
use serde::Serialize;use thiserror::Error;
#[derive(Serialize)]struct Order { id: u32, item: String, quantity: u8,}
#[derive(Debug, Error)]enum ShopError { #[error("l'article {0} est en rupture de stock")] OutOfStock(String),}
fn main() { let order = Order { id: 42, item: "Théière".to_string(), quantity: 2 }; println!("{}", serde_json::to_string(&order).unwrap());
let err = ShopError::OutOfStock("Théière".to_string()); println!("{err}");}Sortie :
{"id":42,"item":"Théière","quantity":2}l'article Théière est en rupture de stockVous n’avez jamais écrit d’impl Display, ni la moindre ligne de code JSON. Les derive l’ont écrit pendant la compilation de votre programme.
Comment ça marche : des tokens entrent, des tokens sortent
Section intitulée « Comment ça marche : des tokens entrent, des tokens sortent »Le compilateur commence par découper votre code source en tokens (jetons) : des noms, de la ponctuation, des littéraux, des parenthèses et des accolades. Une macro procédurale est une fonction qui reçoit ces tokens sous la forme d’un TokenStream et renvoie un nouveau TokenStream. Le compilateur compile ensuite ce que vous avez renvoyé, comme si vous l’aviez tapé vous-même.
Voici les trois formes de fonction. Chacune est marquée par un attribut, pour que le compilateur sache de quelle sorte il s’agit :
use proc_macro::TokenStream;
// #[derive(HelloMacro)] : reçoit la struct ou l'enum, renvoie du code à AJOUTER.#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { /* ... */ }
// #[route(GET, "/")] : reçoit les tokens de l'attribut lui-même et l'élément, renvoie un REMPLACEMENT.#[proc_macro_attribute]pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
// sql!(...) : reçoit tout ce qui se trouve entre les parenthèses, renvoie le remplacement.#[proc_macro]pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }Un TokenStream brut n’est qu’une liste de tokens, et c’est pénible à manipuler. C’est pourquoi presque toutes les macros procédurales utilisent trois crates d’aide :
- syn transforme les tokens en arbre syntaxique. Au lieu de tokens, vous obtenez un
DeriveInputavec le nom du type, ses génériques et ses champs. - quote retransforme du code qui ressemble à du Rust en tokens. Dans
quote! { ... }, vous écrivez du Rust normal, et#namey colle la valeur d’une variable. - proc-macro2 est une enveloppe autour des types
proc_macrodu compilateur ; c’est ce qu’utilisent syn et quote. Elle fonctionne aussi en dehors du compilateur, ce qui facilite les fonctions d’aide et les tests.
Une idée importante : votre macro s’exécute à la compilation. Cargo compile d’abord la crate de la macro, pour votre propre ordinateur, puis le compilateur l’exécute pendant qu’il construit la crate qui l’utilise. Quand le programme finit par s’exécuter, la macro a disparu depuis longtemps. Il ne reste que le code qu’elle a écrit.
Préparer le projet
Section intitulée « Préparer le projet »Une macro procédurale doit vivre dans sa propre crate, avec un type de crate spécial. Cette crate ne peut exporter que des macros, pas de fonctions ni de traits normaux. L’organisation habituelle, c’est donc deux crates côte à côte, comme serde et serde_derive : un pour le trait, un pour le derive. Nous ajouterons une troisième crate, un petit programme appelé shop qui les utilise. (Besoin de revoir les crates et les workspaces ? Voir Cargo et les crates.)
mkdir procmacro-demo && cd procmacro-democargo new --lib hello_macrocargo new --lib hello_macro_derivecargo new shopVoici l’arborescence finale :
procmacro-demo/├── Cargo.toml (le workspace)├── hello_macro/│ ├── Cargo.toml│ └── src/lib.rs (les traits et la réexportation des derive)├── hello_macro_derive/│ ├── Cargo.toml (proc-macro = true)│ └── src/lib.rs (les macros elles-mêmes)└── shop/ ├── Cargo.toml └── src/main.rs (un programme qui utilise #[derive(...)])Le Cargo.toml du dossier principal réunit les trois crates dans un seul workspace (espace de travail) :
[workspace]members = ["hello_macro", "hello_macro_derive", "shop"]resolver = "2"La crate de la macro est la seule à avoir quelque chose de spécial : proc-macro = true. Il dépend aussi de syn, quote et 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"Les deux autres crates sont normales. hello_macro dépend de la crate du derive, et shop n’a besoin que de hello_macro :
[dependencies]hello_macro_derive = { version = "0.1.0", path = "../hello_macro_derive" }[dependencies]hello_macro = { version = "0.1.0", path = "../hello_macro" }Étape 1 : #[derive(HelloMacro)]
Section intitulée « Étape 1 : #[derive(HelloMacro)] »Commençons tout petit. Nous voulons que tout type marqué #[derive(HelloMacro)] reçoive une fonction hello_macro() qui affiche le nom du type. D’abord le trait, dans hello_macro/src/lib.rs. La dernière ligne réexporte les macros derive : ainsi, les utilisateurs n’ont besoin que de cette seule crate. C’est exactement ce que fait serde.
pub trait HelloMacro { fn hello_macro();}
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str];}
// Réexporte les macros derive : les utilisateurs n'ont besoin que de cette crate.pub use hello_macro_derive::{Describe, HelloMacro};(Describe sert à l’étape 2.) Maintenant, la macro elle-même, dans hello_macro_derive/src/lib.rs :
use proc_macro::TokenStream;use quote::quote;use syn::{parse_macro_input, Data, DeriveInput, Fields}; // Data et Fields : étape 2
#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // 1. Transforme les tokens en un arbre syntaxique que l'on peut examiner. let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident;
// 2. Construit le nouveau code avec quote!. let generated = quote! { impl hello_macro::HelloMacro for #name { fn hello_macro() { println!("Hello, Macro! My name is {}!", stringify!(#name)); } } };
// 3. Rend les nouveaux tokens au compilateur. generated.into()}Trois étapes, que vous retrouverez dans presque toutes les macros derive :
parse_macro_input!transforme les tokens en unDeriveInput.ast.identest le nom du type, par exempleProduct. Si l’analyse échoue, cette macro renvoie pour vous une erreur de compilation en bonne et due forme.quote!construit le blocimpl.#namey colle le nom.stringify!(#name)transforme le nom en texte :"Product"..into()convertit leproc_macro2::TokenStreamde quote enproc_macro::TokenStreamdu compilateur.
Remarquez le chemin complet hello_macro::HelloMacro. Le code généré est collé dans le fichier de l’utilisateur : il ne peut donc pas compter sur les lignes use de celui-ci.
Quand quelqu’un écrit #[derive(HelloMacro)] struct Product, le compilateur ajoute du code qui fonctionne exactement comme cette version écrite à la main. Celle-ci s’exécute dans le Playground, puisqu’aucune crate de macros n’entre en jeu (nous gardons le texte en anglais pour qu’il corresponde au projet d’exemple) :
trait HelloMacro { fn hello_macro();}
struct Product;
// Voici ce que #[derive(HelloMacro)] écrit pour vous :impl HelloMacro for Product { fn hello_macro() { println!("Hello, Macro! My name is {}!", "Product"); }}
fn main() { Product::hello_macro();}Sortie :
Hello, Macro! My name is Product!L’écrire une fois à la main, ça va. L’écrire pour 40 types, c’est là qu’un derive commence à être rentable.
Étape 2 : #[derive(Describe)] lit les champs
Section intitulée « Étape 2 : #[derive(Describe)] lit les champs »HelloMacro n’utilisait que le nom du type. Les vrais derive, comme ceux de serde, regardent les champs. Construisons Describe : il renvoie les noms des champs d’une struct, ce qui est bien pratique pour l’en-tête d’un fichier CSV. syn vous donne les champs via ast.data :
Data::Struct,Data::EnumouData::Union: la sorte de type dont il s’agit.- Pour une struct,
Fields::Named(struct Product { id: u32 }),Fields::Unnamed(struct Meters(f64)) ouFields::Unit(struct Marker;).
Ajoutez ceci sous la première macro, dans 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),*] } } })}Les nouveautés :
- Le vrai travail se fait dans une fonction d’aide normale qui renvoie un
syn::Result. Vous pouvez ainsi utiliser?etreturn Err(...)comme dans n’importe quel autre code de gestion des erreurs. La fonction de la macro, tout en haut, se contente de transformer une erreur en erreur de compilation. let ... elserègle tout de suite le cas « ce n’est pas une struct » et garde le chemin normal bien à plat.#(#field_names),*est la répétition de quote. Elle colle chaque chaîne duVec, séparées par des virgules. C’est la même idée que$(...),*dansmacro_rules!. ChaqueStringdevient un littéral de chaîne, donc on obtient&["id", "name", "price_cents"].field.ident.as_ref().unwrap()est sans danger ici : dansFields::Named, tous les champs ont un nom.
Utilisez maintenant les deux derive dans shop/src/main.rs. Remarquez que use hello_macro::{Describe, HelloMacro} importe les traits et les macros derive. Ils portent le même nom, mais vivent dans des espaces de noms différents : c’est exactement comme ça que fonctionne 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 fichier CSV : l'en-tête vient du derive, les lignes de vraies valeurs. 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);}Lancez-le depuis le dossier du workspace :
cargo run -p shopSortie (lors d’une première compilation) :
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 centsLes lignes Compiling, Finished et Running sont affichées par Cargo, toujours en anglais. Regardez l’ordre : Cargo construit syn et hello_macro_derive avant shop, parce que le compilateur doit exécuter votre macro pendant qu’il compile shop. Si vous ajoutez un champ à Customer, l’en-tête du CSV se met à jour tout seul à la compilation suivante.
De bons messages d’erreur
Section intitulée « De bons messages d’erreur »Que se passe-t-il si quelqu’un place #[derive(Describe)] sur un enum ? Comme expand_describe renvoie une syn::Error, l’utilisateur obtient une erreur du compilateur d’apparence tout à fait normale. (Nous avons mis les exemples cassés de cette page dans shop/src/bin/broken.rs, d’où ce chemin.)
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 { | ^^^^^^^^^^^Le message est le texte que nous avons écrit dans la macro (« Describe ne peut être dérivé que pour des structs »). Deux choses rendent cette erreur réussie :
syn::Error::new_spanned(name, ...)retient où se trouve le nom dans le fichier de l’utilisateur (son span, sa position dans le code). C’est pour ça que les^^^^^pointent surOrderStatus, et non sur une ligne quelconque de votre macro.to_compile_error()transforme l’erreur en un appelcompile_error!("..."). Le compilateur la signale ensuite comme n’importe quelle autre erreur. Vous pouvez d’ailleurs utiliser cette même macrocompile_error!dans du code normal.
Une tuple struct reçoit l’autre message (« Describe a besoin de champs nommés »), qui pointe lui aussi au bon endroit :
error: Describe needs named fields, like `struct Point { x: i32 }` --> shop/src/bin/broken.rs:4:8 |4 | struct Meters(f64); | ^^^^^^Écrivez vos messages pour la personne qui utilise la macro. Dites-lui ce qui ne va pas, et ce qui marcherait à la place.
Voir le code généré avec cargo expand
Section intitulée « Voir le code généré avec cargo expand »Les macros peuvent donner l’impression d’être magiques, et la magie, c’est difficile à déboguer. cargo-expand vous montre le code une fois que toutes les macros se sont exécutées. Installez-le une fois pour toutes (voir Les outils de Rust pour d’autres outils pratiques) :
cargo install cargo-expandcargo expand -p shopUne partie de ce qu’il affiche pour notre projet :
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"] }}Voilà votre bloc impl, avec les noms des champs déjà remplis et stringify!(#name) déjà devenu "Product". Quand un derive ne fait pas ce que vous attendez, lancez d’abord cargo expand. Il développe aussi println! et les autres macros, alors cherchez vos propres blocs impl.
Tester votre macro
Section intitulée « Tester votre macro »Une crate de macros a du mal à se tester lui-même, puisqu’il ne peut exporter que des macros. La solution simple : écrire des tests normaux dans une crate qui utilise la macro. Mettez ceci dans hello_macro/tests/describe.rs et lancez 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 ... okMais comment vérifier que le cas de l’enum donne une bonne erreur ? Un test normal n’y arrive pas, parce que le fichier de test ne compilerait pas. C’est à ça que sert la crate trybuild. Vous placez de petits fichiers « qui doivent échouer » dans un dossier ; trybuild compile chacun d’eux et compare l’erreur du compilateur à un fichier .stderr enregistré :
#[test]fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui/*.rs");}La plupart des crates de macros populaires, dont serde et thiserror, testent leurs messages d’erreur de cette façon.
Quand faut-il en écrire une ?
Section intitulée « Quand faut-il en écrire une ? »Les macros procédurales sont puissantes, mais elles ont un coût. Soyez honnête avec vous-même avant de vous lancer :
- Temps de compilation. syn est une grosse crate. La première compilation de chaque projet qui utilise votre macro doit la compiler. (Les features par défaut de syn suffisent pour les derive. Vous n’avez besoin de
features = ["full"]que pour analyser des choses comme le corps d’une fonction, et cela ralentit la compilation.) - Complexité. Vous avez maintenant une crate de plus, du code qui écrit du code, et des erreurs plus difficiles à suivre.
- Outils. Les éditeurs et rust-analyzer comprennent mieux du code ordinaire que du code produit par une macro.
Un bon ordre pour essayer :
- Une fonction, un trait avec une méthode par défaut, ou des génériques. La plupart des problèmes de « code répétitif » s’arrêtent là.
- Une macro
macro_rules!(voir Les macros). Elle ne demande aucune crate supplémentaire. - Une macro procédurale, quand le code dépend de la structure d’un type (son nom, ses champs ou ses variantes), que beaucoup de types en ont besoin et que d’autres personnes vont l’utiliser. C’est pour ça que serde, thiserror et clap utilisent des derive.
Pour comparer, voici Describe écrit avec macro_rules!. Ça fonctionne et ça ne demande aucune crate supplémentaire, mais la struct doit être écrite à l’intérieur de l’appel de macro. Ça devient vite peu pratique, par exemple si vous voulez aussi des commentaires de documentation, des champs pub ou d’autres 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!("{} has fields {:?}", Customer::type_name(), Customer::field_names()); println!("{},{}", ana.email, ana.city);}Sortie :
Customer has fields ["email", "city"][email protected],LisbonPour un ou deux types dans votre propre programme, cette solution (ou tout simplement écrire l’impl à la main) est souvent le meilleur choix. Un derive est plus agréable pour ceux qui l’utilisent : ils gardent leur struct normale et n’ajoutent qu’une ligne.
Erreurs courantes
Section intitulée « Erreurs courantes »Oublier proc-macro = true. Sans la section [lib] dans le Cargo.toml de la crate de la macro, cette crate est une bibliothèque normale, et le compilateur refuse :
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`Le compilateur vous dit que #[proc_macro_derive] ne s’utilise que dans les crates de type proc-macro, et c’est aussi pour ça qu’il ne trouve pas la crate proc_macro.
Utiliser panic! ou unwrap() pour les erreurs de l’utilisateur. Ça « marche », mais l’erreur est floue et pointe sur tout le #[derive], pas sur le problème :
error: proc-macro derive panicked --> shop/src/bin/broken.rs:3:10 |3 | #[derive(Describe)] | ^^^^^^^^ | = help: message: Describe can only be derived for structsIci, le compilateur dit seulement que la macro derive a fait un panic, et affiche le message en guise d’aide. Renvoyez plutôt une syn::Error avec new_spanned, comme à l’étape 2.
Des chemins courts dans le code généré. Si vous écrivez impl Describe for #name au lieu de impl hello_macro::Describe for #name, cela ne marche que si l’utilisateur a importé Describe par hasard. Quelqu’un qui écrit #[derive(hello_macro::Describe)] obtient ceci :
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`Autrement dit : le trait Describe est introuvable dans cette portée (scope). Utilisez toujours des chemins complets dans quote! : hello_macro::Describe, ::std::fmt::Display, ::core::option::Option.
Exercices
Section intitulée « Exercices »Ces exercices modifient le vrai projet, il n’y a donc pas de bouton « Essayez par vous-même ». Construisez le projet de cette page sur votre propre ordinateur et lancez cargo run -p shop.
1. Compter les champs
Section intitulée « 1. Compter les champs »Ajoutez une troisième méthode, fn field_count() -> usize, au trait Describe et au derive. Product::field_count() doit renvoyer 3. Indice : vous avez déjà le vecteur field_names. Enregistrez field_names.len() dans une variable avant quote!, puis collez-la avec #.
Voir la solution
Dans hello_macro/src/lib.rs, ajoutez la méthode au trait :
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str]; fn field_count() -> usize;}Dans expand_describe, comptez les champs et ajoutez la méthode à l’impl généré :
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 } } })Ensuite, println!("{} has {} fields", Product::type_name(), Product::field_count()); dans main affiche :
Product has 3 fieldsLe comptage se fait dans votre macro, à la compilation. cargo expand montre que le résultat est simplement fn field_count() -> usize { 3usize }, une constante : il ne reste plus aucun travail à faire à l’exécution.
2. Accepter les structs génériques
Section intitulée « 2. Accepter les structs génériques »Essayez #[derive(Describe)] sur une struct générique comme struct Page<T> { items: Vec<T>, number: u32 }. Ça échoue avec error[E0107]: missing generics for struct Page (il manque les génériques), parce que le code généré dit impl hello_macro::Describe for Page et oublie le <T>. Corrigez le derive pour que les structs génériques fonctionnent. Indice : ast.generics.split_for_impl() vous donne trois morceaux à coller dans la ligne de l’impl. Voir Les génériques pour vous rappeler à quoi ressemble impl<T>.
Voir la solution
Dans expand_describe, séparez les génériques et utilisez les trois morceaux dans la ligne de l’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),*] } } })Maintenant, ceci fonctionne dans 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 vaut <T> (avec ses contraintes éventuelles), ty_generics vaut <T>, et where_clause est la partie where ..., s’il y en a une. Tout derive sérieux fait cela, alors retenez split_for_impl.
Récapitulatif
Section intitulée « Récapitulatif »- Une macro procédurale est une fonction Rust qui s’exécute à la compilation : un
TokenStreamentre, unTokenStreamsort. - Il en existe trois sortes : derive (
#[derive(Serialize)]), d’attribut (#[tokio::main]) et de type fonction (sql!(...)). - Elles vivent dans leur propre crate avec
proc-macro = true. Une crate principale les réexporte en général à côté des traits. - syn analyse l’entrée (
DeriveInput,Data,Fields) et quote écrit la sortie (#name,#(...),*). - Signalez les erreurs de l’utilisateur avec
syn::Error::new_spanned(...).to_compile_error(), vérifiez le résultat aveccargo expandet testez les erreurs avec trybuild. - Essayez d’abord une fonction, un trait ou
macro_rules!. Une macro procédurale en vaut la peine quand beaucoup de types ont besoin de code basé sur leur structure.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Quand le code d'une macro procédurale s'exécute-t-il ?
2. Quelle sorte de macro procédurale est #[tokio::main] ?
3. À quoi servent syn et quote ?
4. Votre derive doit refuser les enums. Quelle est la meilleure façon de faire ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Procedural Macros — The Rust Book (en anglais) : la présentation officielle d’un derive
HelloMacro, ainsi que des macros d’attribut et de type fonction. - Procedural Macros — The Rust Reference (en anglais) : les règles exactes des trois sortes, les types de crate, les spans et l’hygiène.
- Documentation de syn (en anglais) :
DeriveInput,Data,Fields,Erroret les fonctions d’analyse utilisées sur cette page. - Documentation de quote (en anglais) : comment fonctionnent l’interpolation avec
#varet la répétition avec#(...),*dansquote!. - proc-macro-workshop (en anglais) : des projets pratiques et gratuits de David Tolnay (un derive builder, un derive de debug et d’autres), avec des tests qui vous guident pas à pas.