Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Makra

Makr takich jak println! używaliśmy w całej książce, ale nie zbadaliśmy jeszcze w pełni, czym jest makro i jak działa. Termin makro odnosi się do rodziny funkcji w Rust—makra deklaratywne z macro_rules! oraz trzy rodzaje makr proceduralnych:

  • Niestandardowe makra #[derive], które określają kod dodany za pomocą atrybutu derive używanego w strukturach i enumach
  • Makra podobne do atrybutów, które definiują niestandardowe atrybuty możliwe do użycia z dowolnym elementem
  • Makra podobne do funkcji, które wyglądają jak wywołania funkcji, ale operują na tokenach określonych jako ich argument

Będziemy mówić o każdym z nich po kolei, ale najpierw przyjrzyjmy się, dlaczego w ogóle potrzebujemy makr, skoro mamy już funkcje.

Różnica między makrami a funkcjami

Zasadniczo, makra to sposób pisania kodu, który pisze inny kod, co jest znane jako metaprogramowanie. W Dodatku C omawiamy atrybut derive, który generuje implementację różnych cech dla ciebie. Używaliśmy również makr println! i vec! w całej książce. Wszystkie te makra rozszerzają się, aby wyprodukować więcej kodu niż ten, który napisałeś ręcznie.

Metaprogramowanie jest przydatne do zmniejszania ilości kodu, który musisz napisać i utrzymać, co jest również jedną z ról funkcji. Jednak makra mają pewne dodatkowe moce, których funkcje nie mają.

Sygnatura funkcji musi deklarować liczbę i typ parametrów, które funkcja posiada. Makra natomiast mogą przyjmować zmienną liczbę parametrów: Możemy wywołać println!("hello") z jednym argumentem lub println!("hello {}", name) z dwoma argumentami. Ponadto, makra są rozszerzane zanim kompilator zinterpretuje znaczenie kodu, więc makro może na przykład zaimplementować cechę na danym typie. Funkcja tego nie może, ponieważ jest wywoływana w czasie wykonania, a cecha musi być zaimplementowana w czasie kompilacji.

Wadą implementacji makra zamiast funkcji jest to, że definicje makr są bardziej złożone niż definicje funkcji, ponieważ piszesz kod Rust, który pisze kod Rust. Z powodu tej pośredniości, definicje makr są zazwyczaj trudniejsze do czytania, zrozumienia i utrzymania niż definicje funkcji.

Inną ważną różnicą między makrami a funkcjami jest to, że musisz definiować makra lub wprowadzać je do zasięgu przed ich wywołaniem w pliku, w przeciwieństwie do funkcji, które możesz zdefiniować i wywołać w dowolnym miejscu.

Makra deklaratywne do ogólnego metaprogramowania

Najczęściej używaną formą makr w Rust są makra deklaratywne. Czasami są również nazywane „makrami na przykładach”, „makrami macro_rules!” lub po prostu „makrami”. W swej istocie makra deklaratywne pozwalają na pisanie codś podobnego do wyrażenia match w Rust. Jak omówiono w Rozdziale 6, wyrażenia match są strukturami kontrolnymi, które przyjmują wyrażenie, porównują wynikową wartość wyrażenia z wzorcami, a następnie uruchamiają kod skojarzony z pasującym wzorcem. Makra również porównują wartość z wzorcami, które są skojarzone z konkretnym kodem: w tej sytuacji wartością jest literał kodu źródłowego Rust przekazany do makra; wzorce są porównywane ze strukturą tego kodu źródłowego; a kod skojarzony z każdym wzorcem, gdy zostanie dopasowany, zastępuje kod przekazany do makra. Wszystko to dzieje się podczas kompilacji.

Aby zdefiniować makro, używasz konstrukcji macro_rules!. Zbadajmy, jak używać macro_rules!, przyglądając się, jak zdefiniowane jest makro vec!. Rozdział 8 omówił, jak możemy używać makra vec! do tworzenia nowego wektora z konkretnymi wartościami. Na przykład, poniższe makro tworzy nowy wektor zawierający trzy liczby całkowite:

#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}

Moglibyśmy również użyć makra vec!, aby stworzyć wektor dwóch liczb całkowitych lub wektor pięciu wycinków ciągów znaków. Nie bylibyśmy w stanie użyć funkcji do wykonania tego samego, ponieważ nie znalibyśmy liczby ani typu wartości z góry.

