backend / rust
Macros : les bases
Explication
Ce que vous allez apprendre
- Comprendre ce qu'une macro fait de plus qu'une fonction classique
- Écrire une macro déclarative simple avec
macro_rules! - Gérer un nombre variable d'arguments dans une macro
- Reconnaître les macros de la stdlib omniprésentes (
vec!,format!,assert_eq!) - Utiliser
dbg!comme réflexe de débogage rapide
Dans quel contexte ?
Un développeur veut générer, pour plusieurs types de points géométriques (Point2D<i32>, Point2D<f64>), une struct avec les mêmes champs et les mêmes méthodes utilitaires, sans dupliquer manuellement le code pour chaque variante. Une macro déclarative peut générer ce code répétitif automatiquement à la compilation, à partir d'un patron défini une seule fois.
D'abord, il faut comprendre ce qu'une macro fait que ni une fonction ni un générique ne peuvent faire
Une fonction opère sur des valeurs à l'exécution. Une macro, elle, opère sur le code source lui-même, à la compilation : elle prend du code en entrée (via un pattern matching spécial) et produit du code en sortie, avant même que la compilation proprement dite ne commence. C'est ce qui permet à vec![1, 2, 3] de générer exactement le code équivalent à trois appels .push() successifs, de façon totalement transparente.
Une fois ce principe compris, la syntaxe macro_rules! devient plus lisible
macro_rules! carre { ($x:expr) => { $x * $x }; } définit un patron : n'importe quelle expression ($x:expr) passée à carre!(...) sera substituée dans le corps de la macro. C'est du pattern matching, comme un match, mais appliqué à du code source plutôt qu'à des valeurs.
| Élément de macro | Rôle |
|---|---|
$x:expr | Capture une expression Rust valide |
$nom:ident | Capture un identifiant (nom de variable, de type) |
$type:ty | Capture un type |
$(...)+ / $(...)* | Répétition, une ou plusieurs fois / zéro ou plusieurs fois |
Prérequis
Il faut être à l'aise avec les fonctions et les generics (leçons précédentes) : une macro sert souvent à générer un code que les generics seuls ne pourraient pas exprimer.
Il reste un cas d'usage bien plus fréquent en pratique que les macros manuelles
#[derive(Debug, Default)], déjà croisé dans les leçons précédentes sur les structs, est en réalité une "derive macro" — une macro appliquée automatiquement à une déclaration de type pour générer une implémentation de trait. C'est de très loin la forme de macro la plus utilisée au quotidien par un développeur Rust, bien plus que l'écriture de macro_rules! personnalisées.
Piège fréquent
Une macro macro_rules! récursive (comme max_de! qui s'appelle elle-même pour traiter un nombre variable d'arguments) doit toujours avoir un cas de base clairement défini en premier, sinon la génération de code peut boucler sans jamais s'arrêter à la compilation, provoquant une erreur difficile à diagnostiquer.
Enfin, un outil de débogage à connaître absolument
Bonne pratique
Utilise dbg!(expression) plutôt qu'un println! manuel pendant le débogage rapide : dbg! affiche automatiquement le fichier, la ligne, l'expression source ET sa valeur, tout en retournant la valeur elle-même — ce qui permet de l'insérer directement dans une expression existante sans casser le code environnant.
Maintenant que tu comprends la génération de code à la compilation, la prochaine leçon aborde le territoire le plus délicat du langage : unsafe, et les cinq opérations précises qu'il autorise.
Commandes & code
Macros : les bases
Les macros génèrent du code à la compilation — au-delà de ce qu'une fonction peut exprimer.
// Macros declaratives (macro_rules!) : pattern matching sur le code source lui-meme
macro_rules! carre {
($x:expr) => {
$x * $x
};
}
// Macro variadique : accepte un nombre arbitraire d'arguments
macro_rules! max_de {
($x:expr) => { $x };
($x:expr, $($reste:expr),+) => {
{
let a = $x;
let b = max_de!($($reste),+);
if a > b { a } else { b }
}
};
}
// Macro qui genere une struct + son implementation (evite la duplication)
macro_rules! creer_point {
($nom:ident, $type:ty) => {
struct $nom {
x: $type,
y: $type,
}
impl $nom {
fn nouveau(x: $type, y: $type) -> Self {
Self { x, y }
}
fn distance_origine(&self) -> f64 {
((self.x * self.x + self.y * self.y) as f64).sqrt()
}
}
};
}
creer_point!(Point2D, i32);
fn main() {
println!("{}", carre!(5));
println!("{}", max_de!(3, 7, 2, 9, 4));
let p = Point2D::nouveau(3, 4);
println!("{}", p.distance_origine()); // 5.0
// Macros de la stdlib omnipresentes en Rust idiomatique
let v = vec![1, 2, 3]; // construit un Vec
println!("{:?}", v); // formatage avec Debug
let s = format!("{}-{}", 1, 2); // construit une String formattee
assert_eq!(2 + 2, 4); // panique si faux, utilise partout dans les tests
assert!(true, "message si echec"); // avec message custom
let _ = matches!(Some(3), Some(n) if n > 2); // teste un pattern, retourne bool
// Derive macros : generent du code a partir d'attributs (deja vu en cours precedents)
#[derive(Debug, Default)]
struct Config {
port: u16,
debug: bool,
}
let c = Config::default(); // Default derive genere une instance avec zero values
println!("{:?}", c);
// dbg! : affiche fichier/ligne/expression/valeur, tres utile pour le debug rapide
let x = 5;
let y = dbg!(x * 2); // affiche sur stderr: [src/main.rs:XX] x * 2 = 10
println!("{}", y);
}Résumé
macro_rules!fait du pattern matching sur du code source pour le générer à la compilation.- Les macros variadiques (
$(...)+,$(...)*) traitent un nombre arbitraire d'arguments. - Les derive macros (
#[derive(Debug)]) sont la forme de macro la plus utilisée au quotidien. - Les macros de la stdlib (
vec!,format!,assert_eq!,dbg!) sont omniprésentes dans tout code Rust idiomatique.
Exercices pratiques
Mission : une macro variadique qui boucle a l'infini
Objectif : Diagnostiquer une macro_rules! recursive mal formee et une struct dupliquee, puis corriger la macro et en generer une version generique.
Contexte
Un dev veut ecrire une macro min_de! (inverse de max_de! vu en cours) pour trouver le plus petit d'un nombre variable d'arguments, mais son projet ne compile plus, avec une erreur de compilation qui mentionne 'recursion limit reached while expanding the macro' :
macro_rules! min_de {
($x:expr, $($reste:expr),+) => {
{
let a = $x;
let b = min_de!($($reste),+);
if a < b { a } else { b }
}
};
}Separement, le projet contient trois struct quasi identiques Point2Di32, Point2Df64 et Point2Du32, copiees-collees a la main avec seulement le type qui change, ce que l'equipe veut remplacer par une seule macro generatrice.