Macros#
Au chapitre 2, nous avons utilisé println! sans expliquer la nature de ce point d’exclamation. Au chapitre 8, nous avons appliqué #[derive(Debug, Clone)] sans détailler le mécanisme sous-jacent. Ces deux constructions sont des macros : du code qui génère du code. Les macros constituent le système de métaprogrammation de Rust. Elles opèrent à la compilation, avant la vérification de types, et permettent d’écrire des abstractions impossibles à exprimer avec des fonctions seules. Ce chapitre présente les macros déclaratives (macro_rules!), les répétitions, la récursion, puis introduit les macros procédurales.
Macros vs fonctions#
Définition 128 (Macro)
Une macro est une transformation syntaxique effectuée à la compilation. Elle reçoit un fragment d’arbre syntaxique en entrée et produit du code Rust en sortie. Contrairement aux fonctions, les macros opèrent sur la structure du code, pas sur ses valeurs.
Les différences fondamentales entre macros et fonctions sont les suivantes :
Aspect |
Fonction |
Macro |
|---|---|---|
Moment d’exécution |
À l’exécution |
À la compilation |
Entrée |
Valeurs typées |
Fragments syntaxiques |
Nombre de paramètres |
Fixe |
Variable |
Vérification de types |
Avant l’appel |
Après expansion |
Notation |
|
|
Remarque 103
Les macros ne remplacent pas les fonctions. On recourt à une macro lorsqu’une fonction ne suffit pas : nombre variable d’arguments, génération de code répétitif, accès à des informations syntaxiques (noms de champs, noms de types). Lorsqu’une fonction classique — éventuellement générique (chapitre 14) — résout le problème, elle est toujours préférable car plus simple à lire, à tester et à déboguer.
macro_rules! — macros déclaratives#
Définition 129 (Macro déclarative)
Une macro déclarative se définit avec macro_rules!. Elle associe un ou plusieurs motifs (patterns) à du code de remplacement. Lors de l’invocation, le compilateur compare les tokens fournis aux motifs et produit le code correspondant au premier motif qui correspond.
Syntaxe de base#
La forme minimale d’une macro déclarative est :
macro_rules! saluer {
() => {
println!("Bonjour !")
};
}
saluer!();
Bonjour !
Le motif () correspond à une invocation sans argument. Le corps entre => { ... } est le code généré. Chaque branche se termine par un point-virgule.
Fragments (metavariables)#
Définition 130 (Fragment de macro)
Un fragment (ou metavariable) est un paramètre de macro, noté $nom:type_de_fragment. Le type de fragment indique quelle catégorie syntaxique le compilateur doit reconnaître. Les principaux types sont :
Fragment |
Catégorie reconnue |
|---|---|
|
Expression |
|
Identifiant |
|
Type |
|
Motif (pattern) |
|
Bloc |
|
Instruction |
|
Littéral |
|
Un token tree unique (le plus flexible) |
Exemple 110 (Macro avec fragments)
Voir le code ci-dessous.
macro_rules! creer_variable {
($nom:ident, $val:expr) => {
let $nom = $val;
};
}
creer_variable!(x, 42);
creer_variable!(message, String::from("Rust"));
println!("x = {}, message = {}", x, message);
x = 42, message = Rust
Le fragment $nom:ident capture un identifiant ; $val:expr capture une expression arbitraire. Le compilateur substitue ces captures dans le corps de la macro.
Remarque 104
Une macro peut définir plusieurs branches (motifs), à la manière d’un match. Le compilateur les essaie de haut en bas et s’arrête au premier qui correspond. On place généralement les motifs les plus spécifiques en premier.
Répétitions#
Définition 131 (Répétition dans une macro)
La syntaxe $( ... )séparateur* (ou +) permet de capturer zéro ou plusieurs (*) ou un ou plusieurs (+) éléments répétés. Le séparateur optionnel (souvent ,) est inséré entre les occurrences lors de l’expansion.
Les formes courantes sont :
$($e:expr),*— zéro ou plusieurs expressions séparées par des virgules$($e:expr),+— une ou plusieurs expressions séparées par des virgules$($e:expr);*— séparées par des points-virgules
Exemple 111 (La macro vec! simplifiée)
La macro vec! de la bibliothèque standard utilise des répétitions. En voici une version simplifiée.
macro_rules! mon_vec {
( $( $element:expr ),* ) => {
{
let mut v = Vec::new();
$( v.push($element); )*
v
}
};
}
let nombres = mon_vec![1, 2, 3, 4, 5];
println!("{:?}", nombres);
let vide: Vec<i32> = mon_vec![];
println!("{:?}", vide);
[1, 2, 3, 4, 5]
[]
Le bloc $( v.push($element); )* est répété autant de fois qu’il y a d’éléments capturés. La correspondance entre la capture et l’expansion se fait automatiquement.
Exemple 112 (Macro générant des implémentations)
Les répétitions sont particulièrement utiles pour générer du code identique pour plusieurs types.
macro_rules! impl_affichable {
( $( $t:ty ),+ ) => {
$(
impl std::fmt::Display for $t {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Wrapper({})", self.0)
}
}
)+
};
}
struct WrapI32(i32);
struct WrapF64(f64);
impl_affichable!(WrapI32, WrapF64);
println!("{}", WrapI32(42));
println!("{}", WrapF64(3.14));
Wrapper(42)
Wrapper(3.14)
Macros récursives#
Définition 132 (Macro récursive)
Une macro est récursive lorsque l’une de ses branches s’invoque elle-même avec un sous-ensemble des tokens d’entrée. Ce mécanisme permet de traiter des listes de taille arbitraire ou de construire des structures emboîtées.
Exemple 113 (Compteur récursif)
Voir le code ci-dessous.
macro_rules! compter {
() => { 0usize };
($premier:tt $($reste:tt)*) => {
1usize + compter!($($reste)*)
};
}
let n = compter!(a b c d e);
println!("Nombre de tokens : {}", n);
Nombre de tokens : 5
La branche de base () => { 0usize } arrête la récursion. La branche récursive consomme un token tree ($premier:tt) et se rappelle avec le reste.
Exemple 114 (Construction d’une HashMap)
Voici une macro qui crée une HashMap à partir de paires clé-valeur, combinant répétitions et expansion.
macro_rules! carte {
( $( $cle:expr => $val:expr ),* $(,)? ) => {
{
let mut m = std::collections::HashMap::new();
$( m.insert($cle, $val); )*
m
}
};
}
let capitales = carte! {
"France" => "Paris",
"Allemagne" => "Berlin",
"Espagne" => "Madrid",
};
for (pays, capitale) in &capitales {
println!("{} : {}", pays, capitale);
}
Allemagne : Berlin
Espagne : Madrid
France : Paris
()
Remarque 105
Le motif $(,)? en fin de liste autorise une virgule finale optionnelle (trailing comma). C’est une convention idiomatique en Rust qui facilite l’ajout d’éléments.
Macros procédurales#
Définition 133 (Macro procédurale)
Une macro procédurale est une fonction Rust compilée séparément qui reçoit un flux de tokens en entrée et produit un flux de tokens en sortie. Contrairement à macro_rules!, elle opère de manière programmatique sur l’arbre syntaxique. Elle doit être définie dans un crate dédié avec proc-macro = true dans Cargo.toml.
Il existe trois types de macros procédurales :
Type |
Syntaxe d’invocation |
Usage typique |
|---|---|---|
Derive |
|
Implémenter un trait automatiquement |
Attribut |
|
Transformer un item (fonction, struct, etc.) |
Function-like |
|
Syntaxe libre, comme |
Remarque 106
Les macros procédurales nécessitent un crate séparé (avec proc-macro = true dans Cargo.toml). Les bibliothèques syn (analyse syntaxique), quote (génération de code) et proc-macro2 (manipulation de tokens) forment l’outillage standard.
Macros derive#
Définition 134 (Macro derive)
Une macro derive génère automatiquement une implémentation de trait pour une structure ou une énumération annotée par #[derive(NomDuTrait)]. Le compilateur transmet la définition du type (ses champs, ses variantes) à la macro, qui produit le bloc impl correspondant.
On utilise #[derive(...)] depuis le chapitre 8 pour des traits standard comme Debug, Clone, PartialEq ou Copy. Le mécanisme sous-jacent repose sur des macros procédurales de type derive.
Exemple 115 (Derive en action)
Voir le code ci-dessous.
#[derive(Debug, Clone, PartialEq)]
struct Etudiant {
nom: String,
note: f64,
}
let alice = Etudiant { nom: String::from("Alice"), note: 17.5 };
let copie = alice.clone();
println!("{:?}", copie);
println!("Identiques ? {}", alice == copie);
Etudiant { nom: "Alice", note: 17.5 }
Identiques ? true
Anatomie d’une macro derive personnalisée#
Voici la structure conceptuelle d’une macro derive. Ce code doit résider dans un crate séparé et ne peut pas s’exécuter dans un noyau interactif.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Saluer)]
pub fn derive_saluer(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let nom = &ast.ident;
let gen = quote! {
impl #nom {
fn saluer(&self) {
println!("Bonjour, je suis un {} !", stringify!(#nom));
}
}
};
gen.into()
}
Remarque 107
Le flux de travail est le suivant : (1) syn analyse le TokenStream d’entrée en un arbre syntaxique structuré (DeriveInput), (2) on extrait les informations nécessaires (nom du type, champs, etc.), (3) quote! construit le code de sortie avec interpolation via #variable, (4) le résultat est converti en TokenStream.
Macros d’attribut et function-like#
Les macros d’attribut (#[proc_macro_attribute]) transforment un item complet (fonction, structure, module). Elles reçoivent deux TokenStream : les arguments de l’attribut et l’item annoté. Les macros function-like (#[proc_macro]) s’invoquent comme macro_rules! mais offrent la puissance programmatique complète. Elles sont notamment utilisées pour implémenter des DSL validés à la compilation (requêtes SQL, formats HTML, etc.).
Hygiène et portée#
Définition 135 (Hygiène des macros)
Une macro est dite hygiénique lorsque les identifiants qu’elle introduit ne peuvent pas entrer en conflit avec les identifiants du code appelant. Les macros macro_rules! de Rust sont partiellement hygiéniques : les variables locales introduites par la macro sont isolées du contexte d’appel.
Proposition 39 (Garantie d’hygiène)
Dans une macro macro_rules!, les variables locales créées dans le corps de la macro appartiennent à un contexte syntaxique distinct de celui de l’appelant. Deux variables portant le même nom — l’une dans la macro, l’autre dans le code appelant — ne sont pas confondues par le compilateur.
Exemple 116 (Démonstration de l’hygiène)
Voir le code ci-dessous.
macro_rules! definir_x {
() => {
let x = "défini par la macro";
println!("Dans la macro : {}", x);
};
}
let x = "défini par l'appelant";
definir_x!();
println!("Dans l'appelant : {}", x);
Dans la macro : défini par la macro
Dans l'appelant : défini par l'appelant
Les deux variables x coexistent sans conflit. Le x de la macro n’écrase pas le x de l’appelant, grâce à l’hygiène.
Remarque 108
Comparaison avec le C/C++. Les macros du préprocesseur C (#define) sont de simples substitutions textuelles. Elles ne sont pas hygiéniques : une macro peut capturer ou masquer des variables du contexte appelant, produisant des bogues subtils et difficiles à diagnostiquer. En Rust, l’hygiène rend les macros déclaratives considérablement plus sûres.
Remarque 109
Portée des macros. Par défaut, une macro macro_rules! n’est visible que dans le module où elle est définie, et uniquement après sa définition (la portée est textuelle). Pour l’exporter, on utilise #[macro_export] (place la macro à la racine du crate) ou #[macro_use] sur la déclaration du module.
Macros utilitaires courantes#
La bibliothèque standard fournit de nombreuses macros au-delà de println! (chapitre 2).
// dbg! --- affiche l'expression et sa valeur sur stderr
let a = 2;
let b = dbg!(a * 3) + 1;
println!("b = {}", b);
// concat! et stringify! --- manipulation de chaînes à la compilation
let s = concat!("Bonjour", " ", "le", " ", "monde");
println!("{}", s);
println!("L'expression est : {}", stringify!(2 + 3 * x));
b = 7
Bonjour le monde
L'expression est : 2 + 3 * x
[src/lib.rs:176:9] a * 3 = 6
Remarque 110
La macro dbg! retourne la valeur inspectée, ce qui permet de l’insérer dans une chaîne d’expressions sans modifier la logique. Parmi les autres macros utilitaires : todo! (panique avec un message indiquant du code non implémenté), unimplemented! (variante sémantique), env! (lit une variable d’environnement à la compilation) et include_str! (inclut le contenu d’un fichier comme &str).
Résumé#
Les macros en Rust se déclinent en deux grandes familles :
Les macros déclaratives (
macro_rules!) utilisent un système de correspondance de motifs sur les tokens. Elles sont concises, hygiéniques et suffisent pour la grande majorité des besoins : génération de code répétitif, nombre variable d’arguments, DSL simples.Les macros procédurales (derive, attribut, function-like) offrent la puissance complète du langage Rust pour transformer du code. Elles sont nécessaires pour les cas avancés : dérivation automatique de traits, analyse syntaxique personnalisée, validation à la compilation.
Le système de macros de Rust se distingue par son hygiène, son intégration au système de types et son absence de surcoût à l’exécution. Lorsqu’un problème peut être résolu par des fonctions génériques (chapitre 14) ou des traits (chapitre 13), on préfère ces mécanismes plus simples. Les macros restent l’outil de choix pour les abstractions qui dépassent les capacités du système de types.