Swift utrzymuje stabilność ABI dla odpornych struktur, przechowując przesunięcia pól w metadanych uruchomieniowych, zamiast twardo kodować je jako wartości przesunięć w binariach klientów. Kiedy moduł eksportuje strukturę, która nie jest zamrożona, kompilator generuje kod, który uzyskuje dostęp do przechowywanych właściwości przez Tabela Przesunięć Pól umieszczoną w metadanych typu. Ta pośredniość pozwala twórcom bibliotek na dodawanie nowych właściwości przechowywanych w przyszłych wersjach bez unieważniania istniejących binariów skompilowanych przeciwko starszym układom struktur. W przeciwieństwie do tego, struktury @zamrożone wykorzystują bezpośrednie obliczanie przesunięć, co daje szybszy dostęp do pamięci, ale na stałe zamraża układ. Koszt to drobna strata wydajności z powodu dodatkowego ładowania pamięci z tabeli przesunięć w porównaniu do adresowania bezpośredniego.
Wyobraź sobie architektowanie rdzenia SDK Analityki dystrybuowanego jako dynamiczna biblioteka dla setek aplikacji klientów. SDK definiuje strukturę Config z początkowo dwiema polami: apiKey i environment. Sześć miesięcy po wydaniu, wymagania produktowe zmuszają do dodania pól retryPolicy i timeoutInterval do tej struktury.
// W AnalyticsSDK (Moduł A) - Początkowo skompilowane public struct Config { public let apiKey: String public let environment: String // Nowe pola dodane w v2.0 bez @zamrożona: // public let retryPolicy: RetryPolicy }
Gdyby struktura była @zamrożona, ta zmiana spowodowałaby awarię istniejących aplikacji klienckich, ponieważ twardo kodowały rozmiar struktury i przesunięcia pól podczas kompilacji. Rozważyliśmy trzy podejścia, aby rozwiązać ten problem ewolucji. Pierwsze podejście polegało na konwersji struktury na klasę, wykorzystując alokację na stercie i stabilność wskaźników; zachowało to kompatybilność ABI, ale wprowadziło niepożądany narzut liczników referencji i semantyki referencji, które złamałyby gwarancję niezmienności wartości typów. Drugie podejście sugerowało wysłanie równoległej struktury ConfigV2, jednocześnie deprecjonując oryginalną; to utrzymywało kompatybilność, ale zfragmentowało powierzchnię API i zmusiło programistów do migracji w sposób jawny. Trzecie podejście przyjęło odporne struktury, usuwając atrybut @zamrożony, co pozwoliło kompilatorowi na generowanie pośrednich dostępów do pól za pomocą wyszukiwania metadanych.
Wybraliśmy trzecie rozwiązanie, ponieważ równoważyło wydajność i przyszłą elastyczność. Binaries klientów dalej działały bez rekompilacji, ponieważ dynamicznie zapytywały o przesunięcia pól z metadanych SDK w czasie wykonywania. Rezultatem była bezproblemowa ewolucja struktury konfiguracji między wersjami SDK, chociaż udokumentowaliśmy, że często dostępne pola konfiguracyjne powinny być lokalnie buforowane, aby złagodzić dodatkowe koszty pośredniości.
Jak Swift określa rozmiar i wyrównanie odpornych struktur podczas kompilacji kodu klienta, który importuje definiujący moduł?
Podczas kompilacji w oparciu o odporną strukturę, Swift nie może statycznie znać konkretnego rozmiaru ani wyrównania, ponieważ nowe pola mogą zostać dodane później. Zamiast tego kompilator generuje kod, który konsultuje się z Tabela Świadków Wartości (VWT) powiązaną z metadanymi typu w czasie wykonywania. VWT dostarcza funkcje dla rozmiaru, wyrównania, kroku i destrukcji, pozwalając klientowi na alokację odpowiedniej ilości miejsca na stosie lub pamięci na stercie bez wcześniejszej wiedzy o układzie struktury.
Dlaczego przełączanie nad odporną enum wymaga klauzuli @unknown default, a co się dzieje w tle, gdy dodany jest nowy przypadek?
Odporny enum nie ujawnia swojej pełnej listy przypadków modułom importującym, co uniemożliwia wyczerpujące przełączanie bez klauzuli domyślnej. Kiedy autor biblioteki dodaje nowy przypadek, metadane enumu aktualizują się, aby uwzględnić nową wartość tagu. Kod klienta skompilowany z @unknown default może obsługiwać ten nieznany tag w czasie wykonywania, przechodząc do domyślnej sekcji, podczas gdy zamrożone enumy spowodowałyby zatrzymanie przy nierozpoznanych tagach, ponieważ instrukcja przełączania była skompilowana jako tabela skoków bez możliwości zapasowej.
Jaką konkretną optymalizację zapewnia atrybut @inlinable w granicach modułów i dlaczego łamie on odporność?
@inlinable ujawnia ciało funkcji lub metody kompilatorowi importującego modułu, umożliwiając inline’owanie między modułami i eliminację martwego kodu. To łamie odporność, ponieważ kompilator klienta osadza szczegóły implementacji bezpośrednio w klienckim binarium. Jeśli autor biblioteki później zmieni implementację, klient nadal będzie używał starego zakodowanego kodu, co może prowadzić do subtelnych rozbieżności w zachowaniu lub awarii, jeśli zmieniły się wewnętrzne struktury danych.