programowanieArchitekt aplikacji/biblioteki Rust

Czym są makra w Rust? Rodzaje makr, różnica między proceduralnymi a deklaratywnymi, kiedy lepiej wybrać jedno z nich oraz jakie niebezpieczeństwa mogą wystąpić podczas ich używania?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

W Rust makra pozwalają na generowanie kodu na etapie kompilacji, zapewniając potężne narzędzia do metaprogramowania, zmniejszania boilerplate oraz realizacji DSL. Główne rodzaje makr:

  • Deklaratywne makra (makro-reguły, macro_rules!): definiowane za pomocą składni podobnej do match, działają na zasadzie szablon->zamiana. Najczęstszy typ to macro_rules!.
  • Proceduralne makra: deklarowane jako zewnętrzne funkcje w crate, otrzymują AST (TokenStream) i zwracają zmodyfikowany kod. Podzielone na #[derive], makra atrybutowe (#[some_macro]) i podobne do funkcji (custom_macro!()).

Deklaratywne są prostsze w użyciu dla kodu szablonowego, proceduralne dają większą kontrolę nad składnią i analizą tokenów.

Przykład deklaratywnego makra:

macro_rules! vec_of_strings { ($($x:expr),*) => { { let mut v = Vec::new(); $(v.push($x.to_string());)* v } }; } let v = vec_of_strings!("a", "b"); // => Vec<String>

Przykład makra proceduralnego (przykład derive):

#[derive(Debug, Clone)] struct MyStruct; // samo derive Debug jest zaimplementowane jako makro proceduralne w std.

Pytanie z przymrużeniem oka

Czy makra w Rust mogą generować składniowo niepoprawny kod lub kod z błędami w czasie wykonywania?

Odpowiedź: Tak, makra nie sprawdzają poprawności rozwinięcia w momencie pisania; błędy mogą ujawniać się dopiero po podstawieniu makra przez kompilator. Deklaratywne makra mogą prowadzić do nieoczywistych błędów składniowych. Makra proceduralne mogą generować niepoprawny lub podatny na błędy kod, dlatego krytycznie ważne jest dokładne testowanie ich działania.

Przykład:

macro_rules! make_error { () => { let x = ; // błąd składniowy wystąpi podczas używania makra } }

Przykłady rzeczywistych błędów z powodu nieznajomości szczegółów tematu


Historia

W dużym projekcie w celu redukcji boilerplate użyto macro_rules!, nie pokrywając wszystkich przypadków wzorów. Użytkownik przypadkowo przekazał do makra nieobsługiwane wyrażenie, co doprowadziło do nieczytelnego błędu kompilacji, którego przyczynę trudno było śledzić.


Historia

Podczas przenoszenia makr proceduralnych między crate wystąpiły problemy z niekompatybilnością wersji TokenStream API, przez co IDE zawieszało się, a błąd ujawniał się tylko w kompilacjach no_std.


Historia

Podczas pisania DSL do konfiguracji z makrem proceduralnym zaimplementowano niebezpieczną analizę wejściowych tokenów (bez walidacji typów), co prowadziło do dziwnych błędów w czasie wykonywania, podatności oraz niemożności poprawnego wdrożenia części nowej funkcjonalności.