Azure Native Qumulo Maintenant disponible dans l'UE, au Royaume-Uni et au Canada - En savoir plus

Écrire des macros procédurales en rouille

Rédigé par:

Le logiciel de Qumulo est entièrement écrit en C depuis un certain temps. Ces dernières années, nous flirtons avec Rust et les nombreux avantages qu'il offre - j'ai déjà écrit à ce sujet ici.

Récemment, nous avons travaillé sur l'écriture de notre propre client LDAP, et une partie de cela a été l'écriture de notre propre bibliothèque de sérialisation ASN.1 BER. Nous avons été en mesure de fournir un moyen idiomatique de générer de la sérialisation grâce à une fonctionnalité redoutable souvent négligée de Rust: macros.

Les macros ont été un élément de nombreux langages de programmation bien avant la présence de Rust. Ils ont tendance à être évalués avant la phase de compilation normale, et parfois par un programme complètement séparé (appelé préprocesseur). Ils sont utilisés pour générer plus de code non-macro à compiler lors de la phase de compilation normale. Chez Qumulo, nous connaissons bien les macros de C, bien que les macros de C soient très différentes des macros de Rust. Les macros C fonctionnent selon les principes de l'analyse de texte, tandis que les macros Rust fonctionnent tokens.

// Macro C #define LOCATION () ("file:" __FILE__) // Macro_rules de macro Rust! location {() => {concat! ("fichier:", fichier! ())}}

En fait, il existe deux types de macros dans Rust: les macros définies et exécutées de la même manière que les macros C (voir ci-dessus) et un autre type appelé macros procédurales. Ces macros sont écrites dans Rust lui-même (au lieu d'un langage de macro). Ils sont compilés dans des plugins (bibliothèques dynamiques) et exécutés par le compilateur lors de la compilation. Cela signifie que nous pouvons facilement écrire des macros bien plus compliquées. Ils alimentent des bibliothèques populaires telles que serde ou rocket, et constituent le type de macros que nous avons utilisé dans notre bibliothèque de sérialisation. Ils viennent dans quelques types, je vais me concentrer sur dériver des macros procédurales, qui sont généralement utilisés pour implémenter automatiquement un trait pour un type.

trait Bonjour {fn bonjour (); } // Foobar implémente le trait Hello maintenant, et nous n'avons pas eu à écrire // un bloc impl! # [derive (Hello)] struct FooBar;

En gros, une macro procédurale est simplement une fonction qui prend un flux de jetons en entrée et donne un flux de jetons en sortie. Ils ont tendance à avoir deux phases distinctes que j'appellerai une phase d'analyse et une phase de génération.

Puisque nous ne recevons qu'un flot de jetons, si nous voulons obtenir des informations structurelles sur les jetons, nous devons d’abord les analyser. Cela peut être fait en utilisant syn. Pendant la phase d'analyse, les jetons sont analysés en types syn, puis en types intermédiaires de la macro.

Au cours de la phase de génération, les types de la phase d’analyse sont exploités pour générer un nouveau code. le Devis La bibliothèque peut être utilisée pour créer des modèles et générer un flux de jetons. Généralement, avec les macros procédurales dérivées, ils produisent du code qui implémente un trait.

utilisez proc_macro :: TokenStream; utilisez syn :: {parse_macro_input, DeriveInput}; utilisez quote :: quote; # [proc_macro_derive (Hello)] pub fn derive_hello (entrée: TokenStream) -> TokenStream {// Phase d'analyse let derive_input = parse_macro_input! (entrée comme DeriveInput); laissez ident = & derive_input.ident; laissez name = derive_input.ident.to_string (); // Générer la phase (quote! {Impl Hello for #ident {fn hello () {println! ("Hello from {}", #name);}}}). Into ()}


Dériver des macros procédurales peut économiser beaucoup de travail. Plutôt que de mettre en œuvre le même trait pour plusieurs types différents, nous pouvons écrire une macro qui implémente le trait pour tout type.

utilisez ber :: {Encode, Decode, DefaultIdentifier}; # [derive (Encode, PartialEq, Debug, DefaultIdentifier, Decode)] struct StructPrimitives {x: u32, is_true: bool, negative: i32,} # [test] fn encode () {let s = StructPrimitives {x: 42, is_true : vrai, négatif: -42}; // Nous avons une méthode d'encodage grâce aux macros! let mut encoded = vec! []; s.encode (& mut encodé) .unwrap (); }

Notre bibliothèque de sérialisation est capable de coder la structure StructPrimitives même si nous n'avons pas explicitement écrit l'implémentation de Encode. Grâce à la macro procédurale, la bibliothèque est capable de l'écrire elle-même.

La prise en charge par le compilateur des macros procédurales, ainsi que des excellentes bibliothèques telles que syn et quote, facilite leur écriture dans Rust smooth. Les macros procédurales sont l'une des nombreuses raisons pour lesquelles je suis heureux d'avoir choisi Rust. Nous les avons déjà exploités pour diverses utilisations et je suis sûr que nous continuerons à en trouver de nouvelles.

Également! Marquez vos calendriers! S'il vous plaît joindre à nous à Qumulo sur Mardi août 13 à 6: 30 pm pour le Meetup mensuel sur la rouille de Seattle. La nourriture et les boissons seront fournies!

Collin Wallace de Qumulo discutera de ce qui suit: «Qumulo avait une grande base de code C avec un peu de magie pour permettre des méthodes, des interfaces et une forme limitée de traits / génériques. Maintenant, nous avons une base de code mixte C + Rust, et nous avons été pointilleux pour que le nouveau code Rust soit idiomatique * sans * nous sentir déplacé par rapport au code C auquel il est lié. Dans cette présentation, j'explorerai le chemin que nous avons utilisé pour y parvenir, qui tire parti des macros procédurales, des plugins de compilateur, des en-têtes C générés automatiquement et de certaines approches réfléchies du test.

Contactez-nous ici si vous souhaitez organiser une réunion ou demander une démonstration. Et Abonnez-vous à notre blog pour des meilleures pratiques et ressources plus utiles!

Articles Similaires

Remonter en haut