Listing 20-35 pokazuje nieco uproszczoną definicję makra vec!.

Filename: src/lib.rs
#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}
Listing 20-35: Uproszczona wersja definicji makra vec!

Uwaga: Rzeczywista definicja makra vec! w standardowej bibliotece zawiera kod do wcześniejszego przydzielania odpowiedniej ilości pamięci. Ten kod jest optymalizacją, której tutaj nie uwzględniamy, aby przykład był prostszy.

Adnotacja #[macro_export] wskazuje, że to makro powinno być dostępne zawsze, gdy pakiet, w którym makro jest zdefiniowane, zostanie wprowadzony do zasięgu. Bez tej adnotacji makro nie może zostać wprowadzone do zasięgu.

Następnie rozpoczynamy definicję makra od macro_rules! i nazwy definiowanego makra bez wykrzyknika. Nazwa, w tym przypadku vec, jest poprzedzona nawiasami klamrowymi oznaczającymi ciało definicji makra.

Struktura w ciele vec! jest podobna do struktury wyrażenia match. Tutaj mamy jedno ramię ze wzorcem ( $( $x:expr ),* ), po którym następuje => i blok kodu skojarzony z tym wzorcem. Jeśli wzorzec pasuje, skojarzony blok kodu zostanie wygenerowany. Biorąc pod uwagę, że jest to jedyny wzorzec w tym makrze, istnieje tylko jeden prawidłowy sposób dopasowania; każdy inny wzorzec spowoduje błąd. Bardziej złożone makra będą miały więcej niż jedno ramię.

Poprawna składnia wzorców w definicjach makr różni się od składni wzorców omówionej w Rozdziale 19, ponieważ wzorce makr są dopasowywane do struktury kodu Rust, a nie do wartości. Przejdźmy przez to, co oznaczają fragmenty wzorców w Listing 20-29; pełną składnię wzorców makr znajdziesz w Referencjach Rust.

Najpierw używamy zestawu nawiasów do objęcia całego wzorca. Używamy znaku dolara ($), aby zadeklarować zmienną w systemie makr, która będzie zawierać kod Rust pasujący do wzorca. Znak dolara jasno wskazuje, że jest to zmienna makra, w przeciwieństwie do zwykłej zmiennej Rust. Następnie następuje zestaw nawiasów, który przechwytuje wartości pasujące do wzorca w nawiasach do wykorzystania w kodzie zastępującym. Wewnątrz $() znajduje się $x:expr, który pasuje do dowolnego wyrażenia Rust i nadaje wyrażeniu nazwę $x.

Przecinek po $() wskazuje, że dosłowny znak separatora przecinka musi powtarzać się między każdą instancją kodu, który pasuje do kodu w $(). Gwiazdka * określa, że wzorzec pasuje do zera lub więcej elementów poprzedzających *.

Kiedy wywołujemy to makro za pomocą vec![1, 2, 3];, wzorzec $x pasuje trzy razy do trzech wyrażeń 1, 2 i 3.

Teraz przyjrzyjmy się wzorcowi w ciele kodu skojarzonego z tym ramieniem: temp_vec.push() w $()* jest generowane dla każdej części, która pasuje do $() we wzorcu zero lub więcej razy, w zależności od tego, ile razy wzorzec pasuje. $x jest zastępowane każdym pasującym wyrażeniem. Kiedy wywołujemy to makro za pomocą vec![1, 2, 3];, wygenerowany kod, który zastępuje to wywołanie makra, będzie następujący:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

Zdefiniowaliśmy makro, które może przyjmować dowolną liczbę argumentów dowolnego typu i generować kod do tworzenia wektora zawierającego określone elementy.

Aby dowiedzieć się więcej o tym, jak pisać makra, zajrzyj do dokumentacji online lub innych zasobów, takich jak „The Little Book of Rust Macros” rozpoczęta przez Daniela Keepa i kontynuowana przez Lukasa Wirtha.

Makra proceduralne do generowania kodu z atrybutów

Druga forma makr to makro proceduralne, które działa bardziej jak funkcja (i jest rodzajem procedury). Makra proceduralne przyjmują pewien kod jako wejście, operują na tym kodzie i produkują pewien kod jako wyjście, zamiast dopasowywać się do wzorców i zastępować kod innym kodem, jak to robią makra deklaratywne. Trzy rodzaje makr proceduralnych to niestandardowe derive, podobne do atrybutów i podobne do funkcji, a wszystkie działają w podobny sposób.

