Macros déclaratives et procédurales
Nota : certains codes présentés ici ne compilent pas dans le playground.
Les macros en Rust permettent de faire de la métaprogrammation, c’est-à-dire d’écrire du code qui génère d’autres morceaux de code avant la compilation finale.
Contrairement aux fonctions qui manipulent des valeurs à l’exécution, les macros manipulent directement l’arbre de syntaxe abstraite (AST) du langage au moment de la compilation.
On distingue deux grandes familles de macros :
- les macros déclaratives (par correspondance de motifs) et
- les macros procédurales (par transformation de flux de tokens).
Les Macros Déclaratives (macro_rules!)
Souvent appelées “macros par l’exemple”, elles permettent de définir une syntaxe personnalisée en faisant correspondre des motifs de code, à la manière d’un bloc match.
- Designateurs de fragments : On utilise des jetons comme
expr(expression),ident(identifiant),ty(type), oustmt(instruction) pour capturer les éléments de syntaxe. - Répétitions : Les symboles
+ou*permettent de gérer des listes d’arguments de taille variable (comme dansvec![]).
#[macro_export]
macro_rules! creer_map {
// Motifs : capture une liste de clés => valeurs séparées par des virgules
( $( $cle:expr => $valeur:expr ),* ) => {
{
let mut map = std::collections::HashMap::new();
$(
map.insert($cle, $valeur);
)*
map
}
};
}
fn main() {
// Utilisation de la macro déclarative
let capitales = creer_map![
"France" => "Paris",
"Italie" => "Rome"
];
println!("Capitale de la France : {:?}", capitales.get("France"));
}
Les macros procédurales
Les macros procédurales fonctionnent comme des fonctions de compilation : elles prennent un flux de tokens (TokenStream) en entrée, exécutent du code Rust arbitraire pour le manipuler, et retournent un nouveau flux de tokens en sortie.
Pour les créer, il faut obligatoirement utiliser une crate dédiée de type bibliothèque avec l’option proc-macro = true dans le fichier Cargo.toml. On s’appuie généralement sur deux crates piliers de l’écosystème : syn (pour analyser le flux de tokens en structures exploitables) et quote (pour reconvertir du code Rust en tokens).
Il existe trois types de macros procédurales :
Les macros dérivées (Derive macros)
Elles s’ajoutent à l’attribut #[derive(...)] pour générer automatiquement l’implémentation de traits sur des structures ou des énumérations, sans modifier le code d’origine.
// Code dans la crate de la macro procédurale (ex: mon_derive)
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Decrire)]
pub fn decrire_derive(input: TokenStream) -> TokenStream {
// Analyse l'AST de la structure cible
let ast = parse_macro_input!(input as DeriveInput);
let nom = &ast.ident;
// Génère le code de l'implémentation du trait
let generated = quote! {
impl Decrire for #nom {
fn decrire(&self) {
println!("Je suis une instance de la structure '{}'.", stringify!(#nom));
}
}
};
generated.into()
}
Utilisation dans notre projet principal :
use mon_derive::Decrire;
trait Decrire {
fn decrire(&self);
}
#[derive(Decrire)]
struct Utilisateur {
nom: String,
}
Les macros d’attribut (Attribute-like macros)
Elles permettent de créer des attributs personnalisés (comme #[tokio::main] ou #[get("/")]) applicables sur n’importe quel élément (fonctions, structures, modules). Elles reçoivent deux arguments : les arguments passés à l’attribut lui-même et l’item sur lequel l’attribut est posé.
// Signature type d'une macro d'attribut
#[proc_macro_attribute]
pub fn mon_attribut(args: TokenStream, item: TokenStream) -> TokenStream {
// Modifie ou enveloppe l'élément ciblé
item
}
Les macros fonctionnelles (Function-like macros)
Elles ressemblent aux macros déclaratives à l’appel (avec le point d’exclamation !), mais sont traitées par une fonction procédurale pour effectuer des manipulations syntaxiques complexes (comme la macro sqlx::query!).
// Signature type d'une macro fonctionnelle
#[proc_macro]
pub fn ma_macro_fonctionnelle(input: TokenStream) -> TokenStream {
// Traite le contenu textuel passé à l'intérieur des parenthèses
input
}
Hygiène des Macros
L’un des grands atouts de Rust par rapport aux macros préprocesseurs du C/C++ est l’hygiène.
- Macros déclaratives : Elles sont parfaitement hygiéniques. Les variables locales créées à l’intérieur de la macro ne risquent pas d’entrer en conflit ou de masquer les variables portant le même nom dans le code de l’utilisateur qui appelle la macro.
- Macros procédurales : Elles ne sont pas automatiquement hygiéniques. Pour éviter les collisions de noms de variables ou de fonctions, on utilise des identifiants uniques appelés
Span(fournis par la crateproc-macroousyn), qui attachent des informations de contexte et d’origine à chaque token créé.