Macros procédurales — Derive

Macros procédurales — Derive

1. Qu'est-ce qu'une derive macro ?

Une derive macro s'utilise comme ça :

#[derive(Debug, Clone, MaMacro)]
struct Personne {
    nom: String,
    age: u32,
}

Debug et Clone sont des derives de la stdlib. MaMacro est une derive personnalisée. Elle reçoit la définition complète de Personne et génère du code Rust supplémentaire — généralement une implémentation de trait.


2. Structure d'une crate proc-macro

Les macros procédurales doivent être dans leur propre crate séparée.

# ma_lib_derive/Cargo.toml
[package]
name = "ma_lib_derive"
version = "0.1.0"

[lib]
proc-macro = true

[dependencies]
syn   = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
# ma_lib/Cargo.toml
[dependencies]
ma_lib_derive = { path = "../ma_lib_derive" }
// ma_lib_derive/src/lib.rs
use proc_macro::TokenStream;

#[proc_macro_derive(MaTrait)]
pub fn derive_ma_trait(input: TokenStream) -> TokenStream {
    // input = la struct/enum sur laquelle #[derive(MaTrait)] est appliqué
    // retour = code Rust à ajouter
    todo!()
}

3. syn — parser le code

syn transforme les tokens bruts en arbre syntaxique utilisable.

use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(MaTrait)]
pub fn derive_ma_trait(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let nom_struct = &input.ident;       // Identifiant : Personne, Config, etc.
    let generics   = &input.generics;    // Les paramètres génériques <T, U>
    let data       = &input.data;        // Contenu : struct, enum, union

    todo!()
}

Les champs utiles de DeriveInput :

input.ident      // nom de la struct/enum
input.generics   // <T: Clone, U>
input.attrs      // attributs #[...] posés sur la struct
input.data       // Data::Struct, Data::Enum, Data::Union

4. quote — générer du code

quote! produit des tokens Rust à partir d'un template. Les variables sont interpolées avec #.

use quote::quote;

let nom = &input.ident;

let expanded = quote! {
    impl #nom {
        pub fn hello(&self) -> String {
            format!("Je suis une instance de {}", stringify!(#nom))
        }
    }
};

TokenStream::from(expanded)

Règles d'interpolation :

let name: &Ident = ...;
let ty: &Type = ...;
let items: &[...] = ...;

quote! {
    #name          // Interpolation simple
    #ty            // Un type
    #(#items),*   // Répétition (comme $(...),* en macro_rules!)
}

5. Exemple complet — Describe

Une derive macro qui génère une méthode describe() sur n'importe quelle struct.

// ma_lib_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(Describe)]
pub fn derive_describe(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    let expanded = quote! {
        impl #name {
            pub fn describe(&self) -> String {
                format!("Instance de {}", stringify!(#name))
            }
        }
    };

    TokenStream::from(expanded)
}
// utilisation
use ma_lib_derive::Describe;

#[derive(Describe)]
struct Config {
    host: String,
    port: u16,
}

fn main() {
    let c = Config { host: "localhost".to_string(), port: 8080 };
    println!("{}", c.describe()); // "Instance de Config"
}

6. Accéder aux champs de la struct

Pour itérer sur les champs et générer du code par champ :

use syn::{Data, Fields};

#[proc_macro_derive(ListeChamps)]
pub fn derive_liste_champs(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    // Extraire les champs nommés
    let champs = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => panic!("ListeChamps ne supporte que les structs avec champs nommés"),
        },
        _ => panic!("ListeChamps ne supporte que les structs"),
    };

    // Noms des champs comme strings
    let noms_champs: Vec<_> = champs
        .iter()
        .map(|f| f.ident.as_ref().unwrap().to_string())
        .collect();

    let expanded = quote! {
        impl #name {
            pub fn champs() -> &'static [&'static str] {
                &[ #(#noms_champs),* ]
            }
        }
    };

    TokenStream::from(expanded)
}
#[derive(ListeChamps)]
struct Personne {
    nom: String,
    age: u32,
    email: String,
}

fn main() {
    println!("{:?}", Personne::champs()); // ["nom", "age", "email"]
}

7. Attributs helper sur les champs

On peut définir des attributs personnalisés à placer sur les champs :

#[proc_macro_derive(Validable, attributes(valider))]
pub fn derive_validable(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    let champs = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(f) => &f.named,
            _ => panic!(),
        },
        _ => panic!(),
    };

    // Regarde si le champ a #[valider(min_len = 3)]
    for champ in champs {
        for attr in &champ.attrs {
            if attr.path().is_ident("valider") {
                // Parser les arguments de l'attribut
            }
        }
    }

    // Génère la validation...
    todo!()
}
#[derive(Validable)]
struct RegisterForm {
    #[valider(min_len = 3, max_len = 50)]
    username: String,
    #[valider(email)]
    email: String,
    #[valider(min_len = 8)]
    password: String,
}

C'est exactement comme ça que derive_form fonctionne dans Runique pour générer la validation des formulaires.