Modul 19 — Makros: Code schreiben, der Code schreibt
macro_rules!, Muster, Erweiterung und Repetition: Wie Rust-Makros funktionieren. Lerne, wiederverwendbaren Code zu schreiben, der zur Compile-Zeit expandiert — von vec! bis zu eigenen Domain-Makros.
Was sind Makros? Code, der Code erzeugt
Lektion 88 von 7
Funktionen vs. Makros
Eine Funktion nimmt Werte entgegen. Ein Makro nimmt Code (Tokens)
entgegen und erzeugt daraus neuen Code, der zur Compile-Zeit eingefügt wird.
Das ! in println!, vec! und assert_eq! markiert einen Makro-Aufruf.
fn main() {
println!("Hallo, Welt!");
assert_eq!(6 + 4, 10);
}
macro_rules! — das Grundmuster
macro_rules! ist die Hauptart, eigene Makros zu schreiben. Ein Makro ist eine
Reihe von Mustern und Vorlagen:
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
fn main() {
say_hello!();
}
- Definitionsseite: kein
!nachsay_hello— das!gehört nur zum Aufruf. - Muster
()→ Vorlage{ println!(...); }: passen die Argumente, wird die Vorlage eingesetzt.
Drei Aufruf-Syntaxen
assert_eq!(6 + 4, 10); // Klammern
assert_eq![6 + 4, 10]; // eckige Klammern
assert_eq!{6 + 4, 10}; // geschweifte Klammern
Alle drei sind gleichwertig. Konvention: Klammern für assert_eq!, eckige für
vec!, geschweifte für macro_rules! selbst.
Der Zweck von Makros
Makros sind fast immer Convenience: Sie sparen Tipparbeit, indem sie
wiederkehrende Boilerplate erzeugen — etwa vec! statt einem manuellen
Vec::new() plus push-Aufrufen. Sie können aber auch Dinge, die Funktionen
nicht können: Sie können z. B. teure Argumente nur bei Bedarf auswerten
(wie assert! den Fehlertext) oder Operatoren-Syntax erzeugen.
Praxis-Szenario: Die Wiederholung
Sechs Feld-Handler sehen fast identisch aus: getter, setter, Validierung — pro Feld zwanzig Zeilen Copy-Paste. Ein neuer Feldtyp bedeutet sechs Stellen zum Vergessen. Der Teamlead erwähnt declarative Macros.
Übung: Einfaches Makro korrigieren
🧩 Übung m19-l88-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Muster & Erweiterung
Lektion 89 von 7
Muster: Metavariablen
Innerhalb eines Musters fängt $name:typ einen Argumentteil ein. $ kennzeichnet
eine Metavariable. Ein häufiger Typ ist expr (ein Ausdruck):
macro_rules! drucke_zweimal {
($v:expr) => {
println!("{v} {v}");
};
}
fn main() {
drucke_zweimal!(2 + 3); // 5 5
}
Die Metavariable $v enthält den Ausdruck 2 + 3 und wird an beiden Stellen
der Vorlage eingesetzt — der Ausdruck wird also zweimal ausgewertet.
Wichtige Fragmenttypen
expr— ein Ausdruck (2 + 3,foo(),x)ident— ein Bezeichner (my_var)ty— ein Typ (Vec<String>)tt— ein einzelnes Token (oder Token-Baum): der universelle Bausteinliteral— ein Literal (42,"text")pat— ein Muster
Die Erweiterung: Vorlage füllen
macro_rules! drucke_zweimal {
// Muster: Vorlage:
($v:expr) => { println!("{v} {v}"); };
}
Rust erweiternt Makros sehr früh beim Kompilieren: Der Compiler liest von vorn nach hinten und definiert/erweitert Makros, während er liest. Deshalb gilt: Ein Makro muss vor seinem ersten Aufruf definiert sein — anders als Funktionen, die irgendwo in der Crate stehen dürfen.
Ausdrücke werden mehrfach ausgewertet
Ein expr, das mehrfach in der Vorlage auftaucht, wird auch mehrfach
ausgewertet. Das ist gefährlich, wenn der Ausdruck teuer ist oder
Seiteneffekte hat:
drucke_zweimal!(konsumiere()); // wird ZWEIMAL aufgerufen!
Rust-Makros führen hier keine "Werte binden" durch wie Funktionen — sie fügen den Ausdruck textlich ein. Sei dir dessen bei Metavariablen bewusst.
Übung: Makro mit Wiederholung vervollständigen
🧩 Übung m19-l89-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Repetition: Listen verarbeiten
Lektion 90 von 7
Das Herzstück von vec!
Ein Makro wie vec! muss eine beliebig lange Komma-Liste verarbeiten. Dafür
gibt es die Repetitions-Syntax:
$($x:expr),*
Das passt auf: null oder mehr expr, getrennt durch Kommas. Die Wiederholung
wird im Template mit $(...) geschrieben.
Ein eigenes my_vec!
macro_rules! my_vec {
($($x:expr),*) => {{
let mut v = Vec::new();
$(v.push($x);)*
v
}};
}
fn main() {
let zahlen = my_vec!(1, 2, 3, 4);
println!("{:?}", zahlen); // [1, 2, 3, 4]
}
Was hier passiert:
$($x:expr),*matcht die Liste der Argumente.$(v.push($x);)*wiederholt denpush-Aufruf einmal pro Argument:v.push(1); v.push(2); v.push(3); v.push(4);
Mehrere Wiederholungs-Formen
Die Standard-Bibliothek implementiert drei vec!-Formen — alle aus Mustern:
let leere: Vec<i32> = vec![]; // leeres Muster
let buffer = vec![0u8; 1000]; // x; n → Repeat
let noodles = vec!["udon", "ramen", "soba"]; // Liste
Ein Makro probiert seine Regeln der Reihe nach durch. Passt keine, ist es
ein Fehler — das kannst du mit compile_error! freundlicher gestalten.
$(...),* im Detail
Die Repetition besteht aus zwei Teilen:
$(PATTERN),*— im Muster: passt auf eine komma-getrennte Liste, deren jedes ElementPATTERNentspricht.*heißt "0 oder mehr",+"1 oder mehr".$(...)*— in der Vorlage: wiederholt den enthaltenen Code für jedes gefangene Element.
Man skaliert typischerweise so: Original.
macro_rules! log_alle {
($($arg:expr),*) => {
$(eprintln!("{arg}");)*
};
}
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Built-In Makros: file!, line!, stringify!
Lektion 91 von 7
Compiler-Makros
Einige Makros sind in rustc fest verdrahtet und könnten mit macro_rules!
allein nicht gebaut werden:
file!()— die Datei, in der der Aufruf steht (als String-Literal)line!()— die aktuelle Zeilennummer (u32, ab 1)column!()— die aktuelle Spalte (u32, ab 1)module_path!()— der Rust-Modulpfad wie"fernlib::fronds"stringify!(...tokens...)— die Tokens als String-Literalconcat!(str0, str1, ...)— legt Strings aneinandercompile_error!("msg")— bricht die Compilierung mit einer Meldung ab
stringify! in Aktion
fn main() {
let s = stringify!(1 + 2);
println!("{s}"); // druckt: 1 + 2
// Makro-Aufrufe werden NICHT expandiert:
let t = stringify!(line!());
println!("{t}"); // druckt: line!()
}
assert! nutzt stringify!, um eine Fehlermeldung zu erzeugen, die den
Original-Code der Assertion enthält.
compile_error! für bessere Fehlermeldungen
Makros geben sonst kryptische Fehler wie "no rules expected". Mit einem Muster, das häufige Benutzerfehler abfängt, liefert dein Makro eine klare Meldung:
macro_rules! brauche_zwei {
($a:expr, $b:expr) => { $a + $b };
($($x:expr),*) => {
compile_error!("brauche_zwei! erwartet genau zwei Argumente")
};
}
Woher weiß file!/line! den Ort?
file!, line! und column! melden den Ort des ersten Makro-Aufrufs.
Ruft Makro A Makro B auf, das in einer anderen Datei steht, liefert file!()
die Datei des ersten Aufrufs — also des Anwenders, nicht der Makro-Definition.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Hygiene & Scoping
Lektion 92 von 7
Das Problem: Code aus verschiedenen Scopes
Ein Makro klebt Code aus verschiedenen Bereichen zusammen. Zwei Fragen:
- Kollidiert eine lokale Variable im Makro mit einer im Aufrufer?
- Wie finde ich ein Item (Typ/Funktion), das im Makro verwendet wird?
Lokale Variablen sind hygienisch
Lokale Variablen und Argumente, die dein Makro intern einführt, sind hygienisch: Sie können nicht mit Variablen des Aufrufers kollidieren.
macro_rules! swap {
($a:expr, $b:expr) => {{
let tmp = $a;
*$a = *$b;
*$b = tmp;
}};
}
fn main() {
let mut x = 1;
let mut y = 2;
let mut tmp = 999; // kein Konflikt mit dem internen tmp!
swap!(&mut x, &mut y);
println!("x={x} y={y} tmp={tmp}"); // x=2 y=1 tmp=999
}
Das interne tmp im Makro stört die tmp-Variable im Aufrufer nicht — genau
dafür gibt es Hygiene.
Items sind NICHT hygienisch
Typen, Funktionen, Module und andere Items, die dein Makro referenziert, werden
im Aufruf-Scope aufgelöst. Deshalb schreiben Standard-Makros gerne
::std::vec::Vec statt Vec — so treffen sie immer den richtigen Namensraum,
auch wenn der Aufrufer ein eigenes Vec definiert hat.
Die Regel in einem Satz
Lokale Variablen & Argumente: hygienisch (kein Konflikt mit dem Aufrufer).
Items & andere Namen: nicht hygienisch (im Aufruf-Scope aufgelöst). Für
robuste Makros: Importpfade wie ::std::... verwenden.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Makros importieren & beyond macro_rules!
Lektion 93 von 7
Makros exportieren und importieren
Ein mit macro_rules! definiertes Makro ist ab seiner Definition im selben
Modul sichtbar. Um es nach außen zu geben, schreibt man #[macro_export]:
// lib.rs
#[macro_export]
macro_rules! drucke {
($($arg:expr),*) => {
$(println!("{arg}");)*
};
}
Mit #[macro_export] landet das Makro auf der Crate-Root und ist für
Außenstehende unter meine_crate::drucke! erreichbar. Innerhalb einer Crate
verwendet use crate::... (bzw. use meine_crate::drucke im Crate-Grenzfall
#[macro_use]).
Debugging: trace_macros! und log_syntax!
Makros sind oft schwer zu debuggen, weil man die expandierte Form nicht sieht. Nightly bietet zwei experimentelle Helfer:
trace_macros!(true)— druckt jede Makro-Expansionlog_syntax!()— druckt die Argumente, ohne zu expandierencargo expand(externes Tool) — zeigt den voll expandierten Code
Beyond macro_rules!: Prozedurale Makros
Neben den deklarativen macro_rules!-Makros gibt es prozedurale Makros
(Prozeduren, die zur Compile-Zeit laufen und Tokens transformieren):
#[derive(...)]— kann z. B.DebugoderCloneautomatisch generieren- Attribute-Makros — wie
#[test]oder Web-Router-Annotationen - Funktionsähnliche Makros — wie
macro_rules!, aber mit vollem Code
derive(Serialize), derive(Deserialize) in Serde oder #[tokio::main] sind
bekannte prozedurale Makros aus der Praxis.
Wann Makro, wann Funktion?
Faustregel: Funktion, wenn möglich. Makros erhöhen Komplexität, sind
typischerweise nur lokal sichtbar und schwerer zu debuggen. Greif zu einem
Makro, wenn du echte Metaprogrammierung brauchst: Listen-Syntax erzeugen,
Boilerplate generieren (derive), oder Lazy Evaluation verlangen. Sonst: eine
normale Funktion ist einfacher, lesbarer und testbar.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Übung: Ein eigenes Makro bauen
Lektion 94 von 7
Projekt: Ein unwrap-entlastendes Makro
Du baust jetzt ein eigenes deklaratives Makro, das mehrere Fähigkeiten dieses Moduls kombiniert:
- Definiere ein Makro
lauf_und_fang!mitmacro_rules!. - Es nimmt einen Ausdruck entgegen (Metavariable
$v:expr). - Führe den Ausdruck aus; schlägt er
panic!aus, fange den Fehler und gibErrzurück; sonst gibOkzurück. - Nutze ein Built-In-Makro (
stringify!), um im Fehlerfall die betroffene Codezeile im Klartext auszugeben.
Das prüft: Mustermatching, Metavariablen, Built-In-Makros — und verlangt ja,
kurz über Hygiene nachzudenken, weil du catch_unwind mit UnwindSafe nutzt.
Rezepte aus diesem Modul
- Makro definieren:
macro_rules! name { (MUSTER) => { VORLAGE }; } - Ausdruck fangen:
$v:expr - Listen verarbeiten:
$($x:expr),*+$(...)* - Ort/Text ausgeben:
file!(),line!(),stringify!($v) - Fehler melden:
compile_error!("...")
Übung: Lauf-und-Fang-Makro
🧩 Übung m19-l7-e1
🔮 Vorhersage: Was passiert beim Ausführen?
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren: