Rust-Makros in LabEx erkunden

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Makros. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab erkunden wir das Konzept von Makros in Rust, einschließlich deklarativer Makros mit macro_rules! und drei Arten von prozeduralen Makros: benutzerdefinierte #[derive]-Makros, attributähnliche Makros und funktionähnliche Makros.

Makros

Wir haben in diesem Buch Makros wie println! verwendet, aber wir haben noch nicht vollständig untersucht, was ein Makro ist und wie es funktioniert. Der Begriff Makro bezieht sich auf eine Familie von Features in Rust: deklarative Makros mit macro_rules! und drei Arten von prozeduralen Makros:

  • Benutzerdefinierte #[derive]-Makros, die den Code angeben, der mit dem derive-Attribut hinzugefügt wird, das auf Structs und Enums verwendet wird
  • Attributähnliche Makros, die benutzerdefinierte Attribute definieren, die auf jedem Element verwendbar sind
  • Funktionähnliche Makros, die wie Funktionsaufrufe aussehen, aber auf den als Argument angegebenen Tokens operieren

Wir werden nacheinander über jedes dieser Punkte sprechen, aber zunächst schauen wir uns an, warum wir überhaupt Makros brauchen, wenn wir bereits Funktionen haben.

Der Unterschied zwischen Makros und Funktionen

Im Grunde genommen sind Makros eine Möglichkeit, Code zu schreiben, der anderen Code erzeugt, was als Metaprogrammierung bekannt ist. Im Anhang C besprechen wir das derive-Attribut, das Ihnen eine Implementierung verschiedener Traits erzeugt. Wir haben auch die Makros println! und vec! im gesamten Buch verwendet. Alle diese Makros expandieren, um mehr Code zu erzeugen, als den Code, den Sie manuell geschrieben haben.

Die Metaprogrammierung ist nützlich, um die Menge an Code zu reduzieren, den Sie schreiben und pflegen müssen, was auch eine der Rollen von Funktionen ist. Allerdings haben Makros einige zusätzliche Kräfte, die Funktionen nicht haben.

Die Signatur einer Funktion muss die Anzahl und den Typ der Parameter angeben, die die Funktion hat. Makros hingegen können eine variable Anzahl von Parametern akzeptieren: wir können println!("hello") mit einem Argument oder println!("hello {}", name) mit zwei Argumenten aufrufen. Außerdem werden Makros vor der Interpretation des Codes durch den Compiler expandiert, sodass ein Makro beispielsweise ein Trait für einen bestimmten Typ implementieren kann. Eine Funktion kann das nicht, da sie zur Laufzeit aufgerufen wird und ein Trait zur Compilezeit implementiert werden muss.

Der Nachteil der Implementierung eines Makros anstelle einer Funktion ist, dass Makrodefinitionen komplexer als Funktionsdefinitionen sind, da Sie Rust-Code schreiben, der Rust-Code erzeugt. Aufgrund dieser Indirektion sind Makrodefinitionen im Allgemeinen schwieriger zu lesen, zu verstehen und zu pflegen als Funktionsdefinitionen.

Ein weiterer wichtiger Unterschied zwischen Makros und Funktionen ist, dass Sie Makros vor ihrem Aufruf in einer Datei definieren oder in den Gültigkeitsbereich bringen müssen, im Gegensatz zu Funktionen, die Sie überall definieren und überall aufrufen können.

Deklarative Makros mit macro_rules! für die allgemeine Metaprogrammierung

Die am weitesten verbreitete Form von Makros in Rust ist das deklarative Makro. Diese werden manchmal auch als "Makros am Beispiel", "macro_rules!-Makros" oder einfach nur "Makros" bezeichnet. Im Kern erlauben deklarative Makros es Ihnen, etwas zu schreiben, das ähnelt einem Rust-match-Ausdruck. Wie in Kapitel 6 besprochen, sind match-Ausdrücke Steuerstrukturen, die einen Ausdruck nehmen, den resultierenden Wert des Ausdrucks mit Mustern vergleichen und dann den mit dem passenden Muster assoziierten Code ausführen. Makros vergleichen auch einen Wert mit Mustern, die mit bestimmten Codeblöcken assoziiert sind: In dieser Situation ist der Wert der in den Makro übergebene literale Rust-Quellcode; die Muster werden mit der Struktur dieses Quellcodes verglichen; und der mit jedem Muster assoziierte Code ersetzt, wenn er übereinstimmt, den an den Makro übergebenen Code. Alles dies geschieht während der Kompilierung.

