Les macros

Nous avons déjà utilisé des macros tout au long de ce livre, comme println!, mais nous n'avons pas examiné en profondeur ce qu'est une macro et comment elles fonctionnent. Le terme macro renvoie à une famille de fonctionnalités de Rust : les macros déclaratives avec macro_rules! et trois types de macros procédurales :

  • Des macros #[derive] personnalisées qui renseigne du code ajouté grâce à l'attribut derive utilisé sur les structures et les énumérations
  • Les macros qui ressemblent à des attributs qui définissent des attributs personnalisés qui sont utilisables sur n'importe quel élément
  • Les macros qui ressemblent à des fonctions mais qui opèrent sur les éléments renseignés en argument

Nous allons voir chacune d'entre elles à leur tour, mais avant, posons-nous la question de pourquoi nous avons besoin de macros alors que nous avons déjà les fonctions.

La différence entre les macros et les fonctions

Essentiellement, les macros sont une façon d'écrire du code qui écrit un autre code, ce qui s'appelle la métaprogrammation. Dans l'annexe C, nous verrons l'attribut derive, qui génère une implémentation de différents traits pour vous. Nous avons aussi utilisé les macros println! et vec! dans ce livre. Toutes ces macros se déploient pour produire plus de code que celui que vous avez écrit manuellement.

La métaprogrammation est utile pour réduire la quantité de code que vous avez à écrire et à maintenir, ce qui est aussi un des rôles des fonctions. Cependant, les macros ont quelques pouvoirs en plus que les fonctions n'ont pas.

La signature d'une fonction doit déclarer le nombre et le type de paramètres qu'a cette fonction. Les macros, à l'inverse, peuvent prendre un nombre variable de paramètres : nous pouvons appeler println!("salut") avec un seul paramètre, ou println!("salut {}", nom) avec deux paramètres. De plus, les macros sont déployées avant que le compilateur n'interprète la signification du code, donc une macro peut, par exemple, implémenter un trait sur un type donné. Une fonction ne peut pas le faire, car elle est exécutée à l'exécution et un trait doit être implémenté à la compilation.

Le désavantage d'implémenter une macro par rapport à une fonction est que les définitions de macros sont plus complexes que les définitions de fonction car vous écrivez du code Rust qui écrit lui-même du code Rust. A cause de cette approche, les définitions de macro sont généralement plus difficiles à lire, à comprendre et à maintenir que les définitions de fonctions.

Une autre différence importante entre les macros et les fonctions est que vous devez définir les macros ou les importer dans la portée avant de les utiliser dans le fichier, contrairement aux fonctions que vous pouvez définir n'importe où et y faire appel n'importe où.

Les macros déclaratives avec macro_rules! pour la métaprogrammation générale

La forme la plus utilisée de macro en Rust est la macro déclarative. Elles sont parfois appelées “macros définies par un exemple”, “macros macro_rules!” ou simplement “macros”. Fondamentalement, les macros déclaratives vous permettent d'écrire quelque chose de similaire à une expression match de Rust. Comme nous l'avons vu au chapitre 6, les expressions match sont des structures de contrôle qui prennent en argument une expression, comparent la valeur qui en résulte avec les motifs et ensuite exécutent le code associé au motif qui correspond. Les macros comparent elles aussi une valeur avec des motifs qui sont associés à code particulier : dans cette situation, la valeur est littéralement le code source Rust envoyé à la macro ; les motifs sont comparés avec la structure de ce code source ; et le code associé à chaque motif vient remplacer le code passé à la macro, lorsqu'il correspond. Tout ceci se passe lors de la compilation.

Pour définir une macro, il faut utiliser la construction macro_rules!. Explorons l'utilisation de macro_rules! en observant comment la macro vec! est définie. Le chapitre 8 nous a permis de comprendre comment utiliser la macro vec! pour créer un nouveau vecteur avec des valeurs précises. Par exemple, la macro suivante crée un nouveau vecteur qui contient trois entiers :


#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}

Nous aurions pu aussi utiliser la macro vec! pour créer un vecteur de deux entiers ou un vecteur de cinq slices de chaînes de caractères. Nous n'aurions pas pu utiliser une fonction pour faire la même chose car nous n'aurions pas pu connaître le nombre ou le type des valeurs au départ.

