스위프트는 클로저를 C 및 Objective-C로 컴파일러가 생성한 THUNK 함수와 특정 메모리 레이아웃 변환을 통해 브리지합니다. @convention(c)의 경우, 컴파일러는 클로저가 빈 캡처 목록을 가져야 한다고 요구합니다. 이는 C 함수 포인터가 외부 컨텍스트 매개변수 없이 원시 주소이기 때문에 외부 범위 변수를 참조할 수 없기 때문입니다. @convention(block)의 경우, 컴파일러는 힙에 있는 Objective-C 블록 구조체를 생성하여 isa 포인터, 플래그, 호출 함수 포인터 및 캡처된 변수 레이아웃을 포함시킵니다. 이를 통해 ARC가 블록의 생명주기를 유지 관리할 수 있습니다. 중요한 불변 조건은 @convention(c) 클로저가 더미 포인터를 피하기 위해 힙 할당된 객체에 대한 참조를 캡처하지 않아야 하고, @convention(block) 클로저는 Objective-C 코드에서 블록의 존재 기간 동안 캡처된 참조가 유지되고 있는지 확인해야 한다는 것입니다.
실시간 오디오 처리 라이브러리를 개발하는 동안 팀은 Core Audio의 C API (AURenderCallback)에 콜백 함수를 등록하면서 UIKit의 Objective-C 기반 애니메이션 API에 완료 핸들러를 노출해야 했습니다. 주요 도전 과제는 self와 오디오 버퍼 상태를 캡처한 Swift 클로저를 이러한 외부 함수 인터페이스에 전달하는 것이었으며, 메모리 안전을 위반하거나 참조 주기를 도입하지 않도록 해야 했습니다. 이러한 제약 조건은 오디오 버퍼에 대한 제로 오버헤드 접근을 요구하면서 실시간 오디오 스레드와 메인 UI 스레드 간의 스레드 안전을 유지해야 했습니다.
고려한 한 가지 접근 방식은 C 콜백을 위한 전역 정적 함수와 함께 싱글톤 관리자를 사용하는 것이었습니다. 이 방법은 오디오 유닛 포인터로 키가 지정된 스레드 로컬 사전에서 컨텍스트를 저장했습니다. 캡처 문제는 피했지만, 스레드 안전 복잡성과 테스트하기 어려운 전역 가변 상태를 도입했습니다.
또 다른 접근 방식은 Swift 클로저를 보유하고 C 함수 포인터를 노출하는 Objective-C 래퍼 클래스를 만드는 것이었습니다. 이 구조는 void* 컨텍스트 매개변수를 통해 래퍼를 역참조합니다. 상태 유지형이었지만, 이는 브리징 오버헤드를 추가하고 예상보다 이른 해제를 방지하기 위해 수동으로 유지/해제 호출이 필요했습니다. 수동 메모리 관리는 오디오 유닛 초기화 및 해체와 완벽하게 동기화되지 않을 경우 누수를 초래할 위험이 있었습니다.
선택된 솔루션은 Core Audio 콜백에 대해 @convention(c)를 사용하고 안전하게 비트 변환된 컨텍스트 포인터를 약한 참조를 포함하는 구조체에 전달하였으며, UIKit 완료에 대해서는 @convention(block)를 사용하는 방식이었습니다. 이로써 전역 상태를 제거하고 ARC가 Objective-C 블록을 올바르게 관리하는 것을 보장했습니다. 명시적 메모리 장벽이 오디오 스레드 전환 중 C 컨텍스트 포인터를 보호했습니다.
결과적으로 이는 결정론적 메모리 사용의 제로 오버헤드 C 브리지를 얻었습니다. 시스템은 UI 레이어에서 참조 주기가 발생하지 않았으며, 오디오 처리는 전역 잠금을 사용하지 않고 실시간 성능 제약 조건을 유지했습니다.
스위프트가 언어 수준에서 @convention(c) 클로저에서의 캡처를 금지하는 이유는 무엇인가요?
C 함수 포인터는 암묵적인 컨텍스트나 "userdata" 매개변수의 지원 없이 단순한 메모리 주소로 표현됩니다. 이는 외부 변수를 캡처하는 클로저가 C 코드에서 제공할 수 없는 참조를 저장할 장소가 필요함을 의미합니다. 스위프트는 이러한 제약 조건을 컴파일 시간에 강제하여 개발자가 스택 또는 힙 메모리를 참조하는 클로저를 실수로 생성하는 것을 방지합니다. 이러한 참조는 C 함수 포인터가 스위프트 컨텍스트보다 오래 살아남을 경우 덩어리 포인터가 됩니다.
ARC는 현재 범위를 넘어 저장된 Objective-C 코드에 전달된 @convention(block) 클로저의 생명주기를 어떻게 관리하나요?
스위프트가 클로저를 @convention(block)로 변환할 때 컴파일러는 힙에 할당된 Objective-C 블록 구조체를 방출합니다. 이 구조체는 NSObject 메모리 레이아웃을 따르며, ARC가 블록이 경계를 넘을 때 Block_copy 및 Block_release 작업을 적용할 수 있습니다. Objective-C 코드가 인스턴스 변수에 블록을 저장하는 경우, 스위프트의 ARC 통합은 캡처된 스위프트 참조가 유지되도록 보장합니다. 이러한 참조는 Objective-C 보유자가 블록을 해제할 때 해제되며, 자유 후 사용을 방지하면서 수동 유지 관리를 피합니다.
@convention(c) 함수 유형의 메모리 레이아웃이 표준 스위프트 클로저 참조와 어떻게 다르나요?
표준 스위프트 클로저는 변수를 캡처할 수 있는 참조 카운트 힙 객체 또는 스택에 할당된 컨텍스트 쌍입니다. 반면, @convention(c) 함수 유형은 원시 함수 주소를 나타내는 단일 기계어 단어로 컴파일됩니다. 이 회선에는 관련 메타데이터, 유지 수, 또는 캡처 컨텍스트가 없습니다. 이러한 차이로 인해 표준 스위프트 클로저는 동적으로 배치하고 메모리를 관리할 수 있는 반면, @convention(c) 클로저는 명시적 UnsafeMutableRawPointer 컨텍스트 매개변수를 요구하는 정적 주소입니다.