Swift łączy closures z C i Objective-C za pomocą funkcji thunk generowanych przez kompilator oraz specyficznych transformacji układu pamięci. Dla @convention(c) kompilator wymaga, aby closure miało pustą listę uchwytów, ponieważ wskaźniki funkcji C są surowymi adresami bez parametrów kontekstowych, co uniemożliwia odniesienie do zmiennych z zewnętrznego zakresu. Dla @convention(block) kompilator generuje strukturę bloku Objective-C na stosie, wyposażoną w wskaźnik isa, flagi, wskaźnik funkcji wywołującej, oraz układ uchwyconych zmiennych, co umożliwia ARC zarządzanie cyklem życia bloku poprzez cykle retain/release. Kluczową zasadą jest to, że closures @convention(c) nie mogą uchwytywać odniesień do obiektów przydzielonych na stercie, aby uniknąć wiszących wskaźników, podczas gdy closures @convention(block) muszą zapewnić, że uchwycone odniesienia są zatrzymywane przez cały czas istnienia bloku w kodzie Objective-C.
Podczas opracowywania biblioteki do przetwarzania dźwięku w czasie rzeczywistym, zespół musiał zarejestrować funkcje callback w API C systemu Core Audio (AURenderCallback), jednocześnie udostępniając obsługę ukończenia dla API animacji opartych na Objective-C z UIKit. Głównym wyzwaniem było przekazanie closures Swift, które uchwyciły self i stan bufora audio do tych interfejsów funkcji obcych, nie naruszając bezpieczeństwa pamięci ani nie wprowadzając cykli utrzymania. Ograniczenia wymagały dostępu do buforów audio bez dodatkowych kosztów przy jednoczesnym zapewnieniu bezpieczeństwa wątków pomiędzy wątkiem audio w czasie rzeczywistym a głównym wątkiem UI.
Jednym z rozważanych podejść było wykorzystanie menedżera singletona z globalnymi funkcjami statycznymi do funkcji callback w C. Metoda ta przechowywała kontekst w słowniku lokalnym dla wątków, z kluczem równym wskaźnikom jednostki audio. Chociaż unikało to problemów z uchwytami, wprowadzało złożoność bezpieczeństwa wątków oraz globalny stan zmienny, który był trudny do przetestowania.
Inne podejście polegało na stworzeniu klas opakowujących Objective-C, aby przechować closures Swift i udostępnić wskaźniki funkcji C, które odwoływały się do opakowania za pomocą parametru kontekstowego void*. Choć stanowe, to dodało narzut mostkowy i wymagało ręcznych wywołań retain/release, aby zapobiec przedwczesnej dezintegracji. Ręczne zarządzanie pamięcią narażało na wycieki, jeśli cykl życia opakowania nie był idealnie zsynchronizowany z inicjalizacją i zakończeniem jednostki audio.
Wybrane rozwiązanie wykorzystało @convention(c) dla callbacków Core Audio, przekazując wyraźny wskaźnik kontekstu unsafeBitCast do struktury zawierającej słabe odniesienia do silnika audio, w połączeniu z @convention(block) dla ukończeń UIKit. To wyeliminowało globalny stan, zapewniając, że ARC poprawnie zarządza blokami Objective-C. Eksplicytne bariery pamięci chroniły wskaźniki kontekstu C podczas przejść między wątkami audio.
Rezultatem był mostek C bez dodatkowych kosztów z deterministycznym użyciem pamięci. System nie wykazywał cykli utrzymania w warstwie UI, a przetwarzanie audio utrzymywało wymagania dotyczące wydajności w czasie rzeczywistym bez globalnych blokad.
Dlaczego Swift zabrania uchwytów w closures @convention(c) na poziomie języka?
Wskaźniki funkcji C są przedstawiane jako proste adresy pamięci bez wsparcia dla kontekstu implicitnego lub parametru "danych użytkownika". Oznacza to, że każda closure uchwytująca zewnętrzne zmienne wymagałaby miejsca do przechowywania tych odniesień, którego C nie może zapewnić. Swift egzekwuje to ograniczenie na etapie kompilacji, aby zapobiec tworzeniu przez programistów closures, które odnoszą się do pamięci stosu lub sterty. Takie odniesienia stałyby się wiszącymi wskaźnikami, gdy wskaźnik funkcji C przekroczyłby czas życia kontekstu Swift.
Jak ARC zarządza cyklem życia closures @convention(block), gdy są przekazywane do kodu Objective-C, który przechowuje je poza bieżącym zakresem?
Gdy Swift konwertuje closure na @convention(block), kompilator generuje strukturę bloku Objective-C przydzieloną na stercie. Ta struktura podąża za układem pamięci NSObject, co pozwala ARC zastosować operacje Block_copy i Block_release podczas przekraczania granicy. Jeśli kod Objective-C przechowuje blok w zmiennej instancyjnej, integracja ARC w Swift zapewnia, że uchwycone odniesienia Swift są zatrzymywane. Te odniesienia są zwalniane, gdy właściciel Objective-C zwalnia blok, zapobiegając użyciu po zwolnieniu, unikając przy tym ręcznego zarządzania zatrzymywaniem.
Czym różni się układ pamięci typu funkcji @convention(c) od standardowego odniesienia do closure w Swift?
Standardowa closure Swift jest obiektem na stercie z licznikiem odwołań lub parą kontekstów przydzielonych na stosie, które mogą uchwytywać zmienne. Z kolei typ funkcji @convention(c) kompiluje się do pojedynczego słowa maszynowego reprezentującego surowy adres funkcji. Nie ma powiązanej metadanych, liczników zatrzymań ani kontekstu uchwytów. Ta różnica oznacza, że podczas gdy standardowe closures Swift mogą dynamicznie wysyłać i zarządzać pamięcią, closures @convention(c) są statycznymi adresami wymagającymi eksplicytnych parametrów kontekstowych UnsafeMutableRawPointer.