Interfejs Any został wprowadzony na wczesnym etapie rozwoju Rusta, aby zapewnić możliwości typowania dynamicznego, głównie w scenariuszach obsługi błędów i debugowania, gdzie informacje typowe w czasie kompilacji są niedostępne. Jego projekt odzwierciedla podobne koncepcje w innych językach, takich jak typeid w C++ lub instanceof w Javie, ale model własności Rusta narzuca unikalne ograniczenia. Wymóg 'static wyłonił się z potrzeby zapewnienia, że referencje bez typów nigdy nie przeżywają danych, które opisują, co zapobiega błędom użycia po zwolnieniu pamięci w języku bez zbierania śmieci.
Bez ograniczenia 'static, typ usunięty jako Any mógłby zawierać referencje do danych lokalnych na stosie o ograniczonej żywotności. Jeżeli obiekt typu Any przeżyłby ten kontekst stosu, downcasting i dereferencja uzyskałyby dostęp do zwolnionej pamięci. Ponieważ Any działa poprzez tablice wirtualne i usunięcie typów, kompilator nie może weryfikować żywotności w momencie downcastingu; wymóg 'static służy jako konserwatywna gwarancja, że typ posiada wszystkie swoje dane lub trzyma jedynie statyczne referencje, zapewniając bezpieczeństwo pamięci w obrębie granicy usunięcia.
Definicja interfejsu Any trait Any: 'static wykorzystuje system ograniczeń typów Rusta, aby wymusić to ograniczenie w czasie kompilacji. Tylko typy nie zawierające żadnych referencji nie-statystycznych mogą implementować Any, co gwarantuje, że jakiekolwiek &dyn Any lub Box<dyn Any> pozostaje ważne przez cały czas trwania programu. Umożliwia to bezpieczny downcasting za pomocą downcast_ref() i downcast_mut(), ponieważ dane źródłowe są gwarantowane, że nie zostaną unieważnione przez zakończenie zakresu.
Budowaliśmy system wtyczek dla silnika gier, w którym skrypty mogły rejestrować obsługujące zdarzenia, zwracające dowolne dane do rdzenia silnika. Silnik musiał przechowywać te wartości zwracane w heterogenicznej kolejce do późniejszego przetwarzania przez różne podsystemy, co wymagało usunięcia typów, aby przechować różne typy w jednej kolekcji. Jednak niektóre powiązania skryptowe próbowały zwrócić referencje do tymczasowych zmiennych lokalnych w kontekście wykonywania skryptu, które stałyby się wiszące, gdy tylko zakończył się kontekst skryptu.
Rozwiązanie 1: Własny interfejs z parametrami żywotności
Jednym z podejść było stworzenie własnego interfejsu PluginResult z powiązanym typem dla parametrów żywotności, pozwalając silnikowi śledzić żywotności poprzez obiekt interfejsu. To obiecywało elastyczność, pozwalając na pożyczone dane, ale wymagało skomplikowanych adnotacji żywotności w całej powierzchni API wtyczek. Złożoność zmusiłaby każdego autora wtyczek do zrozumienia zaawansowanych mechanik żywotności w Rust, co tworzyłoby nieakceptowalnie strome krzywe uczenia się i zwiększało ryzyko subtelnych błędów żywotności w kodzie osób trzecich.
Rozwiązanie 2: Niebezpieczna transmutacja żywotności
Inne rozwiązanie zaproponowało użycie kodu unsafe, aby transmutować żywotności poza gdy przechowywano dane, obiecując zasadniczo, że silnik zrzuci wszystkie referencje przed zakończeniem zakresu źródłowego. Choć to pozwoliło na pożądaną ergonomię API, nałożyło ciężar bezpieczeństwa pamięci całkowicie na programistów silnika. Każdy błąd w śledzeniu pochodzenia referencji prowadziłby do wykorzystania luk użycia po zwolnieniu pamięci, naruszając gwarancje bezpieczeństwa Rusta i utrudniając audyt bazy kodu.
Postanowiliśmy wymagać, aby wszystkie wartości zwracane z wtyczek implementowały Any z ograniczeniem 'static, zmuszając autorów skryptów do zwracania danych właścicielskich lub stanu współdzielonego owiniętego w Arc. Ta decyzja poświęciła pewne teoretyczne korzyści wydajności zerokopijnej dla gwarancji, że kolejka zdarzeń silnika może bezpiecznie przechowywać i przetwarzać dane asynchronicznie. Wynik był solidnym API wtyczek bez żadnego kodu unsafe w publicznym interfejsie, choć wymagało to dodania warstw serializacji dla typów, które wcześniej polegały na tymczasowych pożyczonych danych.
Dlaczego Any wymaga 'static, a nie tylko żywotności referencji użytej do utworzenia obiektu interfejsu?
Interfejs Any usuwa informacje o typie w czasie kompilacji, aby wyprodukować tablicę wirtualną, tracąc wszystkie dane o żywotności w tym procesie. Kiedy tworzysz &dyn Any, kompilator nie może zakodować oryginalnej żywotności 'a w obiekcie interfejsu w sposób, który mechanizm downcastingu może później zweryfikować. Wymaganie 'static jest jedynym sposobem, aby upewnić się, że podstawowy typ nie zawiera żadnych wiszących wskaźników bez śledzenia żywotności w czasie wykonania. Gdyby Any akceptowało krótsze żywotności, wskaźnik tablicy wirtualnej musiałby przenosić metadane żywotności, co wymagałoby od Rusta wdrożenia typów zależnych lub sprawdzania pożyczek w czasie wykonywania, co fundamentalnie zmienia model zerokosztowej abstrakcji języka.
Jak Box<dyn Any> interaguje z ograniczeniem 'static, gdy oryginalny typ zawiera referencje nie-statystyczne?
Typ taki jak struct Wrapper<'a>(&'a str) nie może implementować Any, ponieważ nie spełnia ograniczenia interfejsu 'static. W związku z tym nie możesz utworzyć Box<dyn Any> z instancji Wrapper<'a>. Kandydaci często mylnie wierzą, że umieszczanie wartości w pudełku wydłuża jej żywotność; jednak Box tylko posiada alokację na stercie, a nie dane odniesione przez pola w tej alokacji. Jeśli odniesione dane są lokalne na stosie, przeniesienie zewnętrznej struktury na stertę nie wydłuża żywotności referencji, więc kompilator poprawnie odrzuca konwersję na Box<dyn Any>. To zapobiega scenariuszowi, w którym pudełko alokowane na stercie przeżywa kontekst stosu zawierający odniesione dane.
Czy można bezpiecznie zaimplementować własny interfejs Any, który luzuje wymóg 'static używając kodu unsafe i ręcznego śledzenia żywotności?
Choć technicznie możliwe z użyciem unsafe do transmutacji żywotności i własnych tablic wirtualnych, taka implementacja byłaby niespójna, ponieważ system interfejsów Rusta i sprawdzacz pożyczek nie mogą zweryfikować inwariantów żywotności w miejscu downcastingu. Musiałbyś wdrożyć równoległy system typów śledzący żywotności w czasie wykonywania, sprawdzając przy każdym dostępie, że oryginalny zakres wciąż istnieje. To podejście zasadniczo wdraża zbieracz śmieci lub system liczenia referencji, tracąc gwarancje kompilacji Rusta. Co więcej, każda implementacja unsafe oddziaływałaby niespójnie z komponentami standardowej biblioteki, które oczekują inwariantów Any, co prowadziłoby do nieokreślonego zachowania, gdyby mieszano je z obiektami interfejsu std::any::Any.