Retour au cours

backend / rust

Macros : les bases

Leçon 181 exercice

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 macroRôle
$x:exprCapture une expression Rust valide
$nom:identCapture un identifiant (nom de variable, de type)
$type:tyCapture 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.

rust
// 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

1 disponible
1

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' :

rust
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.

Résoudre l’exercice →