Acheter version papier

Rust

Fork me on GitHub

II. Spécificités de Rust

13. Les macros

Nous voici enfin aux fameuses macros dont je vous ai déjà parlé plusieurs fois ! Pour rappel, une macro s'appelle des façons suivantes :

Runla_macro!();
// ou bien :
la_macro![];
// ou encore :
la_macro! {};

Le point important ici est la présence du ! après le nom de la macro. Nous ne parlerons ici pas des macros procédurales (proc-macros), un chapitre leur est dédié dans la troisième partie de ce cours.

Fonctionnement

Nous rentrons maintenant dans le vif du sujet : une macro est définie au travers d’une série de règles qui ressemblent à du pattern-matching. C'est toujours bon ? Parfait !

Une déclaration de macro se fait avec le mot-clé macro_rules (suivie de l'habituel "!"). Exemple :

Runmacro_rules! dire_bonjour {
    () => {
        println!("Bonjour !");
    }
}

dire_bonjour!();

Et on obtient :

Bonjour !

Merveilleux ! Bon jusque-là, rien de bien difficile. Mais ne vous inquiétez pas, ça arrive !

Les arguments (ou flux de tokens)

Bien évidemment, les macros peuvent recevoir des "arguments" même s'il serait plus exact de dire qu'elles reçoivent un flux de tokens :

Runmacro_rules! dire_quelque_chose {
    ($x:expr) => {
        println!("Il dit : '{}'", $x);
    };
}

dire_quelque_chose!("hoy !");

Ce qui affichera :

Il dit : 'hoy !'

Regardons un peu plus en détails le code. Le ($x:expr) en particulier. Ici, nous avons indiqué que notre macro prenait une expression appelée x en paramètre. Après il nous a juste suffi de l'afficher.

Pour le lexique : $x est une metavariable (en un mot) tandis que expr est un spécificateur de fragment.

Maintenant on va ajouter la possibilité de passer une deuxième expression (tout en gardant la possibilité de n'en passer qu'une seule) :

Runmacro_rules! dire_quelque_chose {
    ($x:expr) => {
        println!("Il dit : '{}'", $x);
    };
    ($x:expr, $y:expr) => {
        println!("Il dit '{}' à {}", $x, $y);
    };
}

dire_quelque_chose!("hoy !");
dire_quelque_chose!("hoy !", "quelqu'un");

Et nous obtenons :

Il dit : 'hoy !'
Il dit 'hoy !' à quelqu'un

Les macros fonctionnent donc exactement de la même manière qu'un match, sauf qu'ici on "matche" sur les arguments.

Les différents spécificateurs de fragment

Comme vous vous en doutez, il y a d'autres spécificateurs de fragment en plus des expr. En voici la liste complète :

  • ident : un identifiant (utilisé pour un nom de variable, de type, de fonction, etc). Exemples : x, foo.
  • path : un nom qualifié. Exemple : T::SpecialA.
  • expr : une expression. Exemples : 2 + 2, if true then { 1 } else { 2 }, f(42).
  • ty : un type. Exemples : i32, Vec<(char, String)>, &T.
  • pat_param : un motif (ou "pattern"). Exemples : Some(x) dans if let Some(x) = Some(12), (17, 'a'), _.
  • pat : plus ou moins pareil que pat_param. Supporte potentiellement plus de cas en fonction de l'édition de Rust.
  • stmt : une instruction unique (ou "single statement"). Exemple : let x = 3.
  • block : une séquence d'instructions délimitée par des accolades. Exemple : { log(error, "hi"); return 12; }.
  • item : un item. Exemples : fn foo() { }, struct Bar;.
  • meta : un attribut. Exemple : #[allow(unused_variables)].
  • tt : un " token tree " contenu dans les délimiteurs [], () ou {}.
  • lifetime : Un token de durée de vie. Exemples : 'a, 'static.
  • vis : un qualifieur de visibilité (qui peut être vide). Exemples : pub, pub(crate).
  • literal : une expression litérale. Exemples : a", 'a', 5.

Répétition

Les macros comme vec!, print!, write!, etc... permettent le passage d'un nombre "d'arguments" variable (un peu comme les va_args en C ou les templates variadiques en C++). Cela fonctionne de la façon suivante :

Runmacro_rules! vector {
    (
        $($x:expr),*
    ) => {
        [ $($x),* ].to_vec()
    }
}

let mut v: Vec<u32> = vector!(1, 2, 3);

v.push(6);
println!("{:?}", &v);

Ici, on dit qu'on veut une expression répétée un nombre inconnu de fois (le $(votre_variable),*). La virgule devant l'étoile indique le séparateur entre les arguments. On aurait aussi très bien pu mettre un ;. D'ailleurs pourquoi ne pas essayer ?

Runmacro_rules! vector {
    (
        $($x:expr);*
    ) => {
        [ $($x),* ].to_vec()
    }
}

let mut v: Vec<u32> = vector!(1; 2; 3);

v.push(6);
println!("{:?}", &v);

Dans le cas présent, on récupère le tout dans une slice qui est ensuite transformée en Vec. On pourrait aussi afficher tous les arguments un par un :

Runmacro_rules! vector {
    (
        $x:expr,$($y:expr),*
    ) => (
        println!("Nouvel argument : {}", $x);
        vector!($($y),*);
    );
    ( $x:expr ) => (
        println!("Nouvel argument : {}", $x);
    )
}

vector!(1, 2, 3, 12);

Vous aurez noté que j'ai remplacé les parenthèses par des accolades. Il aurait aussi été possible d'utiliser "{{ }}" ou même "[ ]". Il est davantage question de préférence personnelle.

Pourquoi "{{ }}" ?

Tout simplement parce qu'ici nous avons besoin d'un bloc d'instructions. Si votre macro ne renvoie qu'une simple expression, vous n'en aurez pas besoin.

Pattern matching encore plus poussé

En plus de simples "arguments", une macro peut en fait englober tout un code :

Runmacro_rules! modifier_struct {
    ($(struct $n:ident { $($name:ident: $content:ty,)+ } )+) => {
        $(struct $n { $($name: f32),+ })+
    };
}

modifier_struct! {
    struct Temperature {
        degree: u64,
    }

    struct Point {
        x: u32,
        y: u32,
        z: u32,
    }
}

fn main() {
    // error: expected f32, found u32
    let temp = Temperature { degree: 0u32 };
    // error: expected f32, found u32 (pour les 3 champs)
    let point = Point { x: 0u32, y: 0u32, z: 0u32 };
}

Ce code transforme tous les champs des structures en f32, et ce quel que soit le type initial.

Pas très utile mais ça vous permet de voir que les macros peuvent vraiment étendre les possibilités offertes par Rust.

Scope et exportation d'une macro

Créer des macros c’est bien, pouvoir s'en servir, c'est encore mieux ! Si vos macros sont déclarées dans un fichier à part (ce qui est une bonne chose !), il vous faudra ajouter cette ligne en haut du fichier où se trouvent vos macros :

Run#![macro_use]

Vous pourrez alors les utiliser dans votre projet.

Si vous souhaitez exporter des macros (parce qu'elles font partie d'une bibliothèque par exemple), il vous faudra ajouter au-dessus de la macro :

Run#[macro_export]

Enfin, si vous souhaitez utiliser des macros d'une des dépendances de votre projet, vous pourrez les importer comme cela :

Runuse nom_de_la_dependance::nom_de_la_macro;

À noter qu'avant, les imports de macros avaient besoin de #[macro_use] et ressemblaient à ceci :

Run#[macro_use]
extern crate nom_de_la_dependance;

Comme ça si jamais vous croisez ce genre de code, vous ne serez pas surpris.

Quelques macros utiles

En bonus, je vous donne une petite liste de macros qui pourraient vous être utiles :

Petite macro mais grande économie de lignes !

Pour clôturer ce chapitre, je vous propose le code suivant qui permet d'améliorer celui présenté dans le chapitre sur la généricité grâce à une macro :

Runmacro_rules! creer_animal {
    ($nom_struct:ident) => {
        struct $nom_struct {
            nom: String,
            nombre_de_pattes: usize
        }

        impl Animal for $nom_struct {
            fn get_nom(&self) -> &str {
                &self.nom
            }

            fn get_nombre_de_pattes(&self) -> usize {
                self.nombre_de_pattes
            }
        }
    }
}

trait Animal {
    fn get_nom(&self) -> &str;
    fn get_nombre_de_pattes(&self) -> usize;
    fn affiche(&self) {
        println!("Je suis un animal qui s'appelle {} et j'ai {} pattes !", self.get_nom(), self.get_nombre_de_pattes());
    }
}

creer_animal!(Chien);
creer_animal!(Chat);

fn main() {
    fn affiche_animal<T: Animal>(animal: T) {
        animal.affiche();
    }

    let chat = Chat { nom: "Félix".to_owned(), nombre_de_pattes: 4};
    let chien = Chien { nom: "Rufus".to_owned(), nombre_de_pattes: 4};

    affiche_animal(chat);
    affiche_animal(chien);
}

Je tiens cependant encore à préciser que nous n'avons vu ici que la base des macros : elles permettent de faire des choses nettement plus impressionnantes (certaines crates le démontrent d'ailleurs fort bien). Les possibilités étant quasiment infinies, il ne vous reste plus qu'à expérimenter de votre côté avec ce que nous avons vu ici.