Macros procédurales — Attribute

Macros procédurales — Attribute

1. Derive vs Attribute — la différence

Une derive macro ajoute du code. Une attribute macro remplace l'item par ce qu'elle retourne — elle peut modifier, décorer, ou entièrement transformer.


2. Signature d'une attribute macro

// ma_lib_derive/src/lib.rs
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn mon_attribut(attr: TokenStream, item: TokenStream) -> TokenStream {
    // attr = les arguments entre parenthèses : #[mon_attribut(arg1, arg2)]
    // item = le code sur lequel l'attribut est posé (la fonction, struct, etc.)
    // retour = le code qui remplace l'item
    item // Retourner item sans modification = attribut no-op
}

3. Exemple — mesure du temps d'exécution

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

#[proc_macro_attribute]
pub fn timed(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as ItemFn);

    let name    = &input.sig.ident;
    let vis     = &input.vis;
    let sig     = &input.sig;
    let block   = &input.block;

    let expanded = quote! {
        #vis #sig {
            let __start = std::time::Instant::now();
            let __result = (|| #block)();
            println!(
                "[timed] '{}' : {:?}",
                stringify!(#name),
                __start.elapsed()
            );
            __result
        }
    };

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

#[timed]
fn calcul(n: u64) -> u64 {
    (0..n).sum()
}

fn main() {
    let r = calcul(1_000_000);
    // Affiche : [timed] 'calcul' : 1.2ms
    println!("résultat: {}", r);
}

La fonction calcul est remplacée par une version identique mais entourée d'un chronomètre. L'appelant n'y voit rien.


4. Lire les arguments de l'attribut

#[timed(prefix = "MON_APP")]
fn calcul(n: u64) -> u64 { ... }

Pour parser les arguments, on utilise syn::parse :

use syn::{parse_macro_input, LitStr, Token};
use syn::parse::{Parse, ParseStream};

struct TimedArgs {
    prefix: Option<String>,
}

impl Parse for TimedArgs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if input.is_empty() {
            return Ok(TimedArgs { prefix: None });
        }
        // Attend : prefix = "valeur"
        let _: syn::Ident = input.parse()?; // "prefix"
        let _: Token![=] = input.parse()?;
        let val: LitStr = input.parse()?;
        Ok(TimedArgs { prefix: Some(val.value()) })
    }
}

#[proc_macro_attribute]
pub fn timed(attr: TokenStream, item: TokenStream) -> TokenStream {
    let args = parse_macro_input!(attr as TimedArgs);
    let prefix = args.prefix.unwrap_or_else(|| "timed".to_string());

    let input = parse_macro_input!(item as ItemFn);
    let name  = &input.sig.ident;
    let sig   = &input.sig;
    let vis   = &input.vis;
    let block = &input.block;

    let expanded = quote! {
        #vis #sig {
            let __start = std::time::Instant::now();
            let __result = (|| #block)();
            println!("[{}] '{}' : {:?}", #prefix, stringify!(#name), __start.elapsed());
            __result
        }
    };

    TokenStream::from(expanded)
}

5. Transformer une struct

Une attribute macro peut aussi s'appliquer à une struct et la modifier :

#[proc_macro_attribute]
pub fn avec_id(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut input = parse_macro_input!(item as syn::ItemStruct);

    // Ajoute un champ `id: u32` à la struct
    if let syn::Fields::Named(ref mut fields) = input.fields {
        let id_field: syn::Field = syn::parse_quote! {
            pub id: u32
        };
        fields.named.push(id_field);
    }

    quote! { #input }.into()
}
#[avec_id]
struct Article {
    titre: String,
    contenu: String,
}

// Après expansion, la struct a 3 champs : id, titre, contenu
fn main() {
    let a = Article { id: 1, titre: "Hello".to_string(), contenu: "...".to_string() };
}

6. Cas d'usage réels

Les attribute macros sont utilisées partout dans l'écosystème Rust :

Axum — définir des handlers HTTP

// Simplifié — Axum utilise des extracteurs, pas d'attribute macro
// Mais des frameworks comme Actix-web utilisent ce pattern
#[get("/users")]
async fn list_users() -> impl Responder { ... }

Tokio — transformer une fn en runtime async

#[tokio::main]
async fn main() {
    // Tokio injecte le runtime autour de ce bloc
}

Ce qui se génère (simplifié) :

fn main() {
    tokio::runtime::Runtime::new()
        .unwrap()
        .block_on(async {
            // ton code ici
        });
}

Serde — contrôle fin de la sérialisation

#[derive(Serialize, Deserialize)]
struct Config {
    #[serde(rename = "host_name")]
    host: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    port: Option<u16>,
}

serde et rename sont des attributs helper enregistrés par la derive macro Serialize/Deserialize.

⚡ Runique exercise Beta
Say hello to start an exercise for this course.