Podczas tworzenia makr proceduralnych definicje muszą znajdować się we własnym pakiecie ze specjalnym typem pakietu. Wynika to ze złożonych przyczyn technicznych, które mamy nadzieję wyeliminować w przyszłości. W Listing 20-36 pokazujemy, jak zdefiniować makro proceduralne, gdzie some_attribute jest zastępcą dla użycia konkretnej odmiany makra.

Filename: src/lib.rs
use proc_macro::TokenStream;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Listing 20-36: Przykład definiowania makra proceduralnego

Funkcja, która definiuje makro proceduralne, przyjmuje TokenStream jako wejście i produkuje TokenStream jako wyjście. Typ TokenStream jest zdefiniowany przez pakiet proc_macro, który jest dołączony do Rust i reprezentuje sekwencję tokenów. To jest rdzeń makra: Kod źródłowy, na którym makro operuje, tworzy wejściowy TokenStream, a kod, który makro produkuje, jest wyjściowym TokenStream. Funkcja posiada również dołączony atrybut, który określa, jaki rodzaj makra proceduralnego tworzymy. Możemy mieć wiele rodzajów makr proceduralnych w tym samym pakiecie.

Przyjrzyjmy się różnym rodzajom makr proceduralnych. Zaczniemy od niestandardowego makra derive, a następnie wyjaśnimy małe różnice, które sprawiają, że inne formy są odmienne.

Niestandardowe makra derive

Stwórzmy pakiet o nazwie hello_macro, który definiuje cechę nazwaną HelloMacro z jedną skojarzoną funkcją nazwaną hello_macro. Zamiast wymagać od naszych użytkowników implementowania cechy HelloMacro dla każdego z ich typów, dostarczymy makro proceduralne, aby użytkownicy mogli opatrzyć swój typ adnotacją #[derive(HelloMacro)], aby uzyskać domyślną implementację funkcji hello_macro. Domyślna implementacja wyświetli Hello, Macro! My name is TypeName!, gdzie TypeName to nazwa typu, na którym ta cecha została zdefiniowana. Innymi słowy, napiszemy pakiet, który umożliwi innemu programiście napisanie kodu takiego jak w Listing 20-37 przy użyciu naszego pakietu.

Filename: src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}
Listing 20-37: Kod, który użytkownik naszego pakietu będzie mógł napisać, używając naszego makra proceduralnego

Ten kod wyświetli Hello, Macro! My name is Pancakes! po zakończeniu. Pierwszym krokiem jest stworzenie nowego pakietu bibliotecznego w ten sposób:

$ cargo new hello_macro --lib

Następnie, w Listing 20-38, zdefiniujemy cechę HelloMacro i jej skojarzoną funkcję.

Filename: src/lib.rs
pub trait HelloMacro {
    fn hello_macro();
}
Listing 20-38: Prosta cecha, której będziemy używać z makrem derive

Mamy cechę i jej funkcję. W tym momencie użytkownik naszego pakietu mógłby implementować cechę, aby osiągnąć pożądaną funkcjonalność, jak w Listing 20-39.

Filename: src/main.rs
use hello_macro::HelloMacro;

struct Pancakes;

impl HelloMacro for Pancakes {
    fn hello_macro() {
        println!("Hello, Macro! My name is Pancakes!");
    }
}

fn main() {
    Pancakes::hello_macro();
}
Listing 20-39: Jak by to wyglądało, gdyby użytkownicy napisali ręczną implementację cechy HelloMacro

Jednak musieliby napisać blok implementacji dla każdego typu, którego chcieliby użyć z hello_macro; chcemy ich oszczędzić tego wysiłku.

Dodatkowo, nie możemy jeszcze dostarczyć funkcji hello_macro z domyślną implementacją, która wyświetli nazwę typu, na którym zaimplementowano cechę: Rust nie posiada możliwości refleksji, więc nie może odczytać nazwy typu w czasie wykonania. Potrzebujemy makra do generowania kodu w czasie kompilacji.

Następnym krokiem jest zdefiniowanie makra proceduralnego. W chwili pisania tego tekstu makra proceduralne muszą znajdować się we własnym pakiecie. Ostatecznie to ograniczenie może zostać zniesione. Konwencja strukturyzowania pakietów i pakietów makr jest następująca: Dla pakietu o nazwie foo, pakiet makra proceduralnego derive nazywa się foo_derive. Stwórzmy nowy pakiet o nazwie hello_macro_derive w projekcie hello_macro:

