Le logiciel de Qumulo a été entièrement écrit en C pendant un moment. Au cours des dernières années, nous avons flirté avec Rust et ses nombreux avantages. 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 jetons.

// C macro #define LOCATION () (“fichier:” __FILE__) // macro macro_rules de rouille! 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 hello (); } // Foobar implémente maintenant le trait Hello 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.

use proc_macro :: TokenStream; use syn :: {parse_macro_input, DeriveInput}; utilisez quote :: quote; # [proc_macro_derive (Hello)] pub fn derive_hello (input: TokenStream) -> TokenStream {// Phase d'analyse laisser derive_input = parse_macro_input! (entrée comme DeriveInput); let ident = & derive_input.ident; let name = derive_input.ident.to_string (); // Generate Phase (quote! {Impl Hello pour #ident {fn hello () {println! ("Bonjour de {}", #name);}}}). Dans ()}


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.

use ber :: {Encode, Decode, DefaultIdentifier}; # [derive (Encode, PartialEq, Debug, DefaultIdentifier, Decode)] struct StructPrimitives {x: u32, is_true: bool, négatif: i32,} # [test] fn encoder () {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 codé) .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. Nous avons maintenant une base de code mixte C + Rust, et nous sommes conscients que le nouveau code Rust doit être idiomatique * sans * se sentir déplacé par rapport au code C auquel il est lié. Dans cette présentation, je vais explorer le chemin que nous avons utilisé pour atteindre cet objectif, qui utilise des macros procédurales, des plugins de compilateur, des en-têtes C générés automatiquement et des méthodes de test réfléchies. "

Partager avec votre réseau