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'attributderive
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
}
};
}
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 {
}
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();
}
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)
}
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
)
}
)
}
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()
}
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 !