$ cargo new hello_macro_derive --lib

Nasze dwa pakiety są ściśle powiązane, dlatego tworzymy pakiet makra proceduralnego w katalogu naszego pakietu hello_macro. Jeśli zmienimy definicję cechy w hello_macro, będziemy musieli również zmienić implementację makra proceduralnego w hello_macro_derive. Oba pakiety będą musiały być publikowane oddzielnie, a programiści używający tych pakietów będą musieli dodać je oba jako zależności i wprowadzić je oba do zasięgu. Moglibyśmy zamiast tego sprawić, by pakiet hello_macro używał hello_macro_derive jako zależności i ponownie eksportował kod makra proceduralnego. Jednak sposób, w jaki strukturyzowaliśmy projekt, umożliwia programistom używanie hello_macro nawet jeśli nie chcą funkcjonalności derive.

Musimy zadeklarować pakiet hello_macro_derive jako pakiet makra proceduralnego. Będziemy również potrzebować funkcjonalności z pakietów syn i quote, jak zobaczysz za chwilę, więc musimy dodać je jako zależności. Dodaj następujące wiersze do pliku Cargo.toml dla hello_macro_derive:

Filename: hello_macro_derive/Cargo.toml
[lib]
proc-macro = true

[dependencies]
syn = "2.0"
quote = "1.0"

Aby rozpocząć definiowanie makra proceduralnego, umieść kod z Listing 20-40 w pliku src/lib.rs pakietu hello_macro_derive. Zauważ, że ten kod nie skompiluje się, dopóki nie dodamy definicji funkcji impl_hello_macro.

Filename: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate.
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation.
    impl_hello_macro(&ast)
}
Listing 20-40: Kod, który większość pakietów makr proceduralnych będzie wymagać do przetwarzania kodu Rust

Zauważ, że podzieliliśmy kod na funkcję hello_macro_derive, która jest odpowiedzialna za parsowanie TokenStream, oraz funkcję impl_hello_macro, która jest odpowiedzialna za przekształcanie drzewa składni: to sprawia, że pisanie makra proceduralnego jest wygodniejsze. Kod w zewnętrznej funkcji (w tym przypadku hello_macro_derive) będzie taki sam dla prawie każdego pakietu makr proceduralnych, który zobaczysz lub stworzysz. Kod, który określisz w ciele funkcji wewnętrznej (w tym przypadku impl_hello_macro), będzie inny w zależności od celu twojego makra proceduralnego.

Wprowadziliśmy trzy nowe pakiety: proc_macro, syn, oraz quote. Pakiet proc_macro jest dostarczany z Rust, więc nie musieliśmy dodawać go do zależności w Cargo.toml. Pakiet proc_macro to API kompilatora, które pozwala nam czytać i manipulować kodem Rust z naszego kodu.

Pakiet syn parsuje kod Rust ze stringa do struktury danych, na której możemy wykonywać operacje. Pakiet quote przekształca struktury danych syn z powrotem w kod Rust. Te pakiety znacznie upraszczają parsowanie wszelkiego rodzaju kodu Rust, który możemy chcieć obsłużyć: Napisanie pełnego parsera dla kodu Rust nie jest prostym zadaniem.

Funkcja hello_macro_derive zostanie wywołana, gdy użytkownik naszej biblioteki określi #[derive(HelloMacro)] dla typu. Jest to możliwe, ponieważ otagowaliśmy tutaj funkcję hello_macro_derive za pomocą proc_macro_derive i określiliśmy nazwę HelloMacro, która pasuje do nazwy naszej cechy; jest to konwencja, której przestrzega większość makr proceduralnych.

Funkcja hello_macro_derive najpierw konwertuje input z TokenStream na strukturę danych, którą możemy następnie interpretować i na której wykonywać operacje. W tym miejscu do gry wkracza syn. Funkcja parse w syn pobiera TokenStream i zwraca strukturę DeriveInput reprezentującą sparsowany kod Rust. Listing 20-41 pokazuje odpowiednie części struktury DeriveInput, którą otrzymujemy po sparsowaniu ciągu struct Pancakes;.