L'encart 19-28 montre une définition légèrement simplifiée de la macro vec!.

Fichier : src/lib.rs

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

Encart 19-28 : une version simplifiée de la définition de la macro vec!

Remarque : la définition actuelle de la macro vec! de la bibliothèque standard embarque du code pour pré-allouer la bonne quantité de mémoire en amont. Ce code est une optimisation que nous n'allons pas intégrer ici pour simplifier l'exemple.

L'annotation #[macro_export] indique que cette macro doit être disponible à chaque fois que la crate dans laquelle la macro est définie est importée dans la portée. Sans cette annotation, la macro ne pourrait pas être importée dans la portée.

Ensuite, nous commençons la définition de la macro avec macro_rules! suivi du nom de la macro que nous définissons sans le point d'exclamation. Le nom, qui dans ce cas est vec, est suivi par des accolades indiquant le corps de la définition de la macro.

La structure dans le corps de vec! ressemble à la structure d'une expression match. Ici nous avons une branche avec le motif ( $( $x:expr ), * ), suivie par => et le code du bloc associé à ce motif. Si le motif correspond, le bloc de code associé sera déployé. Etant donné que c'est le seul motif dans cette macro, il n'y a qu'une seule bonne façon d'y correspondre ; tout autre motif va déboucher sur une erreur. Des macros plus complexes auront plus qu'une seule branche.

La syntaxe correcte pour un motif dans les définitions de macros est différente de la syntaxe de motif que nous avons vue au chapitre 18 car les motifs de macros sont comparés à des structures de code Rust plutôt qu'à des valeurs. Examinons la signification des éléments du motif de l'encart 19-28 ; pour voir l'intégralité de la syntaxe du motif de la macro, référez-vous à la documentation.

Premièrement, un jeu de parenthèses englobent l'intégralité du motif. Ensuite vient le symbole dollar ($), suivi par un jeu de parenthèses qui capturent les valeurs qui correspondent au motif entre les parenthèses pour les utiliser dans le code de remplacement. A l'intérieur du $() nous avons $x:expr, qui correspond à n'importe quelle expression Rust et donne le nom $x à l'expression.

La virgule qui suit le $() signifie que cette virgule littérale comme caractère littéral de séparation peut optionnellement apparaître après le code qui correspond au code du $(). Le * informe que ce motif correspond à zéro ou plus éléments répétés correspondant à ce qui précède ce *.

Lorsque nous faisons appel à cette macro avec vec![1, 2, 3];, le motif $x correspond à trois reprises avec les trois expressions 1, 2, et 3.

Maintenant, penchons-nous sur le motif dans le corps du code associé à cette branche : temp_vec.push() dans le $()* est généré pour chacune des parties qui correspondent au $() dans le motif pour zéro ou plus de fois, en fonction de combien de fois le motif correspond. Le $x est remplacé par chaque expression qui correspond. Lorsque nous faisons appel à cette macro avec vec![1, 2, 3];, le code généré qui remplace cet appel de macro ressemblera à ceci :

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

Nous avons défini une macro qui peut prendre n'importe quel nombre d'arguments de n'importe quel type et qui peut générer du code pour créer un vecteur qui contient les éléments renseignés.

Il subsiste quelques cas limites étranges avec macro_rules!. Bientôt, Rust rajoutera un second type de macro déclarative qui fonctionnera de la même manière mais qui corrigera ces cas limites. Après cette mise à jour, macro_rules! sera dépréciée. En sachant cela, ainsi que le fait que la plupart des développeurs Rust vont davantage utiliser les macros qu'en écrire, nous arrêtons là la discussion sur macro_rules!. Pour en apprendre plus sur l'écriture des macros, consultez la documentation en ligne ou d'autres ressources comme “The Little Book of Rust Macros”, débuté par Daniel Keep et continué par Lukas Wirth.

Les macros procédurales pour générer du code à partir des attributs

La seconde forme de macro est la macro procédurale, qui se comporte davantage comme une fonction (et est un type de procédure). Les macros procédurales prennent du code en entrée, travaillent sur ce code et produisent du code en sortie plutôt que de faire des correspondances sur des motifs et remplacer du code avec un autre code, comme le font les macros déclaratives.

Les trois types de macros procédurales (les dérivées personnalisées, celles qui ressemblent aux attributs, et celles qui ressemblent à des fonctions) fonctionnent toutes de la même manière.

