Kapitel 17: Metaprogrammierung mit Makros – Die automatische Druckerpresse
Stell dir vor, du lebst im späten Mittelalter und besitzt ein Skriptorium (eine Schreibstube). Deine Aufgabe ist es, 500 Einladungen für das königliche Ritterturnier zu schreiben. Jede Einladung sieht fast identisch aus: „Edler Herr [Name], wir laden euch herzlich ein zum Turnier am [Datum] zu [Ort]…“
Wenn du jeden Brief einzeln von Hand schreibst, wirst du verrückt. Es dauert Wochen, deine Hand verkrampft und du machst garantiert irgendwann einen Rechtschreibfehler.
Dann erfindet Johannes Gutenberg die Druckerpresse mit beweglichen Lettern. Jetzt musst du die Einladung nur ein einziges Mal als Schablone (als Druckplatte) setzen. An den Stellen für Name, Datum und Ort lässt du Lücken. Wenn du nun ein Buch oder ein Blatt druckst, schiebst du einfach die konkreten Namen in die Lücken. Die Druckerpresse erzeugt in Sekundenschnelle fertige Briefe.
In Rust sind Makros genau diese Druckerpresse. Normale Funktionen arbeiten mit Werten zur Laufzeit (wenn das Programm gestartet ist). Makros hingegen arbeiten mit dem Programmiercode selbst zur Kompilierzeit (wenn das Programm gebaut wird). Sie lesen deinen geschriebenen Code, setzen ihn in Schablonen ein und drucken neuen Code, bevor das eigentliche Programm übersetzt wird.
1. Lernziele – Das wirst du heute lernen
- Funktion vs. Makro: Du verstehst den Unterschied zwischen Laufzeit- und Kompilierzeit-Code.
- Deklarative Makros schreiben: Du lernst, eigene Schablonen mit
macro_rules!zu erstellen. - Platzhalter nutzen: Du begreifst Fragment-Spezifizierer wie
exprundident. - Wiederholungen einbauen: Du baust Makros, die beliebig viele Argumente annehmen.
- Das Prinzip der Hygiene: Du verstehst, warum sich Makro-Variablen nicht mit deinem restlichen Code beißen.
2. Der Unterschied zwischen Funktionen und Makros
Bevor wir Code schreiben, klären wir, warum wir überhaupt Makros brauchen:
- Beliebig viele Argumente: Eine normale Funktion in Rust muss immer eine feste Anzahl an Argumenten haben. Ein Makro wie
println!odervec!kann so viele Argumente annehmen, wie du willst. - Code erzeugen: Ein Makro kann neuen Rust-Code schreiben, z. B. Strukturen oder Funktionen für dich definieren. Eine Funktion kann das nicht.
- Kompilierzeit: Makros kosten zur Laufzeit absolut keine Leistung. Der Code wird vorab generiert.
3. Dein erstes deklaratives Makro: macro_rules!
Deklarative Makros sind die am häufigsten genutzten Makros in Rust. Wir definieren sie mit dem Schlüsselwort macro_rules!.
Schauen wir uns ein einfaches Makro an, das uns eine Begrüßung ausgibt:
// Wir deklarieren das Makro mit dem Namen "begruesse"
macro_rules! begruesse {
// 1. Muster: Wenn das Makro ohne Argumente gerufen wird: begruesse!()
() => {
println!("Hallo, mein Freund!");
};
// 2. Muster: Wenn das Makro mit einem Namen gerufen wird: begruesse!("Thorsten")
// $name ist ein Platzhalter. :expr sagt, dass ein Ausdruck erwartet wird.
($name:expr) => {
println!("Hallo, {}!", $name);
};
}
fn main() {
// Makros werden immer mit einem Ausrufezeichen (!) aufgerufen!
begruesse!(); // Gibt aus: Hallo, mein Freund!
begruesse!("Thorsten"); // Gibt aus: Hallo, Thorsten!
}
Der Aufbau der Platzhalter
In der Zeile ($name:expr) deklarieren wir eine Variable im Makro.
$name: Das Dollarzeichen sagt dem Compiler, dass dies eine Makro-Variable ist.:expr: Dies ist der Fragment-Spezifizierer (Designator). Er sagt: “Hier erwarte ich einen Ausdruck (Expression), z. B. eine Zahl, einen String oder eine Berechnung.”
Ein weiterer wichtiger Designator ist :ident (Identifier). Er steht für einen Bezeichner, also den physischen Namen einer Variable, Struktur oder Funktion:
macro_rules! erstelle_variable {
// Wir übergeben den NAMEN der neuen Variable ($name) und den WERT ($wert)
($name:ident, $wert:expr) => {
let $name = $wert;
};
}
fn main() {
// Wir erstellen eine Variable namens 'temperatur' mit dem Wert 22
erstelle_variable!(temperatur, 22);
// Nun existiert die Variable 'temperatur' in unserem Code!
println!("Temperatur: {}°C", temperatur);
}
4. Wiederholungen in Makros: Das eigene vec!
Oft möchtest du eine Liste von Elementen übergeben (wie bei vec![1, 2, 3]). Dazu bietet Rust eine Wiederholungs-Syntax:
$( ... ),* oder $( ... ),+
$( ... ): Der Teil, der wiederholt werden soll.,: Das Trennzeichen (hier ein Komma).*oder+:*bedeutet “0-mal oder beliebig oft”,+bedeutet “mindestens 1-mal”.
Lass uns ein eigenes Makro schreiben, das Elemente in einen Vektor schiebt:
macro_rules! mein_vektor {
// $( $x:expr ),* bedeutet:
// Beliebig viele Ausdrücke ($x), getrennt durch Kommas.
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
// Dieser Block $( ... )* wird für jeden gematchten Ausdruck wiederholt!
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
fn main() {
let liste = mein_vektor![10, 20, 30];
println!("Liste: {:?}", liste); // [10, 20, 30]
}
5. Makro-Hygiene: Schutz vor Variablen-Kollisionen
In vielen älteren Sprachen (wie C) sind Makros gefährlich, weil sie reiner Textaustausch sind. Wenn ein Makro intern eine Variable deklariert, die denselben Namen hat wie eine Variable in deiner main-Funktion, kommt es zu Fehlern.
In Rust sind Makros hygienisch. Das bedeutet, dass Variablen, die im Makro deklariert werden, in einem eigenen unsichtbaren Namensraum leben:
macro_rules! setze_geheimnis {
() => {
let x = 42; // Variable x im Makro deklariert
};
}
fn main() {
let x = 100;
setze_geheimnis!();
// Welches x wird hier gedruckt?
println!("x ist: {}", x); // Gibt immer noch 100 aus!
}
Obwohl das Makro intern let x = 42 ausgeführt hat, bleibt das x in der main-Funktion unberührt. Der Compiler trennt die beiden Variablen strikt voneinander.
6. Compilerfehler-Show: Typische Fehler verstehen
Ein klassischer Fehler bei Anfängern betrifft die Reihenfolge der Definition.
fn main() {
hallo!(); // Compilerfehler!
}
macro_rules! hallo {
() => {
println!("Hallo!");
};
}
Die Fehlermeldung des Compilers:
error: cannot find macro `hallo` in this scope
--> src/main.rs:2:5
|
2 | hallo!();
| ^^^^^
Die Erklärung:
Der Rust-Compiler liest Dateien von oben nach unten. Da Makros den Code zur Kompilierzeit manipulieren, müssen sie vor ihrer ersten Verwendung definiert sein.
Die Lösung: Schiebe die Definition des Makros über die main-Funktion!
7. Zusammenfassung
- Makros erzeugen Code zur Kompilierzeit und sparen uns lästige Schreibarbeit.
macro_rules!definiert ein deklaratives Makro mit Pattern Matching.- Designators bestimmen den Typ der Lücken:
:exprfür Ausdrücke,:identfür Bezeichner/Namen. - Mit der Syntax
$( ... ),*verarbeiten wir beliebig viele Argumente. - Dank Makro-Hygiene kommen sich interne Makro-Variablen und dein restlicher Code niemals in die Quere.