DeriveInput {
    // --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}
Listing 20-41: Instancja DeriveInput, którą otrzymujemy podczas parsowania kodu posiadającego atrybut makra w Listing 20-37

Pola tej struktury pokazują, że sparsowany kod Rust to struktura jednostkowa z ident (identyfikatorem, czyli nazwą) Pancakes. W tej strukturze jest więcej pól opisujących wszelkiego rodzaju kod Rust; sprawdź dokumentację syn dla DeriveInput po więcej informacji.

Wkrótce zdefiniujemy funkcję impl_hello_macro, w której zbudujemy nowy kod Rust, który chcemy uwzględnić. Ale zanim to zrobimy, zauważ, że wyjście dla naszego makra derive jest również TokenStream. Zwrócony TokenStream jest dodawany do kodu, który piszą użytkownicy naszego pakietu, więc kiedy skompilują swój pakiet, uzyskają dodatkową funkcjonalność, którą zapewniamy w zmodyfikowanym TokenStream.

Być może zauważyłeś, że wywołujemy unwrap, aby spowodować panikę funkcji hello_macro_derive, jeśli wywołanie funkcji syn::parse tutaj zakończy się niepowodzeniem. Konieczne jest, aby nasze makro proceduralne wywołało panikę w przypadku błędów, ponieważ funkcje proc_macro_derive muszą zwracać TokenStream zamiast Result, aby były zgodne z API makr proceduralnych. Uprościliśmy ten przykład, używając unwrap; w kodzie produkcyjnym powinieneś dostarczyć bardziej szczegółowe komunikaty o błędach, używając panic! lub expect.

Teraz, gdy mamy kod do przekształcenia adnotowanego kodu Rust z TokenStream w instancję DeriveInput, wygenerujmy kod, który implementuje cechę HelloMacro na adnotowanym typie, jak pokazano w Listing 20-42.

Filename: hello_macro_derive/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Construct a representation of Rust code as a syntax tree
    // that we can manipulate
    let ast = syn::parse(input).unwrap();

    // Build the trait implementation
    impl_hello_macro(&ast)
}

fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
    let name = &ast.ident;
    let generated = quote! {
        impl HelloMacro for #name {
            fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };
    generated.into()
}
Listing 20-42: Implementacja cechy HelloMacro przy użyciu sparsowanego kodu Rust

Uzyskujemy instancję struktury Ident zawierającą nazwę (identyfikator) adnotowanego typu za pomocą ast.ident. Struktura w Listing 20-41 pokazuje, że gdy uruchomimy funkcję impl_hello_macro na kodzie z Listing 20-37, ident, który otrzymamy, będzie miał pole ident z wartością "Pancakes". W ten sposób zmienna name w Listing 20-42 będzie zawierać instancję struktury Ident, która po wydrukowaniu będzie ciągiem znaków "Pancakes", nazwą struktury z Listing 20-37.

Makro quote! pozwala nam zdefiniować kod Rust, który chcemy zwrócić. Kompilator oczekuje czegoś innego niż bezpośredni wynik wykonania makra quote!, więc musimy przekonwertować go na TokenStream. Robimy to, wywołując metodę into, która konsumuje tę pośrednią reprezentację i zwraca wartość wymaganego typu TokenStream.

Makro quote! zapewnia również bardzo fajne mechanizmy szablonowania: Możemy wpisać #name, a quote! zastąpi to wartością zmiennej name. Możesz nawet wykonywać pewne powtórzenia podobnie do tego, jak działają zwykłe makra. Sprawdź dokumentację pakietu quote dla szczegółowego wprowadzenia.

Chcemy, aby nasze makro proceduralne generowało implementację naszej cechy HelloMacro dla typu, który użytkownik adnotował, co możemy uzyskać za pomocą #name. Implementacja cechy ma jedną funkcję hello_macro, której ciało zawiera funkcjonalność, którą chcemy zapewnić: wyświetlenie Hello, Macro! My name is, a następnie nazwy adnotowanego typu.

Używane tutaj makro stringify! jest wbudowane w Rust. Przyjmuje ono wyrażenie Rust, takie jak 1 + 2, i w czasie kompilacji zamienia to wyrażenie w literał ciągu znaków, taki jak "1 + 2". Różni się to od format! lub println!, które są makrami, które ewaluują wyrażenie, a następnie zamieniają wynik na String. Istnieje możliwość, że wejście #name może być wyrażeniem do wydrukowania dosłownie, dlatego używamy stringify!. Użycie stringify! oszczędza również alokację, konwertując #name na literał ciągu znaków w czasie kompilacji.