Lorsque vous créez une macro procédurale, les définitions doivent être rangées dans leur propre crate avec un type spécial de crate. Ceci pour des raisons techniques complexes que nous espérons supprimer dans l'avenir. La déclaration des macros procédurales ressemble au code de l'encart 19-29, dans lequel un_attribut_quelconque est un emplacement pour l'utilisation d'une macro spécifique.

Fichier : src/lib.rs

use proc_macro;

#[un_attribut_quelconque]
pub fn un_nom_quelconque(entree: TokenStream) -> TokenStream {
}

Encart 19-29 : un exemple de déclaration d'une macro procédurale

La fonction qui définit une macro procédurale prend un TokenStream en entrée et produit un TokenStream en sortie. Le type TokenStream est défini par la crate proc_macro qui est fournie par Rust et représente une séquence de jetons. C'est le cœur de la macro : le code source sur lequel la macro opère compose l'entrée TokenStream, et le code que la macro produit est la sortie TokenStream. La fonction a aussi un attribut qui lui est rattaché et qui indique quel genre de macro procédurale nous créons. Nous pouvons avoir différents types de macros procédurales dans la même crate.

Voyons maintenant les différents types de macros procédurales. Nous allons commencer par une macro dérivée personnalisée et nous expliquerons ensuite les petites différences avec les autres types.

Comment écrire une macro dérivée personnalisée

Créons une crate hello_macro qui définit un trait qui s'appelle HelloMacro avec une fonction associée hello_macro. Plutôt que de contraindre les utilisateurs de notre crate à implémenter le trait HelloMacro sur chacun de leurs types, nous allons fournir une macro procédurale qui permettra aux utilisateurs de pouvoir annoter leur type avec #[derive(HelloMacro)] afin d'obtenir une implémentation par défaut de la fonction hello_macro. L'implémentation par défaut affichera Hello, Macro ! Mon nom est TypeName !, dans lequel TypeName est le nom du type sur lequel ce trait a été défini. Autrement dit, nous allons écrire une crate qui permet à un autre développeur d'écrire du code comme l'encart 19-30 en utilisant notre crate.

Fichier : src/main.rs

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}

Encart 19-30 : le code qu'un utilisateur de notre crate pourra écrire lorsqu'il utilisera notre macro procédurale

Ce code va afficher Hello, Macro ! Mon nom est Pancakes ! lorsque vous en aurez fini. La première étape consiste à créer une nouvelle crate de bibliothèque, comme ceci :

$ cargo new hello_macro --lib

Ensuite, nous allons définir le trait HelloMacro et sa fonction associée :

Fichier : src/lib.rs

pub trait HelloMacro {
    fn hello_macro();
}

Nous avons maintenant un trait et sa fonction. A partir de là, notre utilisateur de la crate peut implémenter le trait pour accomplir la fonctionnalité souhaitée, comme ceci :

use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro ! Mon nom est Pancakes !");
    }
}

fn main() {
    Pancakes::hello_macro();
}

Cependant, l'utilisateur doit écrire le bloc d'implémentation pour chacun des types qu'il souhaite utiliser avec hello_macro ; nous souhaitons lui épargner ce travail.

De plus, nous ne pouvons pas encore fournir la fonction hello_macro avec l'implémentation par défaut qui va afficher le nom du type du trait sur lequel nous l'implémentons : Rust n'est pas réflexif, donc il ne peut pas connaître le nom du type à l'exécution. Nous avons besoin d'une macro pour générer le code à la compilation.

La prochaine étape consiste à définir la macro procédurale. Au moment de l'écriture de ces lignes, les macros procédurales ont besoin d'être placées dans leur propre crate. Cette restriction sera levée plus tard. La convention pour structurer les crates et les crates de macros est la suivante : pour une crate foo, une crate de macro procédurale personnalisée de dérivée doit s'appeler foo_derive. Créons une nouvelle crate hello_macro_derive au sein de notre projet hello_macro :

$ cargo new hello_macro_derive --lib

