Die Software von Qumulo ist seit einiger Zeit vollständig in C geschrieben. In den letzten Jahren haben wir mit Rust geflirtet und die vielen Vorteile, die es zu bieten hat - darüber habe ich bereits geschrieben hier.

Kürzlich haben wir daran gearbeitet, unseren eigenen LDAP-Client zu schreiben, und ein Teil davon hat unsere eigene ASN.1 BER-Serialisierungsbibliothek geschrieben. Dank einer häufig übersehenen Killer-Funktion von Rust: Makros konnten wir eine idiomatische Methode zur Generierung von Serialisierung bereitstellen.

Makros waren schon lange vor Rust Bestandteil vieler Programmiersprachen. Sie werden in der Regel vor der normalen Kompilierungsphase und manchmal von einem vollständig separaten Programm (Präprozessor genannt) ausgewertet. Sie werden verwendet, um mehr Nicht-Makro-Code zu generieren, der in der normalen Kompilierungsphase kompiliert werden soll. Hier bei Qumulo kennen wir uns mit Makros aus C aus, obwohl sich C-Makros sehr von Makros in Rust unterscheiden. C-Makros basieren auf Prinzipien des Text-Parsings, während Rust-Makros verwendet werden Token.

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

Tatsächlich gibt es in Rust zwei verschiedene Arten von Makros: solche, die ähnlich wie C-Makros definiert sind und ausgeführt werden (siehe oben), und eine andere Art namens Prozedurale Makros. Diese Makros sind in Rust selbst geschrieben (anstelle einer Makrosprache). Sie werden in Plugins (dynamische Bibliotheken) kompiliert und beim Kompilieren vom Compiler ausgeführt. Dies bedeutet, dass wir mit Leichtigkeit wesentlich kompliziertere Makros schreiben können. Sie versorgen populäre Bibliotheken wie serde oder rocket mit Strom und sind die Art von Makros, die wir in unserer Serialisierungsbibliothek verwendet haben. Sie kommen in ein paar Typen, auf die ich mich konzentrieren werde prozedurale Makros ableiten, die im Allgemeinen verwendet werden, um ein Merkmal für einen Typ automatisch zu implementieren.

Merkmal Hallo {fn hallo (); } // Foobar implementiert jetzt das Hallo-Merkmal, und wir mussten keinen // Impl-Block schreiben! # [ableiten (Hallo)] struct FooBar;

Im Kern ist ein prozedurales Makro nur eine Funktion, die einen Token-Stream als Eingabe und einen Token-Stream als Ausgabe verwendet. Sie neigen dazu, zwei verschiedene Phasen zu haben, die ich eine Analysephase und eine Erzeugungsphase nennen werde.

Da wir nur einen Strom von Tokens erhalten, müssen wir diese zuerst analysieren, wenn wir strukturelle Informationen über die Tokens erhalten möchten. Dies kann mit erfolgen syn. Während der Analysephase werden Token in Syn-Typen und anschließend in alle Zwischentypen des Makros analysiert.

Während der Generierungsphase werden die Typen aus der Analysephase genutzt, um neuen Code zu generieren. Das zitieren Bibliothek kann verwendet werden, um einige Vorlagen zu erstellen und einen Token-Stream zu generieren. Im Allgemeinen geben sie mit abgeleiteten prozeduralen Makros Code aus, der ein Merkmal implementiert.

benutze proc_macro :: TokenStream; benutze syn :: {parse_macro_input, DeriveInput}; benutze quote :: quote; # [proc_macro_derive (Hello)] pub fn derive_hello (Eingabe: TokenStream) -> TokenStream {// Analysephase let derive_input = parse_macro_input! (Eingabe als DeriveInput); let ident = & derive_input.ident; let name = derive_input.ident.to_string (); // Phase generieren (quote! {Impl Hello für #ident {fn hello () {println! ("Hello from {}", #name);}}}). Into ()}


Ableiten von Prozedurmakros kann viel Arbeit sparen. Anstatt dasselbe Merkmal für viele verschiedene Typen zu implementieren, können wir ein Makro schreiben, das das Merkmal für jeden Typ implementiert.

benutze ber :: {Encode, Decode, DefaultIdentifier}; # [ableiten (Encode, PartialEq, Debug, DefaultIdentifier, Decode)] struct StructPrimitives {x: u32, is_true: bool, negativ: i32,} # [test] fn encode () {let s = StructPrimitives {x: 42, is_true : true, negative: -42}; // Wir haben eine Codierungsmethode dank Makros! let mut encoded = vec! []; am encode (& mut encoded) .unwrap (); }

Unsere Serialisierungsbibliothek kann die Struktur StructPrimitives codieren, obwohl wir die Implementierung von Encode nicht explizit geschrieben haben. Dank des prozeduralen Makros kann die Bibliothek es selbst schreiben.

Die Unterstützung durch den Compiler für prozedurale Makros sowie die großartigen begleitenden Bibliotheken wie syn und quote machen das Schreiben in Rust reibungslos. Prozedurale Makros sind einer der vielen Gründe, warum ich froh bin, dass wir uns für Rust entschieden haben. Wir haben sie bereits für eine Vielzahl von Verwendungszwecken eingesetzt, und ich bin sicher, wir werden weiterhin nach neuen Verwendungszwecken suchen.

Ebenfalls! Markieren Sie Ihre Kalender! Bitte besuchen Sie uns in Qumulo am Dienstag, Aug. 13 um 6: 30 pm für die monatliches Seattle Rust Meetup. Essen und Trinken wird gestellt!

Qumulos Collin Wallace wird Folgendes diskutieren: „Qumulo hatte eine große C-Codebasis mit ein wenig Magie, um Methoden, Schnittstellen und eine begrenzte Form von Merkmalen / Generika zuzulassen. Jetzt haben wir eine gemischte C + Rust-Codebasis, und wir waren wählerisch, dass der neue Rust-Code idiomatisch ist, * ohne * das Gefühl zu haben, dass der C-Code, mit dem er verknüpft ist, fehl am Platz ist. In diesem Vortrag werde ich den Weg untersuchen, den wir eingeschlagen haben, um dies zu erreichen. Dabei werden prozedurale Makros, Compiler-Plugins, automatisch generierte C-Header und einige durchdachte Testansätze verwendet. “

Teilen Sie mit Ihrem Netzwerk

GET A DEMO