W tym momencie cargo build powinno zakończyć się pomyślnie zarówno w hello_macro, jak i hello_macro_derive. Podłączmy te pakiety do kodu w Listing 20-37, aby zobaczyć makro proceduralne w akcji! Utwórz nowy projekt binarny w katalogu projects za pomocą cargo new pancakes. Musimy dodać hello_macro i hello_macro_derive jako zależności w pliku Cargo.toml pakietu pancakes. Jeśli publikujesz swoje wersje hello_macro i hello_macro_derive na crates.io, byłyby to zwykłe zależności; jeśli nie, możesz określić je jako zależności path w następujący sposób:

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

Umieść kod z Listing 20-37 w pliku src/main.rs i uruchom cargo run: powinien wyświetlić Hello, Macro! My name is Pancakes!. Implementacja cechy HelloMacro z makra proceduralnego została włączona bez konieczności implementowania jej przez pakiet pancakes; #[derive(HelloMacro)] dodało implementację cechy.

Następnie zbadamy, jak inne rodzaje makr proceduralnych różnią się od niestandardowych makr derive.

Makra podobne do atrybutów

Makra podobne do atrybutów są podobne do niestandardowych makr derive, ale zamiast generować kod dla atrybutu derive, pozwalają tworzyć nowe atrybuty. Są również bardziej elastyczne: derive działa tylko dla struktur i enumów; atrybuty mogą być stosowane również do innych elementów, takich jak funkcje. Oto przykład użycia makra podobnego do atrybutu. Powiedzmy, że masz atrybut o nazwie route, który adnotuje funkcje podczas używania frameworka aplikacji webowych:

#[route(GET, "/")]
fn index() {

Ten atrybut #[route] byłby zdefiniowany przez framework jako makro proceduralne. Sygnatura funkcji definicji makra wyglądałaby tak:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

Tutaj mamy dwa parametry typu TokenStream. Pierwszy dotyczy zawartości atrybutu: część GET, "/". Drugi to ciało elementu, do którego atrybut jest dołączony: w tym przypadku fn index() {} i reszta ciała funkcji.

Poza tym, makra podobne do atrybutów działają tak samo jak niestandardowe makra derive: Tworzysz pakiet z typem pakietu proc-macro i implementujesz funkcję, która generuje kod, którego potrzebujesz!

Makra podobne do funkcji

Makra podobne do funkcji definiują makra, które wyglądają jak wywołania funkcji. Podobnie jak makra macro_rules!, są bardziej elastyczne niż funkcje; na przykład mogą przyjmować nieznaną liczbę argumentów. Jednak makra macro_rules! mogą być definiowane tylko za pomocą składni podobnej do match, którą omówiliśmy wcześniej w sekcji „Makra deklaratywne do ogólnego metaprogramowania”. Makra podobne do funkcji przyjmują parametr TokenStream, a ich definicja manipuluje tym TokenStream za pomocą kodu Rust, tak jak robią to inne dwa typy makr proceduralnych. Przykładem makra podobnego do funkcji jest makro sql!, które może być wywołane w następujący sposób:

let sql = sql!(SELECT * FROM posts WHERE id=1);

To makro parsowałoby instrukcję SQL wewnątrz i sprawdzałoby, czy jest składniowo poprawna, co jest znacznie bardziej złożonym przetwarzaniem niż to, co może wykonać makro macro_rules!. Makro sql! byłoby zdefiniowane tak:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

Ta definicja jest podobna do sygnatury makra derive: Otrzymujemy tokeny, które znajdują się w nawiasach, i zwracamy kod, który chcieliśmy wygenerować.

Podsumowanie

Uff! Teraz masz w swoim zestawie narzędzi Rust kilka funkcji, których prawdopodobnie nie będziesz często używać, ale będziesz wiedzieć, że są dostępne w bardzo szczególnych okolicznościach. Przedstawiliśmy kilka złożonych tematów, abyś mógł rozpoznać te koncepcje i składnię, gdy napotkasz je w sugestiach komunikatów o błędach lub w cudzym kodzie. Użyj tego rozdziału jako odniesienia, które poprowadzi cię do rozwiązań.

Następnie wprowadzimy w życie wszystko, co omówiliśmy w całej książce, i zrobimy jeszcze jeden projekt!