Azure Native Qumulo jetzt in der EU, im Vereinigten Königreich und in Kanada verfügbar – Erfahren Sie mehr

Prozedurale Makros in Rust schreiben

Geschrieben von:

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

Vor kurzem haben wir daran gearbeitet, unseren eigenen LDAP-Client zu schreiben, und ein Teil davon war das Schreiben unserer eigenen ASN.1 BER-Serialisierungsbibliothek. Dank eines oft übersehenen Killerfeatures von Rust konnten wir eine idiomatische Art der Serialisierung bereitstellen: Makros.

Makros waren ein Element vieler Programmiersprachen, lange bevor es Rust gab. Sie werden in der Regel vor der normalen Kompilierungsphase und manchmal von einem völlig separaten Programm (einem so genannten Präprozessor) ausgewertet. Sie werden verwendet, um mehr Nicht-Makrocode zu generieren, der von der normalen Kompilierungsphase kompiliert werden soll. Hier bei Qumulo sind wir mit Makros aus C ziemlich vertraut, obwohl sich C-Makros sehr von Makros in Rust unterscheiden. C-Makros arbeiten nach Prinzipien der Textanalyse, während Rust-Makros nach ihnen arbeiten Token.

// C-Makro #define LOCATION() („Datei: ” __FILE__) // Rost-Makro 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, die aufgerufen wird prozedurale Makros. Diese Makros sind in Rust selbst geschrieben (anstelle einer Makrosprache). Sie werden in Plugins (dynamische Bibliotheken) kompiliert und vom Compiler beim Kompilieren ausgeführt. Das bedeutet, dass wir mit Leichtigkeit wesentlich kompliziertere Makros schreiben können. Sie unterstützen beliebte Bibliotheken wie serde oder rocket und sind die Art von Makros, die wir in unserer Serialisierungsbibliothek verwendet haben. Es gibt sie 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.

Eigenschaft Hallo {fn hallo(); } // Foobar implementiert jetzt die Hello-Eigenschaft, 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 nimmt und einen Token-Stream als Ausgabe zurückgibt. Sie neigen dazu, zwei unterschiedliche Phasen zu haben, die ich eine Analysephase und eine Generierungsphase nennen werde.

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

Während der Generierungsphase werden die Typen aus der Analysephase genutzt, um neuen Code zu generieren. Das Angebot! Die Bibliothek kann verwendet werden, um Vorlagen zu erstellen und einen Token-Stream zu generieren. Im Allgemeinen geben prozedurale Makros Code aus, der eine Eigenschaft implementiert.

Verwenden Sie proc_macro::TokenStream; benutze syn::{parse_macro_input, DeriveInput}; verwenden Sie Zitat::Zitat; #[proc_macro_derive(Hallo)] pub fn derive_hello(input: TokenStream) -> TokenStream { // Parse-Phase let derive_input = parse_macro_input!(input as DeriveInput); let ident = &derive_input.ident; let name = derive_input.ident.to_string(); // Phase erzeugen (quote! { impl Hallo für #ident { fn hello() { println!("Hallo von {}", #name); } } }).into() }


Das Ableiten prozeduraler Makros kann viel Arbeit sparen. Anstatt dieselbe Eigenschaft für viele verschiedene Typen zu implementieren, können wir ein Makro schreiben, das die Eigenschaft für jeden Typ implementiert.

use 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 : wahr, negativ: -42 }; // Dank Makros haben wir eine Kodierungsmethode! let mut encoded = vec![]; s.encode(&mut codiert).unwrap(); }

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

Die Unterstützung des Compilers 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 Anwendungen genutzt, und ich bin sicher, wir werden immer wieder neue Anwendungen finden.

Ebenfalls! Markieren Sie Ihre Kalender! Bitte schließen Sie sich uns bei Qumulo an Dienstag, 13. August um 6:30 Uhr für die monatliches Seattle Rust Meetup. Für Essen und Trinken ist gesorgt!

Collin Wallace von Qumulo wird Folgendes diskutieren: „Qumulo hatte eine große C-Codebasis mit ein wenig Magie, um Methoden, Schnittstellen und eine begrenzte Form von Merkmalen/Generika zu ermöglichen. Jetzt haben wir eine gemischte C+Rust-Codebasis, und wir waren wählerisch, dass neuer Rust-Code idiomatisch ist, *ohne* sich fehl am Platz für den C-Code zu fühlen, mit dem er verknüpft ist. In diesem Vortrag werde ich den Weg untersuchen, den wir verwendet haben, um dies zu erreichen, der prozedurale Makros, Compiler-Plug-ins, automatisch generierte C-Header und einige durchdachte Testansätze nutzt.“

Kontaktieren Sie uns hier, wenn Sie möchten ein Meeting einrichten oder eine Demo anfordern. Und Abonnieren Sie unseren Blog für weitere hilfreiche Best Practices und Ressourcen!

Verwandte Artikel

Nach oben scrollen