Nos deux crates sont étroitement liées, donc nous créons la crate de macro procédurale à l'intérieur du dossier de notre crate hello_macro. Si nous changeons la définition du trait dans hello_macro, nous aurons aussi à changer l'implémentation de la macro procédurale dans hello_macro_derive. Les deux crates vont devoir être publiées séparément, et les développeurs qui vont utiliser ces crates vont avoir besoin d'ajouter les deux dépendances et les importer dans la portée. Nous pourrions plutôt faire en sorte que la crate hello_macro utilise hello_macro_derive comme dépendance et ré-exporter le code de la macro procédurale. Cependant, la façon dont nous avons structuré le projet donne la possibilité aux développeurs d'utiliser hello_macro même s'ils ne veulent pas la fonctionnalité derive.

Nous devons déclarer la crate hello_macro_derive comme étant une crate de macro procédurale. Nous allons aussi avoir besoin des fonctionnalités des crates syn et quote, comme vous allez le constater bientôt, donc nous allons les ajouter comme dépendances. Ajoutez ceci dans le fichier Cargo.toml de hello_macro_derive :

Fichier : hello_macro_derive/Cargo.toml

[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"

Pour commencer à définir la macro procédurale, placez le code de l'encart 19-31 dans votre fichier src/lib.rs de la crate hello_macro_derive. Notez que ce code ne se compilera pas tant que nous n'ajouterons pas une définition pour la fonction impl_hello_macro.

Fichier : hello_macro_derive/src/lib.rs

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construit une représentation du code Rust en arborescence
    // syntaxique que nous pouvons manipuler
    let ast = syn::parse(input).unwrap();

    // Construit l'implémentation du trait
    impl_hello_macro(&ast)
}

Encart 19-31 : du code dont la plupart des macros procédurales auront besoin pour travailler avec du code Rust

Remarquez que nous avons séparé le code de la fonction hello_macro_derive, qui est responsable de parcourir le TokenStream, de celui de la fonction impl_hello_macro, qui est responsable de transformer l'arborescence syntaxique : cela facilite l'écriture de la macro procédurale. Le code dans la fonction englobante (qui est hello_macro_derive dans notre cas) sera le même pour presque toutes les crates de macro procédurales que vous allez voir ou créer. Le code que vous renseignez dans le corps de la fonction (qui est impl_hello_macro dans notre cas) diffèrera en fonction de ce que fait votre macro procédurale.

Nous avons ajouté trois nouvelles crates : proc_macro, syn et quote. La crate proc_macro est fournie par Rust, donc nous n'avons pas besoin de l'ajouter aux dépendances dans Cargo.toml. La crate proc_macro fournit une API du compilateur qui nous permet de lire et manipuler le code Rust à partir de notre code.

La crate syn transforme le code Rust d'une chaîne de caractères en une structure de données sur laquelle nous pouvons procéder à des opérations. La crate quote re-transforme les structures de données de syn en code Rust. Ces crates facilite le parcours de toute sorte de code Rust que nous aurions besoin de gérer : l'écriture d'un interpréteur complet de code Rust n'a jamais été aussi facile.

La fonction hello_macro_derive va être appelée lorsqu'un utilisateur de notre bibliothèque utilisera #[derive(HelloMacro)] sur un type. Cela sera possible car nous avons annoté notre fonction hello_macro_derive avec proc_macro_derive et nous avons indiqué le nom, HelloMacro, qui correspond au nom de notre trait ; c'est la convention que la plupart des macros procédurales suivent.

La fonction hello_macro_derive commence par convertir le input qui est un TokenStream en une structure de données que nous pouvons ensuite interpréter et sur laquelle faire des opérations. C'est là que syn entre en jeu. La fonction parse de syn prend un TokenStream et retourne une structure DeriveInput qui représente le code Rust. L'encart 19-32 montre les parties intéressantes de la structure DeriveInput que nous obtenons en convertissant la chaîne de caractères struct Pancakes; :

DeriveInput {
    // -- partie masquée ici --

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}

Encart 19-32 : l'instance de DeriveInput que nous obtenons lorsque nous analysons le code qui est décoré par l'attribut de la macro dans l'encart 19-30

Les champs de cette structure montrent que ce code Rust que nous avons converti est une structure unitaire avec l'ident (raccourci de identifier, qui désigne le nom) Pancakes. Il y a d'autres champs sur cette structure décrivant toutes sortes de codes Rust ; regardez la documentation de syn pour DeriveInput pour en savoir plus.

