Rust

Any donation is very welcome
Fork me on GitHub

II. Spécificités de Rust

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

Fonctionnement

Nous rentrons maintenant dans le vif du sujet : une macro est définie au travers d’une série de règles qui sont des conditions de 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 !");
    }
}

fn main() {
    dire_bonjour!();
}

Et on obtient :

Bonjour !

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

Les arguments

Bien évidemment, les macros peuvent recevoir des arguments. Petit exemple :

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

fn main() {
    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 appellée x en paramètre. Après il nous a juste suffit de l'afficher. Maintenant on va ajouter la possibilité de passer un deuxième argument :

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

fn main() {
    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 "match" sur les arguments.

Les différents types d'argument

Comme vous vous en doutez, il y a d'autres types en plus des expr. En voici la liste complète :

  • ident : une identification. 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 : un motif (ou "pattern"). Exemples : "Some(t)"; "(17, 'a')"; "_".
  • 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 "meta item", comme les attributs. Exemple : "cfg(target_os = "windows")".
  • tt : un élément un peu plus global, il peut contenir toute une expression.

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()
    }
}

fn main() {
    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 pu mettre un ';' si on l'avait voulu. D'ailleurs pourquoi ne pas essayer ?

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

fn main() {
    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 un slice qui est ensuite transformé 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);
    )
}

fn main() {
    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 goût. 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. Exemple :

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() {
    let temp = Temperature { degree: 0u32 };
    // error: expected f32, found u32
    let point = Point { x: 0u32, y: 0u32, z: 0u32 };
    // error: expected f32, found u32 (pour les 3 champs)
}

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 (car 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 :

Run#[macro_use]
extern crate nom_de_la_dependance;

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