Prozedurale Makros in Rust: dein eigenes #[derive] schreiben
Jedes Mal, wenn du #[derive(Debug)] oder #[derive(Serialize)] schreibst, läuft ein kleines Programm im Compiler. Es schaut sich dein Struct an und schreibt einen impl-Block für dich. Dieses kleine Programm ist ein prozedurales Makro (Procedural Macro). Und du kannst auch eigene schreiben.
Stell es dir wie einen hilfsbereiten Sachbearbeiter vor. Du gibst ihm ein Formular (dein Struct). Er liest jedes Feld und gibt dir den langweiligen Papierkram fertig ausgefüllt zurück. Auf der Seite Makros hast du macro_rules! kennengelernt. Das arbeitet mit Mustern, die es vergleicht. Prozedurale Makros sind anders: Es sind ganz normale Rust-Funktionen, die Code bekommen und Code zurückgeben. Auf dieser Seite baust du Schritt für Schritt ein echtes und siehst die echte Ausgabe.
Die drei Arten prozeduraler Makros
Abschnitt betitelt „Die drei Arten prozeduraler Makros“Es gibt drei Arten. Innen funktionieren sie alle gleich, aber für die Person, die sie benutzt, sehen sie unterschiedlich aus:
| Art | Was man beim Benutzen schreibt | Bekannte Beispiele |
|---|---|---|
| Derive-Makro | #[derive(Serialize)] über einem Struct oder Enum |
Serialize von serde, Error von thiserror, Parser von clap |
| Attribut-Makro | #[route(GET, "/")] über einer Funktion oder einem anderen Element |
#[tokio::main], #[get("/")] von actix-web |
| Funktionsähnliches Makro | sql!(SELECT * FROM users) |
query! von sqlx, html! von yew |
- Ein Derive-Makro fügt nur Code neben deinem Typ hinzu. Dein Struct bleibt genau so, wie du es geschrieben hast.
- Ein Attribut-Makro ersetzt das Element, über dem es steht.
#[tokio::main]nimmt deinasync fn mainund macht daraus ein normalesfn main, das die tokio-Runtime startet. - Ein funktionsähnliches Makro sieht aus wie ein Aufruf von
macro_rules!. Aber hier entscheidet eine Rust-Funktion, was die Eingabe bedeutet. So kannsqlx::query!dein SQL schon beim Kompilieren prüfen.
Derive-Makros aus anderen Crates benutzt du schon längst. Hier erledigen zwei davon echte Arbeit. Serialize schreibt den JSON-Code (siehe JSON mit Serde), und Error schreibt den Display-Code aus dem Text in #[error(...)] (siehe anyhow und thiserror):
use serde::Serialize;use thiserror::Error;
#[derive(Serialize)]struct Order { id: u32, item: String, quantity: u8,}
#[derive(Debug, Error)]enum ShopError { #[error("Artikel {0} ist ausverkauft")] OutOfStock(String),}
fn main() { let order = Order { id: 42, item: "Teekanne".to_string(), quantity: 2 }; println!("{}", serde_json::to_string(&order).unwrap());
let err = ShopError::OutOfStock("Teekanne".to_string()); println!("{err}");}Ausgabe:
{"id":42,"item":"Teekanne","quantity":2}Artikel Teekanne ist ausverkauftDu hast nie ein impl Display oder JSON-Code geschrieben. Das haben die Derives erledigt, während dein Programm kompiliert wurde.
So funktionieren sie: Tokens rein, Tokens raus
Abschnitt betitelt „So funktionieren sie: Tokens rein, Tokens raus“Der Compiler zerlegt deinen Quellcode zuerst in Tokens: Namen, Satzzeichen, Literale, Klammern. Ein prozedurales Makro ist eine Funktion, die diese Tokens als TokenStream bekommt und einen neuen TokenStream zurückgibt. Danach kompiliert der Compiler, was du zurückgegeben hast – so, als hättest du es selbst getippt.
Hier sind die drei Funktionsformen. Jede bekommt ein Attribut, damit der Compiler weiß, um welche Art es sich handelt:
use proc_macro::TokenStream;
// #[derive(HelloMacro)]: bekommt das Struct oder Enum, gibt Code zum HINZUFÜGEN zurück.#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { /* ... */ }
// #[route(GET, "/")]: bekommt die Tokens des Attributs und das Element, gibt einen ERSATZ zurück.#[proc_macro_attribute]pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { /* ... */ }
// sql!(...): bekommt alles in den Klammern, gibt den Ersatz zurück.#[proc_macro]pub fn sql(input: TokenStream) -> TokenStream { /* ... */ }Ein roher TokenStream ist nur eine Liste von Tokens. Damit zu arbeiten ist mühsam. Deshalb benutzt fast jedes prozedurale Makro drei Hilfs-Crates:
- syn macht aus Tokens einen Syntaxbaum. Statt Tokens bekommst du ein
DeriveInputmit dem Namen des Typs, seinen Generics und seinen Feldern. - quote macht aus Code, der wie Rust aussieht, wieder Tokens. In
quote! { ... }schreibst du normales Rust, und#namefügt eine Variable ein. - proc-macro2 ist eine Hülle um die
proc_macro-Typen des Compilers, die syn und quote benutzen. Es funktioniert auch außerhalb des Compilers. Das macht Hilfsfunktionen und Tests einfacher.
Ein wichtiger Gedanke: Dein Makro läuft beim Kompilieren. Cargo kompiliert zuerst das Makro-Crate, und zwar für deinen eigenen Rechner. Der Compiler führt es dann aus, während er das Crate baut, das es benutzt. Wenn das Programm am Ende läuft, ist das Makro längst weg. Übrig bleibt nur der Code, den es geschrieben hat.
Das Projekt einrichten
Abschnitt betitelt „Das Projekt einrichten“Ein prozedurales Makro muss in einem eigenen Crate mit einem besonderen Crate-Typ leben. Dieses Crate darf nur Makros exportieren, keine normalen Funktionen oder Traits. Deshalb sieht man meist zwei Crates nebeneinander, wie serde und serde_derive: eins für den Trait, eins für das Derive. Wir fügen noch ein drittes Crate hinzu: ein kleines Programm namens shop, das beide benutzt. (Du willst Crates und Workspaces auffrischen? Schau in Cargo und Crates.)
mkdir procmacro-demo && cd procmacro-democargo new --lib hello_macrocargo new --lib hello_macro_derivecargo new shopSo sieht der fertige Dateibaum aus:
procmacro-demo/├── Cargo.toml (der Workspace)├── hello_macro/│ ├── Cargo.toml│ └── src/lib.rs (die Traits und ein Re-Export der Derives)├── hello_macro_derive/│ ├── Cargo.toml (proc-macro = true)│ └── src/lib.rs (die Makros selbst)└── shop/ ├── Cargo.toml └── src/main.rs (ein Programm, das #[derive(...)] benutzt)Die oberste Cargo.toml verbindet die drei Crates zu einem Workspace:
[workspace]members = ["hello_macro", "hello_macro_derive", "shop"]resolver = "2"Nur das Makro-Crate hat etwas Besonderes: proc-macro = true. Außerdem hängt es von syn, quote und proc-macro2 ab:
[package]name = "hello_macro_derive"version = "0.1.0"edition = "2024"
[lib]proc-macro = true
[dependencies]proc-macro2 = "1"quote = "1"syn = "2"Die anderen beiden Crates sind ganz normal. hello_macro hängt vom Derive-Crate ab, und shop braucht nur hello_macro:
[dependencies]hello_macro_derive = { version = "0.1.0", path = "../hello_macro_derive" }[dependencies]hello_macro = { version = "0.1.0", path = "../hello_macro" }Schritt 1: #[derive(HelloMacro)]
Abschnitt betitelt „Schritt 1: #[derive(HelloMacro)]“Fangen wir ganz klein an. Jeder Typ mit #[derive(HelloMacro)] soll eine Funktion hello_macro() bekommen, die den Namen des Typs ausgibt. Zuerst der Trait, in hello_macro/src/lib.rs. Die letzte Zeile exportiert die Derive-Makros weiter (Re-Export). So brauchen Nutzer nur dieses eine Crate. Genau so macht es auch serde.
pub trait HelloMacro { fn hello_macro();}
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str];}
// Die Derive-Makros weiterexportieren, damit Nutzer nur dieses eine Crate brauchen.pub use hello_macro_derive::{Describe, HelloMacro};(Describe ist für Schritt 2.) Jetzt das Makro selbst, in hello_macro_derive/src/lib.rs:
use proc_macro::TokenStream;use quote::quote;use syn::{parse_macro_input, Data, DeriveInput, Fields}; // Data und Fields: Schritt 2
#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // 1. Die Tokens in einen Syntaxbaum umwandeln, den wir untersuchen können. let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident;
// 2. Den neuen Code mit quote! bauen. let generated = quote! { impl hello_macro::HelloMacro for #name { fn hello_macro() { println!("Hello, Macro! My name is {}!", stringify!(#name)); } } };
// 3. Die neuen Tokens an den Compiler zurückgeben. generated.into()}Drei Schritte. Du wirst sie in fast jedem Derive-Makro wiederfinden:
parse_macro_input!macht aus den Tokens einDeriveInput.ast.identist der Name des Typs, zum BeispielProduct. Wenn das Parsen fehlschlägt, gibt dieses Makro für dich einen ordentlichen Kompilierfehler zurück.quote!baut denimpl-Block.#namefügt den Namen ein.stringify!(#name)macht aus dem Namen den Text"Product"..into()wandelt quotesproc_macro2::TokenStreamin denproc_macro::TokenStreamdes Compilers um.
Achte auf den vollen Pfad hello_macro::HelloMacro. Der erzeugte Code wird in die Datei der Nutzerin oder des Nutzers eingefügt. Er kann sich also nicht auf deren use-Zeilen verlassen.
Wenn jemand #[derive(HelloMacro)] struct Product schreibt, fügt der Compiler Code hinzu, der genau wie diese handgeschriebene Version funktioniert. Diese Version läuft im Playground, weil kein Makro-Crate beteiligt ist. (Den Text lassen wir auf Englisch, damit er zum Beispielprojekt passt.)
trait HelloMacro { fn hello_macro();}
struct Product;
// Das schreibt #[derive(HelloMacro)] für dich:impl HelloMacro for Product { fn hello_macro() { println!("Hello, Macro! My name is {}!", "Product"); }}
fn main() { Product::hello_macro();}Ausgabe:
Hello, Macro! My name is Product!Das einmal von Hand zu schreiben, ist kein Problem. Bei 40 Typen lohnt sich ein Derive dann wirklich.
Schritt 2: #[derive(Describe)] liest die Felder
Abschnitt betitelt „Schritt 2: #[derive(Describe)] liest die Felder“HelloMacro hat nur den Namen des Typs benutzt. Echte Derives wie die von serde schauen sich die Felder an. Bauen wir also Describe: Es listet die Feldnamen eines Structs auf. Das ist zum Beispiel praktisch für die Kopfzeile einer CSV-Datei. syn gibt dir die Felder über ast.data:
Data::Struct,Data::EnumoderData::Union: welche Art von Typ es ist.- Bei einem Struct
Fields::Named(struct Product { id: u32 }),Fields::Unnamed(struct Meters(f64)) oderFields::Unit(struct Marker;).
Füge das unter dem ersten Makro in hello_macro_derive/src/lib.rs ein:
#[proc_macro_derive(Describe)]pub fn describe_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); expand_describe(&ast) .unwrap_or_else(|err| err.to_compile_error()) .into()}
fn expand_describe(ast: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> { let name = &ast.ident;
let Data::Struct(data) = &ast.data else { return Err(syn::Error::new_spanned( name, "Describe can only be derived for structs", )); };
let field_names: Vec<String> = match &data.fields { Fields::Named(fields) => fields .named .iter() .map(|field| field.ident.as_ref().unwrap().to_string()) .collect(), Fields::Unit => Vec::new(), Fields::Unnamed(_) => { return Err(syn::Error::new_spanned( name, "Describe needs named fields, like `struct Point { x: i32 }`", )); } };
Ok(quote! { impl hello_macro::Describe for #name { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] } } })}Das ist neu:
- Die eigentliche Arbeit passiert in einer normalen Hilfsfunktion, die
syn::Resultzurückgibt. So kannst du?undreturn Err(...)benutzen, wie bei jeder anderen Fehlerbehandlung. Die Makro-Funktion ganz oben macht aus einem Fehler nur noch einen Kompilierfehler. let ... elseerledigt den Fall „kein Struct“ sofort und hält den Normalfall schön flach.#(#field_names),*ist die Wiederholung (Repetition) von quote. Sie fügt jeden String aus demVecein, getrennt durch Kommas. Das ist dieselbe Idee wie$(...),*inmacro_rules!. JederStringwird zu einem String-Literal. Daraus wird also&["id", "name", "price_cents"].field.ident.as_ref().unwrap()ist hier sicher: BeiFields::Namedhat jedes Feld einen Namen.
Jetzt benutzt du beide Derives in shop/src/main.rs. Achte darauf: use hello_macro::{Describe, HelloMacro} holt die Traits und die Derive-Makros. Sie haben denselben Namen, leben aber in verschiedenen Namensräumen. Genau so funktioniert auch serde::Serialize.
use hello_macro::{Describe, HelloMacro};
#[derive(HelloMacro, Describe)]struct Product { id: u32, name: String, price_cents: u64,}
#[derive(HelloMacro, Describe)]struct Customer { email: String, city: String,}
fn main() { Product::hello_macro(); Customer::hello_macro();
println!("{} has fields {:?}", Product::type_name(), Product::field_names());
// Eine CSV-Datei: Die Kopfzeile kommt vom Derive, die Zeilen aus echten Werten. println!("{}", Customer::field_names().join(",")); println!("{},{}", ana.email, ana.city);
let teapot = Product { id: 7, name: "Teapot".to_string(), price_cents: 1999 }; println!("{} #{}: {} costs {} cents", Product::type_name(), teapot.id, teapot.name, teapot.price_cents);}Starte es im Workspace-Ordner:
cargo run -p shopAusgabe (bei einem frischen Build):
Compiling proc-macro2 v1.0.107 Compiling unicode-ident v1.0.27 Compiling quote v1.0.47 Compiling syn v2.0.119 Compiling hello_macro_derive v0.1.0 (/home/you/procmacro-demo/hello_macro_derive) Compiling hello_macro v0.1.0 (/home/you/procmacro-demo/hello_macro) Compiling shop v0.1.0 (/home/you/procmacro-demo/shop) Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.45s Running `target/debug/shop`Hello, Macro! My name is Product!Hello, Macro! My name is Customer!Product has fields ["id", "name", "price_cents"]email,city[email protected],LisbonProduct #7: Teapot costs 1999 centsDie Zeilen Compiling, Finished und Running gibt Cargo aus, und zwar immer auf Englisch. Schau dir die Reihenfolge an: Cargo baut syn und hello_macro_derive vor shop. Der Compiler muss dein Makro ja ausführen, während er shop kompiliert. Wenn du Customer ein Feld hinzufügst, passt sich die CSV-Kopfzeile beim nächsten Build von selbst an.
Gute Fehlermeldungen
Abschnitt betitelt „Gute Fehlermeldungen“Was passiert, wenn jemand #[derive(Describe)] über ein Enum schreibt? Weil expand_describe einen syn::Error zurückgibt, bekommt die Person einen ganz normal aussehenden Compilerfehler. (Die kaputten Beispiele auf dieser Seite stehen in shop/src/bin/broken.rs. Deshalb taucht dieser Pfad auf.)
use hello_macro::Describe;
#[derive(Describe)]enum OrderStatus { Pending, Shipped,}error: Describe can only be derived for structs --> shop/src/bin/broken.rs:4:6 |4 | enum OrderStatus { | ^^^^^^^^^^^Die Meldung ist der Text, den wir ins Makro geschrieben haben („Describe kann nur für Structs abgeleitet werden“). Zwei Dinge machen diesen Fehler gut:
syn::Error::new_spanned(name, ...)merkt sich, wo der Name in der Datei der Nutzerin oder des Nutzers steht (sein Span, also die Stelle im Code). Deshalb zeigen die^^^^^aufOrderStatusund nicht auf irgendeine Zeile in deinem Makro.to_compile_error()macht aus dem Fehler einen Aufruf voncompile_error!("..."). Der Compiler meldet ihn dann wie jeden anderen Fehler. Das Makrocompile_error!kannst du übrigens auch in normalem Code benutzen.
Ein Tuple Struct bekommt die andere Meldung („Describe braucht benannte Felder“). Auch sie zeigt auf die richtige Stelle:
error: Describe needs named fields, like `struct Point { x: i32 }` --> shop/src/bin/broken.rs:4:8 |4 | struct Meters(f64); | ^^^^^^Schreib deine Meldungen für die Person, die das Makro benutzt. Sag, was falsch ist und was stattdessen funktionieren würde.
Den erzeugten Code mit cargo expand ansehen
Abschnitt betitelt „Den erzeugten Code mit cargo expand ansehen“Makros können sich wie Zauberei anfühlen. Und Zauberei ist schwer zu debuggen. cargo-expand zeigt dir den Code, nachdem alle Makros gelaufen sind. Installiere es einmal (weitere nützliche Werkzeuge findest du unter Rust-Tools und Einrichtung):
cargo install cargo-expandcargo expand -p shopEin Teil davon, was es für unser Projekt ausgibt:
impl hello_macro::Describe for Product { fn type_name() -> &'static str { "Product" } fn field_names() -> &'static [&'static str] { &["id", "name", "price_cents"] }}impl hello_macro::Describe for Customer { fn type_name() -> &'static str { "Customer" } fn field_names() -> &'static [&'static str] { &["email", "city"] }}Da ist dein impl-Block, mit den eingesetzten Feldnamen, und stringify!(#name) ist schon zu "Product" geworden. Wenn ein Derive nicht tut, was du erwartest, starte zuerst cargo expand. Es expandiert auch println! und andere Makros. Such also nach deinen eigenen impl-Blöcken.
Dein Makro testen
Abschnitt betitelt „Dein Makro testen“Ein Makro-Crate kann sich selbst nur schwer testen, weil es nur Makros exportieren darf. Die einfache Lösung: Schreib normale Tests in einem Crate, das das Makro benutzt. Leg das in hello_macro/tests/describe.rs ab und starte cargo test -p hello_macro:
use hello_macro::Describe;
#[allow(dead_code)]#[derive(Describe)]struct Product { id: u32, name: String,}
#[test]fn lists_field_names_in_order() { assert_eq!(Product::type_name(), "Product"); assert_eq!(Product::field_names(), ["id", "name"]);}running 1 testtest lists_field_names_in_order ... okAber wie testest du, dass der Enum-Fall einen guten Fehler liefert? Mit einem normalen Test geht das nicht, denn die Testdatei würde gar nicht kompilieren. Genau dafür gibt es das Crate trybuild. Du legst kleine Dateien, die „fehlschlagen sollen“, in einen Ordner. trybuild kompiliert jede davon und vergleicht den Fehler des Compilers mit einer gespeicherten .stderr-Datei:
#[test]fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui/*.rs");}Die meisten beliebten Makro-Crates, auch serde und thiserror, testen ihre Fehlermeldungen auf diese Weise.
Wann lohnt sich ein eigenes Makro?
Abschnitt betitelt „Wann lohnt sich ein eigenes Makro?“Prozedurale Makros sind mächtig, aber sie haben ihren Preis. Sei ehrlich zu dir, bevor du eins schreibst:
- Kompilierzeit. syn ist ein großes Crate. Der erste Build jedes Projekts, das dein Makro benutzt, muss es kompilieren. (Für Derives reichen die Standard-Features von syn.
features = ["full"]brauchst du nur, um zum Beispiel Funktionsrümpfe zu parsen, und das macht es langsamer.) - Komplexität. Du hast jetzt ein Crate mehr, Code, der Code schreibt, und Fehler, die schwerer nachzuvollziehen sind.
- Werkzeuge. Editoren und rust-analyzer verstehen normalen Code besser als Code aus einem Makro.
Eine gute Reihenfolge zum Ausprobieren:
- Eine Funktion, ein Trait mit einer Default-Methode oder Generics. Die meisten Probleme mit „sich wiederholendem Code“ lösen sich schon hier.
- Ein
macro_rules!-Makro (siehe Makros). Es braucht kein zusätzliches Crate. - Ein prozedurales Makro, wenn der Code von der Struktur eines Typs abhängt (seinem Namen, seinen Feldern oder Varianten), viele Typen ihn brauchen und andere Leute das Makro benutzen werden. Deshalb setzen serde, thiserror und clap auf Derives.
Zum Vergleich hier Describe mit macro_rules!. Es funktioniert und braucht kein zusätzliches Crate. Aber das Struct muss innerhalb des Makroaufrufs stehen. Das wird schnell unhandlich, zum Beispiel wenn du auch Doku-Kommentare, pub-Felder oder andere Derives willst.
trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str];}
macro_rules! describe_struct { (struct $name:ident { $($field:ident : $ty:ty),* $(,)? }) => { struct $name { $($field: $ty),* }
impl Describe for $name { fn type_name() -> &'static str { stringify!($name) } fn field_names() -> &'static [&'static str] { &[$(stringify!($field)),*] } } };}
describe_struct!(struct Customer { email: String, city: String });
fn main() { println!("{} has fields {:?}", Customer::type_name(), Customer::field_names()); println!("{},{}", ana.email, ana.city);}Ausgabe:
Customer has fields ["email", "city"][email protected],LisbonFür ein oder zwei Typen in deinem eigenen Programm ist das (oder einfach ein handgeschriebenes impl) oft die bessere Wahl. Ein Derive ist angenehmer für die Leute, die es benutzen: Sie behalten ihr normales Struct und fügen nur eine Zeile hinzu.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“proc-macro = true vergessen. Ohne den Abschnitt [lib] in der Cargo.toml des Makro-Crates ist das Crate eine normale Bibliothek, und der Compiler weigert sich:
error: the `#[proc_macro_derive]` attribute is only usable with crates of the `proc-macro` crate type --> hello_macro_derive/src/lib.rs:5:1 |5 | #[proc_macro_derive(HelloMacro)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0432]: unresolved import `proc_macro`Der Compiler sagt dir: #[proc_macro_derive] geht nur in Crates vom Typ proc-macro. Deshalb findet er auch das Crate proc_macro nicht.
panic! oder unwrap() für Fehler der Nutzer. Das „funktioniert“, aber der Fehler ist vage und zeigt auf das ganze #[derive] statt auf das eigentliche Problem:
error: proc-macro derive panicked --> shop/src/bin/broken.rs:3:10 |3 | #[derive(Describe)] | ^^^^^^^^ | = help: message: Describe can only be derived for structsHier sagt der Compiler nur, dass das Derive-Makro eine Panic hatte, und zeigt die Meldung als Hilfe an. Gib stattdessen einen syn::Error mit new_spanned zurück, wie in Schritt 2.
Kurze Pfade im erzeugten Code. Wenn du impl Describe for #name schreibst statt impl hello_macro::Describe for #name, klappt das nur, wenn die Nutzerin oder der Nutzer zufällig Describe importiert hat. Wer #[derive(hello_macro::Describe)] schreibt, bekommt das hier:
error[E0405]: cannot find trait `Describe` in this scope --> shop/src/bin/broken.rs:1:10 |1 | #[derive(hello_macro::Describe)] | ^^^^^^^^^^^^^^^^^^^^^ not found in this scope | = note: this error originates in the derive macro `hello_macro::Describe`Heißt: Der Trait Describe wird in diesem Gültigkeitsbereich (Scope) nicht gefunden. Benutze in quote! deshalb immer volle Pfade: hello_macro::Describe, ::std::fmt::Display, ::core::option::Option.
Übungen
Abschnitt betitelt „Übungen“Diese Übungen verändern das echte Projekt. Deshalb gibt es hier keinen Ausprobieren-Button. Bau das Projekt von dieser Seite auf deinem eigenen Rechner nach und starte cargo run -p shop.
1. Die Felder zählen
Abschnitt betitelt „1. Die Felder zählen“Füge dem Trait Describe und dem Derive eine dritte Methode hinzu: fn field_count() -> usize. Product::field_count() soll 3 zurückgeben. Tipp: Den Vektor field_names hast du schon. Speichere field_names.len() vor quote! in einer Variablen und füge sie mit # ein.
Lösung anzeigen
Ergänze in hello_macro/src/lib.rs die Methode im Trait:
pub trait Describe { fn type_name() -> &'static str; fn field_names() -> &'static [&'static str]; fn field_count() -> usize;}Zähle in expand_describe die Felder und füge die Methode zum erzeugten impl hinzu:
let count = field_names.len();
Ok(quote! { impl hello_macro::Describe for #name { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] }
fn field_count() -> usize { #count } } })Dann gibt println!("{} has {} fields", Product::type_name(), Product::field_count()); in main Folgendes aus:
Product has 3 fieldsDas Zählen passiert in deinem Makro, also beim Kompilieren. cargo expand zeigt: Das Ergebnis ist einfach fn field_count() -> usize { 3usize }, eine Konstante. Zur Laufzeit gibt es nichts mehr zu tun.
2. Generische Structs unterstützen
Abschnitt betitelt „2. Generische Structs unterstützen“Probier #[derive(Describe)] an einem generischen Struct wie struct Page<T> { items: Vec<T>, number: u32 } aus. Das schlägt fehl mit error[E0107]: missing generics for struct Page (die Generics fehlen). Der erzeugte Code sagt nämlich impl hello_macro::Describe for Page und vergisst das <T>. Repariere das Derive, damit generische Structs funktionieren. Tipp: ast.generics.split_for_impl() liefert dir drei Teile, die du in die impl-Zeile einfügst. Unter Generics kannst du nachlesen, wie impl<T> aussieht.
Lösung anzeigen
Teile in expand_describe die Generics auf und benutze alle drei Teile in der impl-Zeile:
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
Ok(quote! { impl #impl_generics hello_macro::Describe for #name #ty_generics #where_clause { fn type_name() -> &'static str { stringify!(#name) }
fn field_names() -> &'static [&'static str] { &[#(#field_names),*] } } })Jetzt funktioniert das hier in shop:
#[derive(Describe)]struct Page<T> { items: Vec<T>, number: u32,}
fn main() { let page = Page { items: vec!["a", "b"], number: 1 }; println!("{} {} {:?}", page.items.len(), page.number, Page::<String>::field_names());}2 1 ["items", "number"]impl_generics ist <T> (mit allen Bounds), ty_generics ist <T>, und where_clause ist der where ...-Teil, falls es einen gibt. Jedes ernsthafte Derive macht das so. Merk dir also split_for_impl.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Ein prozedurales Makro ist eine Rust-Funktion, die beim Kompilieren läuft:
TokenStreamrein,TokenStreamraus. - Es gibt drei Arten: Derive (
#[derive(Serialize)]), Attribut (#[tokio::main]) und funktionsähnlich (sql!(...)). - Sie leben in einem eigenen Crate mit
proc-macro = true. Ein Haupt-Crate exportiert sie meist zusammen mit den Traits weiter. - syn parst die Eingabe (
DeriveInput,Data,Fields), und quote schreibt die Ausgabe (#name,#(...),*). - Melde Fehler der Nutzer mit
syn::Error::new_spanned(...).to_compile_error(), prüf das Ergebnis mitcargo expandund teste Fehler mit trybuild. - Probier zuerst eine Funktion, einen Trait oder
macro_rules!. Ein prozedurales Makro lohnt sich, wenn viele Typen Code brauchen, der auf ihrer Struktur basiert.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Wann läuft der Code eines prozeduralen Makros?
2. Welche Art von prozeduralem Makro ist #[tokio::main]?
3. Was machen syn und quote?
4. Dein Derive soll Enums ablehnen. Wie machst du das am besten?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Prozedurale Makros – Das Rust-Buch: der offizielle Rundgang durch ein
HelloMacro-Derive, dazu attribut-ähnliche und funktionsähnliche Makros (deutsche Übersetzung). - Procedural Macros – The Rust Reference (auf Englisch): die genauen Regeln für die drei Arten, Crate-Typen, Spans und Hygiene.
- Dokumentation von syn (auf Englisch):
DeriveInput,Data,Fields,Errorund die Parse-Hilfen, die auf dieser Seite vorkommen. - Dokumentation von quote (auf Englisch): wie die Interpolation mit
#varund die Wiederholung mit#(...),*inquote!funktionieren. - proc-macro-workshop (auf Englisch): kostenlose Praxisprojekte von David Tolnay (ein Builder-Derive, ein Debug-Derive und mehr) mit Tests, die dich Schritt für Schritt anleiten.