Bientôt, nous définirons la fonction impl_hello_macro, qui nous permettra de construire le nouveau code Rust que nous souhaitons injecter. Mais avant de faire cela, remarquez que la sortie de notre macro derive est aussi un TokenStream. Le TokenStream retourné est ajouté au code que les utilisateurs de notre crate ont écrit, donc lorsqu'ils compilent leur crate, ils récupéreront la fonctionnalité additionnelle que nous injectons dans le TokenStream modifié.

Vous avez peut-être remarqué que nous faisons appel à unwrap pour faire paniquer la fonction hello_macro_derive si l'appel à la fonction syn::parse que nous faisons échoue. Il est nécessaire de faire paniquer notre macro procédurale si elle rencontre des erreurs car les fonctions proc_macro_derive doivent retourner un TokenStream plutôt qu'un Result pour se conformer à l'API de la macro procédurale. Nous avons simplifié cet exemple en utilisant unwrap ; dans du code en production, vous devriez renseigner des messages d'erreur plus précis sur ce qui s'est mal passé en utilisant panic! ou expect.

Maintenant que nous avons le code pour transformer le code Rust annoté d'un TokenStream en une instance de DeriveInput, créons le code qui implémente le trait HelloMacro sur le type annoté, comme montré dans l'encart 19-33.

Fichier : hello_macro_derive/src/lib.rs

use proc_macro::TokenStream;
use quote::quote;
use syn;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construit une représentation du code Rust en arborescence
    // syntaxique que nous pouvons manipuler
    let ast = syn::parse(input).unwrap();

    // Construit l'implémentation du trait
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let nom = &ast.ident;
    let generation = quote! {
        impl HelloMacro for #nom {
            fn hello_macro() {
                println!("Hello, Macro ! Mon nom est {}", stringify!(#nom));
            }
        }
    };
    generation.into()
}

Encart 19-33 : implémentation du trait HelloMacro en utilisant le code Rust interprété

Nous obtenons une instance de structure Ident qui contient le nom (identifier) du type annoté en utilisant ast.ident. La structure de l'encart 19-32 montre que lorsque nous exécutons la fonction impl_hello_macro sur le code de l'encart 19-30, le ident que nous obtenons aura le champ ident avec la valeur "Pancakes". Ainsi, la variable nom de l'encart 19-33 contiendra une instance de la structure Ident qui, une fois affichée, sera la chaîne de caractères "Pancakes", le nom de la structure de l'encart 19-30.

La macro quote! nous permet de définir le code Rust que nous souhaitons retourner. Le compilateur attend quelque chose de différent que le résultat direct produit par l'exécution de quote!, donc nous devons convertir ce dernier en TokenStream. Nous faisons ceci en faisant appel à la méthode into, qui utilise cette représentation intermédiaire et retourne une valeur du type attendu, le type TokenStream ici.

La macro quote! fournit aussi quelques mécaniques de gabarit intéressantes : nous pouvons entrer #nom, et quote! va le remplacer avec la valeur présente dans la variable nom. Vous pouvez même exécuter des répétitions d'une façon similaire à celle des macros classiques. Regardez dans la documentation de quote pour une présentation plus détaillée.

Nous souhaitons que notre macro procédurale génère une implémentation de notre trait HelloMacro pour le type que l'utilisateur a annoté, que nous pouvons obtenir en utilisant #nom. L'implémentation du trait utilise une fonction, hello_macro, dont le corps contient la fonctionnalité que nous souhaitons fournir : l'affichage de Hello, Macro ! Mon nom est suivi par le nom du type annoté.

La macro stringify! utilisée ici est écrite en Rust. Elle prend en argument une expression Rust, comme 1 + 2, et à la compilation transforme l'expression en une chaîne de caractères littérale, comme "1 + 2". Cela est différent de format! ou de println!, des macros qui évaluent l'expression et retourne ensuite le résultat dans une String. Il est possible que l'entrée #nom soit une expression à écrire littéralement, donc nous utilisons stringify!. L'utilisation de stringify! évite aussi une allocation en convertissant #nom en une chaine de caractères littérale à la compilation.

Maintenant, cargo build devrait fonctionner correctement pour hello_macro et hello_macro_derive. Relions maintenant ces crates au code de l'encart 19-30 pour voir les macros procédurales à l'oeuvre ! Créez un nouveau projet binaire dans votre dossier projects en utilisant cargo new pancakes. Nous avons besoin d'ajouter hello_macro et hello_macro_derive comme dépendances dans le Cargo.toml de la crate pancakes. Si vous publiez vos versions de hello_macro et de hello_macro_derive sur crates.io, ce seront des dépendances classiques ; sinon, vous pouvez les indiquer en tant que dépendances locales avec path comme ci-après :

hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

Renseignez le code l'encart 19-30 dans src/main.rs, puis lancez cargo run : cela devrait afficher Hello, Macro ! Mon nom est Pancakes !. L'implémentation du trait HelloMacro à l'aide de la macro procédurale a été incluse sans que la crate pancakes n'ait eu besoin de l'implémenter ; le #[derive(HelloMacro)] a ajouté automatiquement l'implémentation du trait.

Maintenant, découvrons comment les autres types de macros procédurales se distinguent des macros derive personnalisées.

Les macros qui ressemblent à des attributs

Les macros qui ressemblent à des attributs ressemblent aux macros derive personnalisées, mais au lieu de générer du code pour l'attribut derive, elles vous permettent de créer des nouveaux attributs. Elles sont aussi plus flexibles : derive fonctionne uniquement pour les structures et les énumérations ; les attributs peuvent être aussi appliqués aux autres éléments, comme les fonctions. Voici un exemple d'utilisation d'une macro qui ressemble à un attribut : imaginons que vous avez un attribut chemin qui est une annotation pour des fonctions lorsque vous utilisez un environnement de développement d'application web :

#[chemin(GET, "/")]
fn index() {

Cet attribut #[chemin] sera défini par l'environnement de développement comme étant une macro procédurale. La signature de la fonction de définition de la macro ressemblera à ceci :

#[proc_macro_attribute]
pub fn chemin(attribut: TokenStream, element: TokenStream) -> TokenStream {

Maintenant, nous avons deux paramètres de type TokenStream. Le premier correspond au contenu de l'attribut : la partie GET, "/". Le second est le corps de l'élément sur lequel cet attribut sera appliqué : dans notre cas, fn index() {} et le reste du corps de la fonction.

Mis à part cela, les macros qui ressemblent à des attributs fonctionnent de la même manière que les macros derive personnalisées : vous générez une crate avec le type de la crate proc-macro et vous implémentez une fonction qui génèrera le code que vous souhaitez !

Les macros qui ressemblent à des fonctions

Les macros qui ressemblent à des fonctions définissent des macros qui ressemblent à des appels de fonction. De la même manière que les macros macro_rules!, elles sont plus flexibles que les fonctions ; par exemple, elles peuvent prendre une quantité non finie d'arguments. Cependant, les macros macro_rules! peuvent être définies uniquement en utilisant la syntaxe qui ressemble à match et que nous avons vue dans une section précédente. Les macros qui ressemblent à des fonctions prennent en paramètre un TokenStream et leurs définitions manipulent ce TokenStream en utilisant du code Rust comme le font les deux autres types de macros procédurales. Voici un exemple d'une macro qui ressemble à une fonction qui est une macro sql! qui devrait être utilisée comme ceci :

let sql = sql!(SELECT * FROM publications WHERE id=1);

Cette macro devrait interpréter l'instruction SQL qu'on lui envoie et vérifier si elle est syntaxiquement correcte, ce qui est un procédé bien plus complexe que ce qu'une macro macro_rules! peut faire. La macro sql! sera définie comme ceci :

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

Cette définition ressemble à la signature de la macro derive personnalisée : nous récupérons les éléments entre parenthèses et retournons le code que nous souhaitons générer.

Résumé

Ouah ! Maintenant vous avez quelques fonctionnalités de Rust supplémentaires dans votre boite à outils que vous n'utiliserez probablement que rarement, mais vous savez maintenant qu'elles pourront vous aider dans certaines situations très particulières. Nous avons introduits plusieurs sujets complexes afin que vous puissiez les reconnaître, ainsi que la syntaxe associée, lorsque vous les rencontrerez dans des messages de suggestions dans des erreurs ou dans le code de quelqu'un d'autre. Utilisez ce chapitre comme référence pour vous guider vers ces solutions.

Au chapitre suivant, nous allons mettre en pratique tout ce que nous avons appris dans ce livre en l'appliquant à un nouveau projet !