Um ein Makro zu definieren, verwenden Sie den macro_rules!-Konstrukt. Lassen Sie uns untersuchen, wie macro_rules! verwendet wird, indem wir uns ansehen, wie das vec!-Makro definiert ist. Kapitel 8 hat gezeigt, wie wir das vec!-Makro verwenden können, um einen neuen Vektor mit bestimmten Werten zu erstellen. Beispielsweise erzeugt das folgende Makro einen neuen Vektor, der drei ganze Zahlen enthält:

let v: Vec<u32> = vec![1, 2, 3];

Wir könnten auch das vec!-Makro verwenden, um einen Vektor von zwei ganzen Zahlen oder einen Vektor von fünf Zeichenfolien zu erstellen. Wir könnten eine Funktion nicht verwenden, um das Gleiche zu tun, da wir die Anzahl oder den Typ der Werte im Voraus nicht kennen würden.

Listing 19-28 zeigt eine leicht vereinfachte Definition des vec!-Makros.

Dateiname: src/lib.rs

1 #[macro_export]
2 macro_rules! vec {
  3 ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
          4 $(
              5 temp_vec.push(6 $x);
            )*
          7 temp_vec
        }
    };
}

Listing 19-28: Eine vereinfachte Version der vec!-Makrodefinition

Hinweis: Die tatsächliche Definition des vec!-Makros in der Standardbibliothek enthält Code, um das richtige Speichervolumen im Voraus zuzuweisen. Dieser Code ist eine Optimierung, die wir hier nicht einschließen, um das Beispiel einfacher zu halten.

Die #[macro_export]-Annotation [1] gibt an, dass dieses Makro verfügbar sein sollte, wenn das Kratzerzeugnis, in dem das Makro definiert ist, in den Gültigkeitsbereich gebracht wird. Ohne diese Annotation kann das Makro nicht in den Gültigkeitsbereich gebracht werden.

Wir beginnen dann die Makrodefinition mit macro_rules! und dem Namen des Makros, das wir definieren, ohne das Ausrufezeichen [2]. Der Name, in diesem Fall vec, wird von geschweiften Klammern gefolgt, die den Körper der Makrodefinition bezeichnen.

Die Struktur im vec!-Körper ähnelt der Struktur eines match-Ausdrucks. Hier haben wir einen Arm mit dem Muster ( $( $x:expr ),* ), gefolgt von => und dem Codeblock, der mit diesem Muster assoziiert ist [3]. Wenn das Muster übereinstimmt, wird der zugehörige Codeblock ausgegeben. Da dies das einzige Muster in diesem Makro ist, gibt es nur eine gültige Möglichkeit, zu matchen; jedes andere Muster führt zu einem Fehler. Komplexere Makros werden mehr als einen Arm haben.

Die gültige Mustersyntax in Makrodefinitionen unterscheidet sich von der Mustersyntax, die in Kapitel 18 behandelt wurde, da Makromuster gegen die Rust-Codestruktur statt gegen Werte gematcht werden. Lassen Sie uns durchgehen, was die Musterteile in Listing 19-28 bedeuten; für die volle Makromustersyntax siehe die Rust-Referenz unter https://doc.rust-lang.org/reference/macros-by-example.html.

Zunächst verwenden wir eine Klammerung, um das gesamte Muster zu umfassen. Wir verwenden ein Dollarzeichen ($), um eine Variable im Makrosystem zu deklarieren, die den mit dem Muster übereinstimmenden Rust-Code enthalten wird. Das Dollarzeichen macht klar, dass es sich um eine Makrovariable handelt, im Gegensatz zu einer normalen Rust-Variable. Danach folgt eine Klammerung, die Werte fängt, die mit dem Muster innerhalb der Klammer übereinstimmen, um in den Ersetzungscode zu verwenden. Innerhalb von $() ist $x:expr, das mit jedem Rust-Ausdruck übereinstimmt und dem Ausdruck den Namen $x gibt.

Das Komma nach $() gibt an, dass ein literales Komma-Separatorzeichen optional nach dem Code erscheinen könnte, der mit dem Code in $() übereinstimmt. Das * gibt an, dass das Muster null oder mehr von dem, was vor dem * steht, übereinstimmt.

Wenn wir dieses Makro mit vec![1, 2, 3]; aufrufen, stimmt das $x-Muster dreimal mit den drei Ausdrücken 1, 2 und 3 überein.

Lassen Sie uns jetzt das Muster im Körper des Codes betrachten, der mit diesem Arm assoziiert ist: temp_vec.push() [5] innerhalb von $()* bei [4] und [7] wird für jedes Teil generiert, das mit()` im Muster null oder mehr mal übereinstimmt, je nachdem, wie oft das Muster übereinstimmt. Das `x[6] wird mit jedem übereinstimmenden Ausdruck ersetzt. Wenn wir dieses Makro mitvec[1, 2, 3];` aufrufen, wird der generierte Code, der diesen Makroaufruf ersetzt, der folgende sein:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

Wir haben ein Makro definiert, das beliebig viele Argumente beliebigen Typs akzeptieren kann und Code generieren kann, um einen Vektor zu erstellen, der die angegebenen Elemente enthält.

Um mehr über die Schreibung von Makros zu erfahren, konsultieren Sie die Online-Dokumentation oder andere Ressourcen, wie "The Little Book of Rust Macros" unter https://veykril.github.io/tlborm, das von Daniel Keep gestartet und von Lukas Wirth fortgesetzt wurde.

Prozedurale Makros für die Generierung von Code aus Attributen

Die zweite Form von Makros ist das prozedurale Makro, das sich eher wie eine Funktion verhält (und eine Art Prozedur ist). Prozedurale Makros akzeptieren einige Code als Eingabe, operieren auf diesem Code und erzeugen als Ausgabe einen anderen Code, anstatt wie deklarative Makros gegen Muster zu matchen und den Code mit anderem Code zu ersetzen. Die drei Arten von prozeduralen Makros sind benutzerdefiniertes derive, attributähnlich und funktionähnlich, und alle funktionieren auf ähnliche Weise.

Wenn Sie prozedurale Makros erstellen, müssen die Definitionen in ihrem eigenen Kratzerzeugnis mit einem speziellen Kratzertyp liegen. Dies liegt an komplexen technischen Gründen, die wir in Zukunft eliminieren möchten. In Listing 19-29 zeigen wir, wie Sie ein prozedurales Makro definieren, wobei some_attribute ein Platzhalter für die Verwendung einer bestimmten Makrovariante ist.

Dateiname: src/lib.rs

use proc_macro::TokenStream;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}

Listing 19-29: Ein Beispiel für die Definition eines prozeduralen Makros

Die Funktion, die ein prozedurales Makro definiert, nimmt einen TokenStream als Eingabe und erzeugt einen TokenStream als Ausgabe. Der TokenStream-Typ wird vom proc_macro-Kratzerzeugnis definiert, das mit Rust mitgeliefert wird, und stellt eine Sequenz von Tokens dar. Dies ist der Kern des Makros: Der Quellcode, auf dem das Makro operiert, bildet den Eingabe-TokenStream und der Code, den das Makro erzeugt, ist der Ausgabe-TokenStream. Die Funktion hat auch ein Attribut angefügt, das angibt, welche Art von prozeduralem Makro wir erstellen. Wir können in demselben Kratzerzeugnis mehrere Arten von prozeduralen Makros haben.

Lassen Sie uns die verschiedenen Arten von prozeduralen Makros betrachten. Wir beginnen mit einem benutzerdefinierten derive-Makro und erklären dann die kleinen Unterschiede, die die anderen Formen unterschiedlich machen.

Wie man einen benutzerdefinierten derive-Makro schreibt

Lassen Sie uns einen Kratzerzeugnis namens hello_macro erstellen, der ein Trait namens HelloMacro mit einer assoziierten Funktion namens hello_macro definiert. Anstatt es unseren Benutzern zu ermöglichen, das HelloMacro-Trait für jede ihrer Typen zu implementieren, werden wir ein prozedurales Makro bereitstellen, sodass die Benutzer ihr Typ mit #[derive(HelloMacro)] annotieren können, um eine Standardimplementierung der hello_macro-Funktion zu erhalten. Die Standardimplementierung wird Hello, Macro! My name is TypeName! ausgeben, wobei TypeName der Name des Typs ist, für den dieses Trait definiert wurde. Mit anderen Worten, wir werden ein Kratzerzeugnis schreiben, das es einem anderen Programmierer ermöglicht, Code wie in Listing 19-30 mit unserem Kratzerzeugnis zu schreiben.

Dateiname: src/main.rs

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}

Listing 19-30: Der Code, den ein Benutzer unseres Kratzerzeugnisses schreiben kann, wenn er unser prozedurales Makro verwendet

Dieser Code wird Hello, Macro! My name is Pancakes! ausgeben, wenn wir fertig sind. Der erste Schritt ist es, ein neues Bibliothekskratzerzeugnis zu erstellen, wie folgt:

cargo new hello_macro --lib

Als nächstes werden wir das HelloMacro-Trait und seine assoziierte Funktion definieren:

Dateiname: src/lib.rs

pub trait HelloMacro {
    fn hello_macro();
}

Wir haben ein Trait und seine Funktion. Zu diesem Zeitpunkt könnte der Benutzer unseres Kratzerzeugnisses das Trait implementieren, um die gewünschte Funktionalität zu erreichen, wie folgt:

use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

fn main() {
    Pancakes::hello_macro();
}

Allerdings müssten sie den Implementierungsblock für jeden Typ schreiben, den sie mit hello_macro verwenden möchten; wir möchten sie davor bewahren, diese Arbeit zu erledigen.

Zusätzlich können wir der hello_macro-Funktion noch keine Standardimplementierung geben, die den Namen des Typs ausgibt, für den das Trait implementiert wird: Rust hat keine Reflektionseigenschaften, sodass es die Typenamen zur Laufzeit nicht auflösen kann. Wir brauchen ein Makro, um Code zur Compilezeit zu generieren.

Der nächste Schritt ist es, das prozedurale Makro zu definieren. Zum Zeitpunkt der Verfassung dieses Dokuments müssen prozedurale Makros in ihrem eigenen Kratzerzeugnis liegen. Eventuell wird diese Einschränkung in Zukunft aufgehoben. Die Konvention für die Struktur von Kratzerzeugnissen und Makrokratzerzeugnissen lautet wie folgt: für ein Kratzerzeugnis namens foo wird ein benutzerdefiniertes derive-prozedurales Makrokratzerzeugnis foo_derive genannt. Lassen Sie uns ein neues Kratzerzeugnis namens hello_macro_derive im Verzeichnis unseres hello_macro-Projekts starten:

cargo new hello_macro_derive --lib

Unsere beiden Kratzerzeugnisse sind eng miteinander verbunden, sodass wir das prozedurale Makrokratzerzeugnis innerhalb des Verzeichnisses unseres hello_macro-Kratzerzeugnisses erstellen. Wenn wir die Traitdefinition in hello_macro ändern, müssen wir auch die Implementierung des prozeduralen Makros in hello_macro_derive ändern. Die beiden Kratzerzeugnisse müssen separat veröffentlicht werden, und Programmierer, die diese Kratzerzeugnisse verwenden, müssen beide als Abhängigkeiten hinzufügen und beide in den Gültigkeitsbereich bringen. Stattdessen könnten wir das hello_macro-Kratzerzeugnis hello_macro_derive als Abhängigkeit verwenden und den Code des prozeduralen Makros erneut exportieren. Allerdings ermöglicht die von uns gewählte Projektstruktur es Programmierern, hello_macro zu verwenden, auch wenn sie die derive-Funktionalität nicht benötigen.

Wir müssen das hello_macro_derive-Kratzerzeugnis als prozedurales Makrokratzerzeugnis deklarieren. Wir werden auch Funktionalität aus den Kratzerzeugnissen syn und quote benötigen, wie Sie gleich sehen werden, sodass wir sie als Abhängigkeiten hinzufügen müssen. Fügen Sie dem Cargo.toml-Datei für hello_macro_derive Folgendes hinzu:

Dateiname: hello_macro_derive/Cargo.toml

[lib]
proc-macro = true

[dependencies]
syn = "1.0"
quote = "1.0"

Um das prozedurale Makro zu definieren, legen Sie den Code in Listing 19-31 in Ihre src/lib.rs-Datei für das hello_macro_derive-Kratzerzeugnis. Beachten Sie, dass dieser Code nicht kompilieren wird, bis wir eine Definition für die impl_hello_macro-Funktion hinzufügen.

Dateiname: 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 {
    // Konstruieren Sie eine Darstellung von Rust-Code als Syntaxbaum,
    // auf den wir zugreifen können
    let ast = syn::parse(input).unwrap();

    // Erstellen Sie die Traitimplementierung
    impl_hello_macro(&ast)
}

Listing 19-31: Code, den die meisten prozeduralen Makrokratzerzeugnisse benötigen, um Rust-Code zu verarbeiten

Beachten Sie, dass wir den Code in die hello_macro_derive-Funktion unterteilt haben, die für das Parsen des TokenStream verantwortlich ist, und die impl_hello_macro-Funktion, die für die Transformation des Syntaxbaums verantwortlich ist: Dies macht das Schreiben eines prozeduralen Makros komfortabler. Der Code in der äußeren Funktion (hello_macro_derive in diesem Fall) wird für fast jedes prozedurale Makrokratzerzeugnis, das Sie sehen oder erstellen, gleich sein. Der Code, den Sie im Körper der inneren Funktion (impl_hello_macro in diesem Fall) angeben, wird je nach Zweck Ihres prozeduralen Makros unterschiedlich sein.

Wir haben drei neue Kratzerzeugnisse eingeführt: proc_macro, syn (verfügbar unter https://crates.io/crates/syn) und quote (verfügbar unter https://crates.io/crates/quote). Das proc_macro-Kratzerzeugnis kommt mit Rust mit, sodass wir es nicht zu den Abhängigkeiten in Cargo.toml hinzufügen mussten. Das proc_macro-Kratzerzeugnis ist die API des Compilers, die uns ermöglicht, Rust-Code aus unserem Code zu lesen und zu manipulieren.

Das syn-Kratzerzeugnis analysiert Rust-Code aus einer Zeichenfolge in eine Datenstruktur, auf die wir Operationen ausführen können. Das quote-Kratzerzeugnis wandelt syn-Datenstrukturen wieder in Rust-Code um. Diese Kratzerzeugnisse machen es viel einfacher, beliebigen Rust-Code zu analysieren, den wir behandeln möchten: das Schreiben eines vollständigen Parsers für Rust-Code ist keine einfache Aufgabe.

Die hello_macro_derive-Funktion wird aufgerufen, wenn ein Benutzer unseres Bibliothekskratzerzeugnisses ein Typ mit #[derive(HelloMacro)] annotiert. Dies ist möglich, da wir die hello_macro_derive-Funktion hier mit proc_macro_derive annotiert haben und den Namen HelloMacro angegeben haben, der unserem Traitnamen entspricht; dies ist die Konvention, die die meisten prozeduralen Makros befolgen.

Die hello_macro_derive-Funktion konvertiert zunächst die input von einem TokenStream in eine Datenstruktur, auf die wir dann zugreifen und Operationen ausführen können. Hier kommt syn ins Spiel. Die parse-Funktion in syn nimmt einen TokenStream und gibt eine DeriveInput-Struktur zurück, die den analysierten Rust-Code repräsentiert. Listing 19-32 zeigt die relevanten Teile der DeriveInput-Struktur, die wir beim Parsen der Zeichenfolge struct Pancakes; erhalten.

DeriveInput {
    --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}

Listing 19-32: Die DeriveInput-Instanz, die wir erhalten, wenn wir den Code in Listing 19-30 analysieren, der das Attribut des Makros enthält

Die Felder dieser Struktur zeigen, dass der Rust-Code, den wir analysiert haben, eine Einheitenstruktur mit dem ident (Bezeichner, also dem Namen) von Pancakes ist. Es gibt weitere Felder in dieser Struktur, um alle Arten von Rust-Code zu beschreiben; überprüfen Sie die syn-Dokumentation für DeriveInput unter https://docs.rs/syn/1.0/syn/struct.DeriveInput.html für weitere Informationen.

Bald werden wir die impl_hello_macro-Funktion definieren, in der wir den neuen Rust-Code erstellen, den wir hinzufügen möchten. Bevor wir dies tun, beachten Sie, dass die Ausgabe unseres derive-Makros ebenfalls ein TokenStream ist. Der zurückgegebene TokenStream wird zum Code hinzugefügt, den die Benutzer unseres Kratzerzeugnisses schreiben, sodass sie beim Kompilieren ihres Kratzerzeugnisses die zusätzliche Funktionalität erhalten, die wir in dem modifizierten TokenStream bereitstellen.

Sie haben vielleicht bemerkt, dass wir unwrap aufrufen, um die hello_macro_derive-Funktion dazu zu bringen, einen Fehler auszulösen, wenn der Aufruf der syn::parse-Funktion fehlschlägt. Es ist notwendig, dass unser prozedurales Makro bei Fehlern einen Fehler auslöst, da proc_macro_derive-Funktionen TokenStream statt Result zurückgeben müssen, um der prozeduralen Makro-API zu entsprechen. Wir haben diesen Beispiel durch die Verwendung von unwrap vereinfacht; in der Produktionscode sollten Sie spezifischere Fehlermeldungen übergeben, was schiefgelaufen ist, indem Sie panic! oder expect verwenden.

Jetzt, da wir den Code haben, um den annotierten Rust-Code von einem TokenStream in eine DeriveInput-Instanz umzuwandeln, generieren wir den Code, der das HelloMacro-Trait auf dem annotierten Typ implementiert, wie in Listing 19-33 gezeigt.

Dateiname: hello_macro_derive/src/lib.rs

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let gen = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!(
                    "Hello, Macro! My name is {}!",
                    stringify!(#name)
                );
            }
        }
    };
    gen.into()
}

Listing 19-33: Implementieren des HelloMacro-Traits mit dem analysierten Rust-Code

Wir erhalten eine Ident-Strukturinstanz, die den Namen (Bezeichner) des annotierten Typs enthält, indem wir ast.ident verwenden. Die Struktur in Listing 19-32 zeigt, dass wenn wir die impl_hello_macro-Funktion auf den Code in Listing 19-30 ausführen, das ident, das wir erhalten, das ident-Feld mit einem Wert von "Pancakes" haben wird. Somit wird die name-Variable in Listing 19-33 eine Ident-Strukturinstanz enthalten, die beim Drucken die Zeichenfolge "Pancakes", den Namen der Struktur in Listing 19-30, sein wird.

Die quote!-Makro ermöglicht es uns, den Rust-Code zu definieren, den wir zurückgeben möchten. Der Compiler erwartet etwas anderes als das direkte Ergebnis der Ausführung des quote!-Makros, sodass wir es in einen TokenStream umwandeln müssen. Wir tun dies, indem wir die into-Methode aufrufen, die diese Zwischenrepräsentation konsumiert und einen Wert des erforderlichen TokenStream-Typs zurückgibt.

Das quote!-Makro bietet auch einige sehr coole Templatesmechaniken: wir können #name eingeben, und quote! wird es mit dem Wert in der Variable name ersetzen. Sie können sogar eine gewisse Wiederholung durchführen, ähnlich wie bei regulären Makros. Überprüfen Sie die quote-Kratzerzeugnis-Dokumentation unter https://docs.rs/quote für eine umfassende Einführung.

Wir möchten, dass unser prozedurales Makro eine Implementierung unseres HelloMacro-Traits für den Typ erzeugt, den der Benutzer annotiert hat, was wir mit #name erhalten können. Die Traitimplementierung hat die eine Funktion hello_macro, deren Körper die Funktionalität enthält, die wir bereitstellen möchten: das Ausgeben von Hello, Macro! My name is und dann dem Namen des annotierten Typs.

Das hier verwendete stringify!-Makro ist in Rust integriert. Es nimmt einen Rust-Ausdruck, wie z. B. 1 + 2, und wandelt ihn zur Compilezeit in einen Stringliteral um, wie z. B. "1 + 2". Dies unterscheidet sich von format! oder println!, Makros, die den Ausdruck auswerten und das Ergebnis dann in eine String umwandeln. Es besteht die Möglichkeit, dass der #name-Eingabe ein Ausdruck ist, der buchstäblich gedruckt werden soll, sodass wir stringify! verwenden. Die Verwendung von stringify! spart auch eine Allokation, indem #name zur Compilezeit in einen Stringliteral umgewandelt wird.

An diesem Punkt sollte cargo build in beiden hello_macro und hello_macro_derive erfolgreich abgeschlossen werden. Lassen Sie uns diese Kratzerzeugnisse mit dem Code in Listing 19-30 verbinden, um das prozedurale Makro in Aktion zu sehen! Erstellen Sie ein neues binäres Projekt im project-Verzeichnis mit cargo new pancakes. Wir müssen hello_macro und hello_macro_derive als Abhängigkeiten in der Cargo.toml des pancakes-Kratzerzeugnisses hinzufügen. Wenn Sie Ihre Versionen von hello_macro und hello_macro_derive auf https://crates.io veröffentlichen, wären sie reguläre Abhängigkeiten; wenn nicht, können Sie sie als path-Abhängigkeiten wie folgt angeben:

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

Legen Sie den Code in Listing 19-30 in src/main.rs ab, und führen Sie cargo run aus: es sollte Hello, Macro! My name is Pancakes! ausgeben. Die Implementierung des HelloMacro-Traits aus dem prozeduralen Makro wurde ohne dass das pancakes-Kratzerzeugnis es implementieren musste, hinzugefügt; das #[derive(HelloMacro)] hat die Traitimplementierung hinzugefügt.

Als nächstes werden wir untersuchen, wie sich die anderen Arten von prozeduralen Makros von benutzerdefinierten derive-Makros unterscheiden.

Attributähnliche Makros

Attributähnliche Makros ähneln benutzerdefinierten derive-Makros, aber anstatt Code für das derive-Attribut zu generieren, ermöglichen sie es Ihnen, neue Attribute zu erstellen. Sie sind auch flexibler: derive funktioniert nur für Structs und Enums; Attribute können auch auf andere Elemente angewendet werden, wie z. B. Funktionen. Hier ist ein Beispiel für die Verwendung eines attributähnlichen Makros. Nehmen Sie an, Sie haben ein Attribut namens route, das Funktionen annotiert, wenn Sie ein Webanwendungsframework verwenden:

#[route(GET, "/")]
fn index() {

Dieses #[route]-Attribut würde vom Framework als prozedurales Makro definiert werden. Die Signatur der Makrodefinition-Funktion würde so aussehen:

#[proc_macro_attribute]
pub fn route(
    attr: TokenStream,
    item: TokenStream
) -> TokenStream {

Hier haben wir zwei Parameter vom Typ TokenStream. Der erste ist für den Inhalt des Attributes: der GET, "/"-Teil. Der zweite ist der Körper des Elements, an das das Attribut angefügt ist: in diesem Fall fn index() {} und der Rest des Funktionskörpers.

Ansonsten funktionieren attributähnliche Makros auf die gleiche Weise wie benutzerdefinierte derive-Makros: Sie erstellen ein Kratzerzeugnis mit dem proc-macro-Kratzertyp und implementieren eine Funktion, die den gewünschten Code generiert!

Funktionsähnliche Makros

Funktionsähnliche Makros definieren Makros, die wie Funktionsaufrufe aussehen. Ähnlich wie macro_rules!-Makros sind sie flexibler als Funktionen; beispielsweise können sie eine unbekannte Anzahl von Argumenten akzeptieren. Allerdings können macro_rules!-Makros nur mit der match-ähnlichen Syntax definiert werden, die wir in "Declarative Macros with macro_rules! for General Metaprogramming" diskutiert haben. Funktionsähnliche Makros nehmen einen TokenStream-Parameter entgegen, und ihre Definition manipuliert diesen TokenStream wie die anderen beiden Arten von prozeduralen Makros mit Rust-Code. Ein Beispiel für ein funktionsähnliches Makro ist ein sql!-Makro, das möglicherweise wie folgt aufgerufen wird:

let sql = sql!(SELECT * FROM posts WHERE id=1);

Dieses Makro würde den SQL-Befehl darin analysieren und überprüfen, ob er syntaktisch korrekt ist, was eine viel komplexere Verarbeitung ist als das, was ein macro_rules!-Makro tun kann. Das sql!-Makro würde wie folgt definiert werden:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

Diese Definition ähnelt der Signatur des benutzerdefinierten derive-Makros: Wir erhalten die Tokens, die innerhalb der Klammern sind, und geben den Code zurück, den wir generieren möchten.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Macros-Labor abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.