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 Attributderive
hinzugefü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 Merkmale
(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 Merkmal auf einen bestimmten Typ
implementieren kann. Eine Funktion kann das nicht, weil sie zur Laufzeit
aufgerufen wird und ein Merkmal 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 mit macro_rules!
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. Makros vergleichen ebenfalls einen Wert mit
Mustern, die mit einem bestimmten Code verknüpft sind: In diesem Fall ist
der Wert der literale Rust-Quellcode, der an das Makro übergeben wird; die
Muster werden mit der Struktur dieses Quellcodes verglichen; und der mit jedem
Muster verküpften Code ersetzt, wenn er passt, 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 ganze 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 Zeichenkettenanteilstypen (string slices) zu
erstellen. Mit einer Funktion wäre das nicht möglich,
da uns weder die Anzahl noch den Typ der Werte im Voraus bekannt ist.
Codeblock 19-28 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 } }; } }
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 Kiste (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 Name, 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 18 behandelten Mustersyntax, da Makromuster mit der Rust-Codestruktur und nicht mit Werten abgeglichen werden. Lass uns im Folgenden die Bedeutung der Musterteile in Listing 19-28 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 optional
nach dem Code erscheinen könnte, der mit dem Code in $()
übereinstimmt. 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“ (engl. „Das kleine Buch der Rust-Makros“).
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 Kiste mit einem speziellen Kistentyp befinden. Dies geschieht aus
komplexen technischen Gründen, die wir hoffentlich in Zukunft eliminieren
werden. In Codeblock 19-29 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;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Die Funktion, die ein prozedurales Makro definiert, nimmt einen TokenStream
als Eingabe und erzeugt einen TokenStream
als Ausgabe. Der Typ TokenStream
wird durch die Kiste 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 Kiste 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.
Wie man ein benutzerdefiniertes Makro mit derive
schreibt
Lass uns eine Kiste namens hello_macro
erstellen, die ein Merkmal namens
HelloMacro
mit einer assoziierten Funktion namens hello_macro
definiert.
Anstatt unsere Benutzer dazu zu bringen, das Merkmal 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 Merkmal definiert wurde.
Mit anderen Worten, wir werden eine Kiste schreiben, die es einem anderen
Programmierer ermöglicht, mit unserer Kiste Code wie Codeblock 19-30 zu
schreiben.
Dateiname: src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
Dieser Code gibt Hallo Makro! Mein Name ist Pancakes!
aus, wenn wir fertig
sind. Der erste Schritt ist das Erstellen einer neuen Bibliothekskiste (library
crate), etwa so:
$ cargo new hello_macro --lib
Als Nächstes definieren wir das Merkmal HelloMacro
und die damit assoziierte
Funktion:
Dateiname: src/lib.rs
#![allow(unused)] fn main() { pub trait HelloMacro { fn hello_macro(); } }
Wir haben ein Merkmal und seine Funktion. An diesem Punkt könnte unser Kistenbenutzer das Merkmal so implementieren, dass die gewünschte Funktionalität erreicht wird:
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();
}
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
Merkmal 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 Kiste befinden. Irgendwann könnte diese Einschränkung aufgehoben
werden. Die Konvention für die Strukturierung von Kisten und Makrokisten lautet
wie folgt: Für eine Kiste mit dem Namen foo
wird eine prozedurale Makrokiste
mit einem benutzerdefinierten derive-Makro als foo_derive
bezeichnet.
Beginnen wir eine neue Kiste mit dem Namen hello_macro_derive
innerhalb
unseres hello_macro
-Projekts:
$ cargo new hello_macro_derive --lib
Unsere beiden Kisten sind eng miteinander verwandt, daher erstellen wir die
prozedurale Makrokiste innerhalb des Verzeichnisses unserer Kiste
hello_macro
. Wenn wir die Merkmalsdefinition in hello_macro
ändern, müssen
wir auch die Implementierung des prozeduralen Makros in hello_macro_derive
ändern. Die beiden Kisten müssen getrennt veröffentlicht werden und
Programmierer, die diese Kisten verwenden, müssen beide als Abhängigkeiten
hinzufügen und beide in den Gültigkeitsbereich bringen. Wir könnten stattdessen
die Kiste 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 Kiste hello_macro_derive
als prozedurale Makro-Kiste
deklarieren. Wie du gleich sehen wirst, benötigen wir auch Funktionalität von
den Kisten 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 Codeblock 19-31 in deine Datei src/lib.rs der Kiste 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
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[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 Merkmal-Implementierung
impl_hello_macro(&ast)
}
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-Kiste, 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 Kisten eingeführt: proc_macro
, syn
und
quote
. Die Kiste proc_macro
kommt mit Rust, sodass wir das
nicht zu den Abhängigkeiten in Cargo.toml hinzufügen mussten. Die Kiste
proc_macro
ist die API des Compilers, die es uns erlaubt, den Rust-Code aus
unserem Code zu lesen und zu manipulieren.
Die Kiste syn
parst den Rust-Code von einer Zeichenkette in eine
Datenstruktur, auf der wir Operationen durchführen können. Die Kiste quote
wandelt syn
-Datenstrukturen wieder in Rust-Code um. Diese Kisten 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 Merkmalsnamen
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. Codeblock 19-32 zeigt die
relevanten Teile der Struktur DeriveInput
, die wir vom Parsen der
Zeichenkette 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
)
}
)
}
Die Felder dieser Struktur zeigen, dass der Rust-Code, den wir geparst haben,
eine Einheitsstruktur (unit struct) mit dem ident
(identifier, engl.
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
Kisten-Benutzer schreiben. Wenn sie also ihre Kiste 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
abstürzen zu lassen, wenn der Aufruf der Funktion
syn::parse
hier fehlschlägt. Es ist notwendig, dass unser prozedurales Makro
bei Fehlern abstürzt, 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 Merkmal HelloMacro
auf dem annotierten Typ implementiert, wie in
Codeblock 19-33 gezeigt.
Dateiname: hello_macro_derive/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[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 Merkmal-Implementierung
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hallo Makro! Mein Name ist {}!", stringify!(#name));
}
}
};
gen.into()
}
Wir erhalten eine Ident
-Strukturinstanz, die den Namen (Bezeichner) des
annotierten Typs enthält, indem wir ast.ident
verwenden. Die Struktur in
Codeblock 19-32 zeigt, dass, wenn wir die Funktion impl_hello_macro
auf den
Code in Codeblock 19-30 anwenden, das erhaltene ident
ein Feld ident
mit
dem Wert "Pancakes"
enthält. So wird die Variable name
in Codeblock 19-33
eine Instanz der Struktur Ident
enthalten, die die Zeichenkette "Pancakes"
ausgibt, der Name der Struktur in Codeblock 19-30.
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 into
-Methode 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 Kiste
quote!
für eine gründliche Einführung an.
Wir wollen, dass unser prozedurales Makro eine Implementierung unseres Merkmals
HelloMacro
für den Typ, den der Benutzer annotiert hat, erzeugt, die wir mit
#name
erhalten können. Die Merkmalssimplementierung 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
Zeichenketten-Literal, z.B. "1 + 2"
. Dies unterscheidet sich von format!
oder println!
; Makros, 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 Zeichenketten-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 Kisten an den
Code in Codeblock 19-30 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 Kiste 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 Codeblock 19-30 in src/main.rs ein und rufe cargo run
auf:
Es sollte Hallo Makro! Mein Name ist Pancakes!
ausgeben. Die Implementierung
des Merkmals HelloMacro
aus dem prozeduralen Makro wurde eingefügt, ohne dass
die Kiste pancakes
es implementieren musste; #[derive(HelloMacro)]
fügte
die Merkmalsimplementierung 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 Kiste mit dem
Kistentyp 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 mit macro_rules!
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!