Skip to content

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.

Fortschritt0/7 Lektionen
88

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 ! nach say_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?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

89

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 Baustein
  • literal — 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?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

90

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 den push-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 Element PATTERN entspricht. * 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

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

91

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-Literal
  • concat!(str0, str1, ...) — legt Strings aneinander
  • compile_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

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

92

Hygiene & Scoping

Lektion 92 von 7

Das Problem: Code aus verschiedenen Scopes

Ein Makro klebt Code aus verschiedenen Bereichen zusammen. Zwei Fragen:

  1. Kollidiert eine lokale Variable im Makro mit einer im Aufrufer?
  2. 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

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

93

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-Expansion
  • log_syntax!() — druckt die Argumente, ohne zu expandieren
  • cargo 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. Debug oder Clone automatisch 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

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

94

Ü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:

  1. Definiere ein Makro lauf_und_fang! mit macro_rules!.
  2. Es nimmt einen Ausdruck entgegen (Metavariable $v:expr).
  3. Führe den Ausdruck aus; schlägt er panic! aus, fange den Fehler und gib Err zurück; sonst gib Ok zurück.
  4. 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?

Editor wird geladen…
KI-Tutor (sokratisch — keine Komplettlösungen)

✅ Wichtige Erkenntnisse

0/3

Haken setzen, um deinen Lernfortschritt zu markieren:

This site uses essential cookies for Stripe payments. No tracking cookies.