Makros
Wir haben in diesem Buch Makros wie println! verwendet, aber wir haben noch
nicht vollständig erforscht, was ein Makro ist und wie es funktioniert. Der
Begriff Makro bezieht sich auf eine Familie von Funktionalitäten in Rust:
Deklarative Makros mit macro_rules! und drei Arten prozeduraler Makros:
- Benutzerdefinierte Makros mit
#[derive], die Code spezifizieren, der mit dem Attributderivehinzugefügt wurde, das bei Strukturen (structs) und Aufzählungen (enums) verwendet wird - Attribut-ähnliche Makros, die benutzerdefinierte Attribute definieren, die bei jedem Element verwendet werden können
- Funktions-ähnliche Makros, die wie Funktionsaufrufe aussehen, aber auf den als Argument angegebenen Token operieren
Wir werden der Reihe nach über jedes dieser Themen sprechen, aber zuerst wollen wir uns ansehen, warum wir Makros überhaupt 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 schreibt, was als Metaprogrammierung bekannt ist. In Anhang C besprechen
wir das Attribut derive, das dir eine Implementierung verschiedener Traits
(traits) generiert. Wir haben im ganzen Buch auch die Makros println! und
vec! verwendet. All diese Makros werden expandiert, um mehr Code zu erzeugen
als der Code, den du manuell geschrieben hast.
Metaprogrammierung ist nützlich, um die Menge an Code zu reduzieren, die du schreiben und pflegen musst, was auch eine der Aufgaben von Funktionen ist. Makros haben jedoch einige zusätzliche Fähigkeiten, die Funktionen nicht haben.
Eine Funktionssignatur muss die Anzahl und den Typ der Parameter deklarieren,
die die Funktion hat. Makros hingegen können eine variable Anzahl von Parametern
entgegennehmen: Wir können println!("Hallo") mit einem Argument oder
println!("Hallo {}", name) mit zwei Argumenten aufrufen. Außerdem werden
Makros expandiert, bevor der Compiler die Bedeutung des Codes interpretiert,
sodass ein Makro beispielsweise ein Trait auf einen bestimmten Typ
implementieren kann. Eine Funktion kann das nicht, weil sie zur Laufzeit
aufgerufen wird und ein Trait zur Kompilierzeit implementiert werden muss.
Der Nachteil des Implementierens eines Makros anstelle einer Funktion besteht darin, dass Makrodefinitionen komplexer sind als Funktionsdefinitionen, weil du Rust-Code schreibst, der Rust-Code schreibt. 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 besteht darin, dass du Makros definieren oder in den Gültigkeitsbereich bringen musst, bevor du sie in einer Datei aufrufst, im Gegensatz zu Funktionen, die du überall definieren und überall aufrufen kannst.
Deklarative Makros für allgemeine Metaprogrammierung
Die am häufigsten verwendete Form von Makros in Rust ist das deklarative
Makro. Diese werden manchmal auch als „Makros am Beispiel“ (macros by
example), „macro_rules!-Makros“ oder einfach nur „Makros“ bezeichnet. In
ihrem Kern erlauben deklarative Makros, etwas Ähnliches wie einen Rust-Ausdruck
zu schreiben. Wie in Kapitel 6 besprochen, sind match-Ausdrücke
Kontrollstrukturen, die einen Ausdruck entgegennehmen, den resultierenden Wert
des Ausdrucks mit Mustern abgleichen und dann den Code ausführen, der mit dem
passenden Muster verknüpft ist. Deklarative Makros vergleichen ebenfalls einen
Wert mit Mustern, die mit einem bestimmten Code verknüpft sind. Bei deklarativen
Makros ist der Wert der dem Makro übergebene literale Rust-Quellcode. Die Muster
werden mit der Struktur dieses Quellcodes verglichen. Bei Übereinstimmung ersetzt
der mit dem Muster verknüpfte Code den an das Makro übergebenen Code. Dies alles
geschieht während der Kompilierung.
Um ein Makro zu definieren, verwendest du das Konstrukt macro_rules!. Lass uns
untersuchen, wie man macro_rules! benutzt, indem wir uns ansehen, wie das
Makro vec! definiert wird. Kapitel 8 behandelte, wie wir das Makro vec!
verwenden können, um einen neuen Vektor mit bestimmten Werten zu erzeugen. Zum
Beispiel erzeugt das folgende Makro einen neuen Vektor mit drei ganzen Zahlen:
#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}
Wir könnten auch das Makro vec! verwenden, um einen Vektor aus zwei ganzen
Zahlen oder einen Vektor aus fünf String Slices zu erstellen. Mit einer Funktion
wäre das nicht möglich, da weder die Anzahl noch der Typ der Werte im Voraus
bekannt sind.
Listing 20-35 zeigt eine leicht vereinfachte Definition des Makros vec!.
Dateiname: src/lib.rs
#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
}
Listing 20-35: Eine vereinfachte Version der
Makrodefinition vec!
Hinweis: Die eigentliche Definition des Makros
vec!in der Standardbibliothek enthält Code zum Vorbelegen der korrekten Speichermenge. Dieser Code ist eine Optimierung, die wir hier zur Vereinfachung des Beispiels nicht darstellen.
Die Annotation #[macro_export] gibt an, dass dieses Makro immer dann zur
Verfügung gestellt werden soll, wenn die Crate, in der das Makro
definiert ist, in den Gültigkeitsbereich gebracht wird. Ohne diese Annotation
kann das Makro nicht in den Gültigkeitsbereich gebracht werden.
Dann beginnen wir die Makrodefinition mit macro_rules! und dem Namen des
Makros, das wir definieren, ohne Ausrufezeichen. Auf den Namen, in diesem Fall
vec, folgen geschweifte Klammern, die den Rumpf der Makrodefinition
kennzeichnen.
Die Struktur im vec!-Rumpf ähnelt der Struktur eines match-Ausdrucks. Hier
haben wir einen Zweig mit dem Muster ( $( $x:expr ),* ), gefolgt von => und
dem mit diesem Muster verknüpften Codeblock. Wenn das Muster passt, wird der
zugehörige Codeblock ausgegeben. Da dies das einzige Muster in diesem Makro ist,
kann es nur einen passenden Zweig geben; jedes andere Muster führt zu einem
Fehler. Komplexere Makros werden mehr als einen Zweig haben.
Die gültige Mustersyntax in Makrodefinitionen unterscheidet sich von der in Kapitel 19 behandelten Mustersyntax, da Makromuster mit der Rust-Codestruktur und nicht mit Werten abgeglichen werden. Lass uns im Folgenden die Bedeutung der Musterteile in Listing 20-35 betrachten; die vollständige Makromustersyntax findest du in der Rust-Referenz.
Zunächst verwenden wir ein äußeres Klammernpaar, um das gesamte Muster zu
umfassen. Wir verwenden ein Dollarzeichen ($), um eine Variable im
Makrosystem zu deklarieren, die den Rust-Code enthält, der zum Muster passt.
Das Dollarzeichen macht deutlich, dass es sich um eine Makrovariable und nicht
um eine normale Rust-Variable handelt. Danach folgt eine Reihe von Klammern,
die Werte erfassen, die mit dem Muster innerhalb der Klammern übereinstimmen,
um sie im Ersetzungscode zu verwenden. Innerhalb von $() befindet sich
$x:expr, das mit jedem beliebigen Rust-Ausdruck übereinstimmt und dem
Ausdruck den Namen $x gibt.
Das Komma nach $() gibt an, dass ein literales Komma-Trennzeichen zwischen
allen Code-Teilen, die mit dem Code in $() übereinstimmen, vorhanden sein
muss. Der * besagt, dass das Muster keinmal oder mehrmals zu dem passt, was
vor dem * steht.
Wenn wir dieses Makro mit vec![1, 2, 3]; aufrufen, passt das Muster $x
dreimal zu den drei Ausdrücken 1, 2 und 3.
Betrachten wir nun das Muster im Hauptteil des mit diesem Zweig verknüpften
Codes: temp_vec.push() innerhalb von $()* wird für jeden Teil erzeugt, der
keinmal oder mehrmals mit $() im Muster übereinstimmt, je nachdem, wie oft
das Muster passt. Das $x wird durch jeden passenden Ausdruck ersetzt. Wenn
wir dieses Makro mit vec![1, 2, 3]; aufrufen, wird durch diesen Aufruf
folgender Code generiert:
{
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 eine beliebige Anzahl von Argumenten beliebigen Typs aufnehmen und Code zur Erstellung eines Vektors erzeugen kann, der die angegebenen Elemente enthält.
Um mehr darüber zu erfahren, wie man Makros schreibt, konsultiere die Online-Dokumentation oder andere Ressourcen, wie zum Beispiel „The Little Book of Rust Macros“.
Prozedurale Makros zur Code-Generierung 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
etwas Code als Eingabe, operieren mit diesem Code und erzeugen etwas Code als
Ausgabe, anstatt gegen Muster abzugleichen und den Code durch anderen Code zu
ersetzen, wie es deklarative Makros tun. Die drei Arten von prozeduralen Makros
(benutzerdefinierte derive-Makros, Attribut-ähnliche und Funktions-ähnliche)
arbeiten alle auf ähnliche Weise.
Beim Erstellen von prozeduralen Makros müssen sich die Definitionen in einer
eigenen Crate mit einem speziellen Crate-Typ befinden. Dies geschieht aus
komplexen technischen Gründen, die wir hoffentlich in Zukunft eliminieren
werden. In Listing 20-36 zeigen wir, wie man ein prozedurales Makro definiert,
wobei some_attribute ein Platzhalter für die Verwendung einer bestimmten
Makro-Variante ist.
Dateiname: src/lib.rs
use proc_macro::TokenStream;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Listing 20-36: 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 Typ TokenStream
wird durch die Crate proc_macro definiert, die in Rust enthalten ist und eine
Folge von Token darstellt. Dies ist der Kern des Makros: Der Quellcode, mit dem
das Makro arbeitet, bildet die Eingabe TokenStream, und der Code, den das
Makro erzeugt, ist die Ausgabe TokenStream. Die Funktion hat auch ein
Attribut, das angibt, welche Art prozedurales Makro wir erstellen. Wir können
mehrere Arten prozeduraler Makros in derselben Crate haben.
Schauen wir uns die verschiedenen Arten prozeduraler Makros an. Wir beginnen
mit einem benutzerdefinierten derive-Makro und erklären dann die kleinen
Unterschiede, in denen sich die anderen Formen unterscheiden.
Benutzerdefinierte derive-Makros
Lass uns eine Crate namens hello_macro erstellen, die ein Trait namens
HelloMacro mit einer assoziierten Funktion namens hello_macro definiert.
Anstatt unsere Benutzer dazu zu bringen, das Trait HelloMacro für jeden ihrer
Typen zu implementieren, werden wir ein prozedurales Makro zur Verfügung
stellen, damit die Benutzer ihren Typ mit #[derive(HelloMacro)] annotieren
können, um eine Standardimplementierung der Funktion hello_macro zu erhalten.
Die Standardimplementierung gibt Hallo Makro! Mein Name ist TypeName! aus,
wobei TypeName der Name des Typs ist, auf dem dieses Trait definiert wurde.
Mit anderen Worten, wir werden eine Crate schreiben, die es einem anderen
Programmierer ermöglicht, mit unserer Crate Code wie Listing 20-37 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 20-37: Code, den ein Benutzer unserer Crate schreiben kann, wenn er unser prozedurales Makro benutzt
Dieser Code gibt Hallo Makro! Mein Name ist Pancakes! aus, wenn wir fertig
sind. Der erste Schritt ist das Erstellen einer neuen Bibliotheks-Crate, etwa
so:
$ cargo new hello_macro --lib
Als Nächstes definieren wir in Listing 20-38 das Trait HelloMacro und die
damit assoziierte Funktion.
Dateiname: src/lib.rs
#![allow(unused)]
fn main() {
pub trait HelloMacro {
fn hello_macro();
}
}
Listing 20-38: Ein einfaches Trait, das wir mit dem
Makro derive verwenden werden
Wir haben ein Trait und seine Funktion. An diesem Punkt könnte unser Crate-Benutzer das Trait so implementieren, dass die gewünschte Funktionalität erreicht wird, wie in Listing 20-39.
Dateiname: src/main.rs
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hallo Makro! Mein Name ist Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
Listing 20-39: Wie es aussehen würde, wenn Benutzer eine
manuelle Implementierung des Traits HelloMacro schreiben würden
Allerdings müssten sie den Implementierungsblock für jeden Typ, den sie mit
hello_macro verwenden wollten, schreiben; wir wollen ihnen diese Arbeit
ersparen.
Außerdem können wir die Funktion hello_macro noch nicht mit einer
Standardimplementierung versehen, die den Namen des Typs ausgibt, auf dem das
Trait implementiert ist: Rust hat keine Reflektionsfähigkeiten, sodass es den
Namen des Typs zur Laufzeit nicht nachschlagen kann. Wir benötigen ein Makro, um
zur Kompilierzeit Code zu generieren.
Der nächste Schritt ist das Definieren des prozeduralen Makros. Zum Zeitpunkt
der Abfassung dieses Dokuments müssen sich die prozeduralen Makros in einer
eigenen Crate befinden. Irgendwann könnte diese Einschränkung aufgehoben werden.
Die Konvention für die Strukturierung von Crates und Makro-Crates lautet wie
folgt: Für eine Crate mit dem Namen foo wird eine prozedurale Makro-Crate mit
einem benutzerdefinierten derive-Makro als foo_derive bezeichnet. Beginnen
wir eine neue Crate mit dem Namen hello_macro_derive innerhalb unseres
hello_macro-Projekts:
$ cargo new hello_macro_derive --lib
Unsere beiden Crates sind eng miteinander verwandt, daher erstellen wir die
prozedurale Makro-Crate innerhalb des Verzeichnisses unserer Crate
hello_macro. Wenn wir die Trait-Definition in hello_macro ändern, müssen wir
auch die Implementierung des prozeduralen Makros in hello_macro_derive ändern.
Die beiden Crates müssen getrennt veröffentlicht werden und Programmierer, die
diese Crates verwenden, müssen beide als Abhängigkeiten hinzufügen und beide in
den Gültigkeitsbereich bringen. Wir könnten stattdessen die Crate hello_macro
als Abhängigkeit hello_macro_derive verwenden lassen und den prozeduralen
Makrocode erneut exportieren. Wie auch immer, die Art und Weise, wie wir das
Projekt strukturiert haben, ermöglicht es den Programmierern, hello_macro zu
benutzen, selbst wenn sie die derive-Funktionalität nicht wollen.
Wir müssen die Crate hello_macro_derive als prozedurale Makro-Crate
deklarieren. Wie du gleich sehen wirst, benötigen wir auch Funktionalität von
den Crates syn und quote, also müssen wir sie als Abhängigkeiten angeben.
Füge das Folgende zur Datei Cargo.toml für hello_macro_derive hinzu:
Dateiname: hello_macro_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
Um mit der Definition des prozeduralen Makros zu beginnen, platziere den Code in
Listing 20-40 in deine Datei src/lib.rs der Crate hello_macro_derive.
Beachte, dass dieser Code nicht kompiliert werden kann, bis wir eine Definition
für die Funktion impl_hello_macro hinzufügen.
Dateiname: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Konstruiere eine Repräsentation des Rust-Codes als Syntaxbaum,
// den wir manipulieren können
let ast = syn::parse(input).unwrap();
// Baue die Trait-Implementierung
impl_hello_macro(&ast)
}
Listing 20-40: Code, den die meisten prozeduralen Makro-Crates benötigen, um Rust-Code zu verarbeiten
Beachte, dass wir den Code aufgeteilt haben in die Funktion
hello_macro_derive, die für das Parsen des TokenStream verantwortlich ist,
und die Funktion impl_hello_macro, die für die Transformation des Syntaxbaums
verantwortlich ist: Dies macht das Schreiben eines prozeduralen Makros bequemer.
Der Code in der äußeren Funktion (in diesem Fall hello_macro_derive) wird für
fast jede prozedurale Makro-Crate, die du siehst oder erstellst, derselbe sein.
Der Code, den du im Rumpf der inneren Funktion (in diesem Fall
impl_hello_macro) angibst, wird je nach Zweck deines prozeduralen Makros
unterschiedlich sein.
Wir haben drei neue Crates eingeführt: proc_macro, syn und
quote. Die Crate proc_macro kommt mit Rust, sodass wir das
nicht zu den Abhängigkeiten in Cargo.toml hinzufügen mussten. Die Crate
proc_macro ist die API des Compilers, die es uns erlaubt, den Rust-Code aus
unserem Code zu lesen und zu manipulieren.
Die Crate syn parst den Rust-Code von einem String in eine Datenstruktur, auf
der wir Operationen durchführen können. Die Crate quote wandelt
syn-Datenstrukturen wieder in Rust-Code um. Diese Crates machen es viel
einfacher, jede Art von Rust-Code zu parsen, den wir vielleicht verarbeiten
wollen: Einen vollständigen Parser für Rust-Code zu schreiben, ist keine
einfache Aufgabe.
Die Funktion hello_macro_derive wird aufgerufen, wenn ein Benutzer unserer
Bibliothek #[derive(HelloMacro)] an einen Typ spezifiziert. Dies ist möglich,
weil wir die Funktion hello_macro_derive hier mit proc_macro_derive
annotiert und den Namen HelloMacro angegeben haben, der unserem Trait-Namen
entspricht; dies ist die Konvention, der die meisten prozeduralen Makros folgen.
Die Funktion hello_macro_derive wandelt zunächst input aus einem
TokenStream in eine Datenstruktur um, die wir dann interpretieren und
Operationen darauf ausführen können. Hier kommt syn ins Spiel. Die Funktion
parse in syn nimmt einen TokenStream und gibt eine DeriveInput-Struktur
zurück, die den geparsten Rust-Code repräsentiert. Listing 20-41 zeigt die
relevanten Teile der Struktur DeriveInput, die wir vom Parsen des Strings
struct Pancakes; erhalten:
DeriveInput {
// --abschneiden--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
Listing 20-41: Die DeriveInput-Instanz, die wir
erhalten, wir den Codes mit dem Makro-Attribut aus Listing 20-37 parsen
Die Felder dieser Struktur zeigen, dass der Rust-Code, den wir geparst haben,
eine Einheitsstruktur (unit struct) mit dem ident (identifier, Bezeichner,
d.h. dem Namen) von Pancakes ist. Es gibt weitere Felder in dieser Struktur
zur Beschreibung aller Arten von Rust-Code; weitere Informationen findest du in
der syn-Dokumentation für DeriveInput.
Bald werden wir die Funktion impl_hello_macro definieren, wo wir den neuen
Rust-Code bauen werden, den wir einbinden wollen. Aber bevor wir das tun,
beachte, dass die Ausgabe für unser derive-Makro ebenfalls ein TokenStream
ist. Der zurückgegebene TokenStream wird dem Code hinzugefügt, den unsere
Crate-Benutzer schreiben. Wenn sie also ihre Crate kompilieren, erhalten sie die
zusätzliche Funktionalität, die wir im modifizierten TokenStream zur Verfügung
stellen.
Du hast vielleicht bemerkt, dass wir unwrap aufrufen, um die Funktion
hello_macro_derive abzubrechen, wenn der Aufruf der Funktion syn::parse
fehlschlägt. Es ist notwendig, dass unser prozedurales Makro bei Fehlern
abbricht, weil proc_macro_derive-Funktionen einen TokenStream zurückgeben
müssen, kein Result, um mit der prozeduralen Makro-API konform zu sein. Wir
haben dieses Beispiel vereinfacht, indem wir unwrap verwendet haben; in
Produktionscode solltest du spezifischere Fehlermeldungen darüber angeben, was
schief gelaufen ist, indem du panic! oder expect verwendest.
Da wir nun den Code haben, um den annotierten Rust-Code aus einem TokenStream
in eine DeriveInput-Instanz zu verwandeln, lass uns den Code generieren, der
das Trait HelloMacro auf dem annotierten Typ implementiert, wie in Listing
20-42 gezeigt.
Dateiname: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Konstruiere eine Repräsentation des Rust-Codes als Syntaxbaum,
// den wir manipulieren können
let ast = syn::parse(input).unwrap();
// Baue die Trait-Implementierung
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let generated = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hallo Makro! Mein Name ist {}!", stringify!(#name));
}
}
};
generated.into()
}
Listing 20-42: Implementierung des Traits HelloMacro
unter Verwendung des geparsten Rust-Codes
Wir erhalten eine Ident-Strukturinstanz, die den Namen (Bezeichner) des
annotierten Typs enthält, indem wir ast.ident verwenden. Die Struktur in
Listing 20-41 zeigt, dass, wenn wir die Funktion impl_hello_macro auf den Code
in Listing 20-37 anwenden, das erhaltene ident ein Feld ident mit dem Wert
"Pancakes" enthält. So wird die Variable name in Listing 20-42 eine Instanz
der Struktur Ident enthalten, die den String "Pancakes" ausgibt, dem Namen
der Struktur in Listing 20-37.
Mit dem Makro quote! können wir den Rust-Code definieren, den wir zurückgeben
wollen. Der Compiler erwartet etwas anderes als das direkte Ergebnis der
Ausführung des quote!-Makros, also müssen wir es in einen TokenStream
konvertieren. Wir tun dies, indem wir die Methode into aufrufen, die diese
Zwischendarstellung konsumiert und einen Wert des erforderlichen Typs
TokenStream zurückgibt.
Das Makro quote! bietet auch einige sehr coole Vorlage-Mechanismen: Wir können
#name eingeben und quote! wird es durch den Wert in der Variablen name
ersetzen. Du kannst sogar einige Wiederholungen machen, ähnlich wie normale
Makros funktionieren. Schaue dir die Dokumentation der Crate
quote! für eine gründliche Einführung an.
Wir wollen, dass unser prozedurales Makro eine Implementierung unseres Traits
HelloMacro für den Typ, den der Benutzer annotiert hat, erzeugt, die wir mit
#name erhalten können. Die Trait-Implementierung hat eine Funktion
hello_macro, deren Rumpf die Funktionalität enthält, die wir zur Verfügung
stellen wollen: Ausgeben von Hallo Makro! Mein Name ist und dann der Name des
annotierten Typs.
Das hier verwendete Makro stringify! ist in Rust eingebaut. Es nimmt einen
Rust-Ausdruck, z.B. 1 + 2, und verwandelt diesen zur Kompilierzeit in ein
String-Literal, z.B. "1 + 2". Dies unterscheidet sich von Makros wie format!
und println!, die den Ausdruck auswerten und dann das Ergebnis in einen
String umwandeln. Es besteht die Möglichkeit, dass die Eingabe #name ein
Ausdruck ist, der literal auszugeben ist, also verwenden wir stringify!. Die
Verwendung von stringify! erspart zudem eine Speicherzuweisung, indem #name
zur Kompilierzeit in ein String-Literal umgewandelt wird.
An diesem Punkt sollte cargo build sowohl bei hello_macro als auch bei
hello_macro_derive erfolgreich durchlaufen. Schließen wir diese Crates an den
Code in Listing 20-37 an, um das prozedurale Makro in Aktion zu sehen! Erstelle
ein neues Binärprojekt in deinem projects-Verzeichnis durch Aufrufen von
cargo new pancakes. Wir müssen hello_macro und hello_macro_derive als
Abhängigkeiten in der Datei Cargo.toml der Crate pancakes hinzufügen. Wenn
du deine Versionen von hello_macro und hello_macro_derive in
crates.io veröffentlichst, wären das reguläre Abhängigkeiten; wenn
nicht, kannst du sie wie folgt als path-Abhängigkeiten angeben:
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
Gib den Code in Listing 20-37 in src/main.rs ein und rufe cargo run auf:
Es sollte Hallo Makro! Mein Name ist Pancakes! ausgeben. Die Implementierung
des Traits HelloMacro aus dem prozeduralen Makro wurde eingefügt, ohne dass
die Crate pancakes es implementieren musste; #[derive(HelloMacro)] fügte die
Trait-Implementierung hinzu.
Als Nächstes wollen wir untersuchen, inwiefern sich die anderen Arten prozeduraler Makros von den benutzerdefinierten derive-Makros unterscheiden.
Attribut-ähnliche Makros
Attribut-ähnliche Makros ähneln den benutzerdefinierten derive-Makros, aber
anstatt Code für das derive-Attribut zu generieren, erlauben sie dir, neue
Attribute zu erstellen. Sie sind auch flexibler: derive funktioniert nur bei
Strukturen und Aufzählungen; Attribute können auch auf andere Elemente, z.B.
Funktionen, angewendet werden. Hier ist ein Beispiel für die Verwendung eines
Attribut-ähnlichen Makros. Nehmen wir an, du hast ein Attribut namens route,
das Funktionen annotiert, wenn du ein Webapplikations-Framework verwendest:
#[route(GET, "/")]
fn index() {
Dieses Attribut #[route] würde durch das Framework als prozedurales Makro
definiert werden. Die Signatur der Makrodefinitionsfunktion würde wie folgt
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 die
Inhalte GET, "/" des Attributs. Der zweite ist für den Rumpf des Elements, an
den das Attribut angehängt ist: In diesem Fall fn index() {} und der Rest des
Funktionsrumpfs.
Abgesehen davon funktionieren Attribut-ähnliche Makros auf die gleiche Weise wie
benutzerdefinierte derive-Makros: Sie erstellen eine Crate mit dem Crate-Typ
proc-macro 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; sie können
zum Beispiel eine unbekannte Anzahl von Argumenten aufnehmen. Makros können
jedoch nur mit der match-ähnlichen Syntax definiert werden, die wir im
Abschnitt „Deklarative Makros für allgemeine Metaprogrammierung“
besprochen haben. Funktions-ähnliche Makros nehmen einen
TokenStream-Parameter und ihre Definition manipuliert diesen TokenStream
unter Verwendung von Rust-Code, wie es die beiden anderen Arten prozeduraler
Makros tun. Ein Beispiel für ein Funktions-ähnliches Makro ist ein Makro
sql!, das auf diese Weise aufgerufen werden könnte:
let sql = sql!(SELECT * FROM posts WHERE id=1);
Dieses Makro würde die darin enthaltene SQL-Anweisung parsen und prüfen, ob sie
syntaktisch korrekt ist, was eine viel komplexere Verarbeitung ist, als es ein
macro_rules!-Makro tun kann. Das Makro sql! 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 Token, die sich innerhalb der Klammern befinden, und geben den
Code zurück, den wir generieren wollen.
Zusammenfassung
Puh! Jetzt hast du einige Rust-Funktionalitäten in deinem Werkzeugkasten, die du nicht oft verwenden wirst, aber du wirst wissen, dass sie unter ganz bestimmten Umständen verfügbar sind. Wir haben mehrere komplexe Themen eingeführt, sodass du diese Konzepte und Syntax erkennen kannst, wenn du ihnen in Vorschlägen für Fehlermeldungen oder im Code anderer Leute begegnest. Verwende dieses Kapitel als Referenz, um Lösungen zu finden.
Als Nächstes werden wir alles, was wir im Laufe des Buches besprochen haben, in die Praxis umsetzen und ein weiteres Projekt durchführen!