-
[SE-0541] Flexible Swift/C Interoperability for PackagesSwift 2026. 9. 20. 08:30
안녕하세요. 그린입니다 🍏
이번 포스팅에서는 SE-0541 — 패키지를 위한 유연한 Swift/C 상호운용성에 대해 정리해보겠습니다 🙋🏻
Intro
Proposal: SE-0541
Author: Owen Voorhees
Review Manager: Tim Condon
Status: Implemented (Swift Next)
Implementation: swift-package-manager#10292, swift-build#1537, swift-build#1546, swift#90774
Introduction
Swift는 C, C++, Objective-C와의 양방향 상호운용성을 잘 지원하지만, SwiftPM은 그동안 패키지 author가 쓸 수 있는 interop 기능을 제한해습니다.
이 제안은 이런 제약 대부분을 걷어내서, 패키지가 코드를 조직하는 새로운 툴들을 갖게 해줍니다.Motivation
Swift는 C/C++/Objective-C 코드와 상호운용하기 위한 여러 도구를 제공해요.
- Swift 코드는 Clang module로 조직된 코드를 직접 import할 수 있어요.
이게 오늘날 Swift 패키지가 쓰는 주된 interop 메커니즘이에요.
예를 들어 기존 패키지 중 상당수가 SwiftLibrary타겟과, C API를 import하기 위한 Clang module을 정의하는 CLibraryCShims타겟을 함께 갖고 있어요. - Swift module은
@c/@objc어노테이션이 붙은 API를 C 코드에 노출하는 생성된 헤더를 가질 수 있어요.
패키지에서 부분적으로 지원되긴 하지만, 몇 가지 눈에 띄는 제약이 있어요. - Swift module은 같은 이름의 하위 Clang module을 재노출해서, 하나의 import로 통합된 Swift/C API 표면을 제공할 수 있어요.
패키지 타겟은 Swift와 C 계열 소스를 함께 담을 수 없기 때문에 아직 패키지에서는 지원되지 않아요. - Swift module은
@implementation으로 C 헤더의 구현을 제공할 수 있어요.
마찬가지로 패키지 타겟이 Swift/C 소스를 섞는 걸 허용하지 않아서 패키지에서는 잘 지원되지 않아요. - Swift module은 bridging header로 비-모듈러 C 코드를 import할 수 있어요.
이 제안의 목표는, 패키지 저자가 자신의 용례에 가장 잘 맞는 interop 메커니즘을 자유롭게 고를 수 있게 하면서도, 의존하는 패키지가 안전하게 소비할 수 있는 인터페이스를 계속 제공하도록 보장하는 것입니다.
Proposed Solution
이 제안은 패키지가 Swift의 interop 기능을 최대한 활용할 수 있도록 세 가지 변경을 포함해요.
- 기존 제약을 풀어서 패키지가 하나의 타겟 안에서 Swift와 C 소스를 자유롭게 섞을 수 있게 함
- mixed source 타겟을 위한 bridging header를 지정할 수 있는 새 매니페스트 API 추가
- Swift 전용 타겟의 생성된 헤더를 C 타겟의 인터페이스에서 쓸 때 발생할 수 있는 soundness 이슈 수정
Detailed Design
Mixed Source Targets
지금까지 SwiftPM은 타겟에 Swift와 C 계열 소스가 함께 있으면 에러를 냈어요.
이 제안은 이 제약을 없애서 sources 디렉터리가 Swift, C, C++, Objective-C 소스의 어떤 조합이든 담을 수 있게 해줍니다.일반 타겟, executable 타겟, test 타겟, macro 타겟 모두 mixed source 타겟으로 지원돼요.
binary/system library 타겟은 연관된 소스가 없어서 이 변경이 적용되지 않아요.
플러그인 타겟은 빌드 계획 과정의 일부로 해석되고 가벼워야 해서 여전히 Swift 소스만 담도록 제한되지만, 빌드 도구 플러그인이 계획한 태스크의 일부로 실행되는 executable 타겟은 mixed source 타겟일 수 있어요.
Headers and Module Maps
mixed source 타겟의 헤더 조직 규칙은 C 전용 타겟과 대체로 같아요.
기본적으로 타겟 sources 디렉터리의
include하위 디렉터리가 타겟의 public header 경로로 간주돼요.이 경로는 새 타겟을 선언할 때
publicHeadersPath파라미터로 커스터마이즈할 수 있어요.이 제안의 일부로,
testTarget과macro매니페스트 API도 각각 기존target/executableTarget에 있던 것과 동일한publicHeadersPath파라미터를 노출하는 새 오버로드를 추가합니다.SwiftPM은 대부분의 경우 C 전용 타겟에서처럼 mixed source 타겟에 대해서도 module map을 생성해요.
- public headers 디렉터리에
module.modulemap파일이 있으면, 그걸 그대로 쓰고 module map을 생성하지 않아요. - public headers 디렉터리에 module 이름과 같은 basename을 가진 헤더가 있으면, 그 헤더를 umbrella header로 삼아 module map을 생성해요.
- public headers 디렉터리에 module 이름과 같은 이름의 디렉터리가 있고, 그 디렉터리 안에 module 이름과 같은 basename을 가진 헤더가 있으며, 최상위 public headers 디렉터리가 그 외엔 비어 있다면, 그 헤더를 umbrella header로 삼아 module map을 생성해요.
- 그 외의 경우엔 public headers 디렉터리를 umbrella directory로 삼아 module map을 생성해요.
SwiftPM이 module map을 생성할 때는, mixed source 타겟의 Clang module에 Swift가 생성한 헤더를 포함시켜요.
이렇게 하면 하위의 C 계열 타겟이 Swift 소스에 선언된 API를 소비할 수 있어요.
mixed source 타겟의 Swift 소스를 컴파일할 때는, 의존성 순환을 피하기 위해 그 생성된 헤더를 하위 Clang module에서 제외합니다.
전통적으로 mixed source module의 Swift 생성 헤더는, 하위 Clang module이 Darwin 프레임워크 스타일의 include로 접근 가능한, module 이름을 딴 umbrella header를 갖고 있다고 가정해왔어요.
이 가정은 코드를 module로 조직하는 데 더 유연한 패키지에는 항상 들어맞지는 않아요.
이 제안에 따라, mixed source 패키지 타겟의 생성된 헤더는 (필요하다면) public headers 디렉터리 기준 상대 경로의 quoted include를 통해 하위 module의 헤더를 포함하게 되어, 의존하는 쪽이 성공적으로 소비할 수 있도록 보장합니다.
패키지가 커스텀 module map을 제공하는 경우, Swift 생성 헤더는 하위 Clang module에 포함되지 않아요.
SwiftPM은 현재 헤더와 커스텀 module map을 sources 디렉터리에서 직접 참조해요.
이 커스텀 module map을 확장하려면 다음 중 하나가 필요할 거예요.
- 헤더/module map을 설치 위치로 복사하고 그 위치에서 참조하는 방식을 기술하는 메커니즘을 도입하는 것.
이러면 빌드 시스템이 복사되는 시점에 이를 처리하고 생성된 헤더를 주입할 수 있어요. - 의존성 컴파일에 VFS를 주입해서 커스텀 module map 조회를, 생성된 헤더를 주입한 사본으로 리다이렉트하는 것.
Mixed Source Target을 위한 C 계열 언어 설정
기존
publicHeadersPath,cSettings,cxxSettingsAPI로 mixed source 타겟의 C 계열 소스가 어떻게 컴파일되는지 설정할 수 있어요.macro와testTarget타겟 타입은 현재 전체 설정 옵션 집합을 노출하지 않기 때문에, 이 제안은 이 둘에 새 타겟 API를 도입해요.@available(_PackageDescription, introduced: 999) static func macro( name: String, dependencies: [Dependency] = [], path: String? = nil, exclude: [String] = [], sources: [String]? = nil, publicHeadersPath: String? = nil, // New packageAccess: Bool = true, cSettings: [CSetting]? = nil, // New cxxSettings: [CXXSetting]? = nil, // New swiftSettings: [SwiftSetting]? = nil, linkerSettings: [LinkerSetting]? = nil, plugins: [PluginUsage]? = nil ) -> Target @available(_PackageDescription, introduced: 999) public static func testTarget( name: String, dependencies: [Dependency] = [], path: String? = nil, exclude: [String] = [], sources: [String]? = nil, resources: [Resource]? = nil, publicHeadersPath: String? = nil, // New packageAccess: Bool = true, cSettings: [CSetting]? = nil, cxxSettings: [CXXSetting]? = nil, swiftSettings: [SwiftSetting]? = nil, linkerSettings: [LinkerSetting]? = nil, plugins: [PluginUsage]? = nil ) -> Target기존
.interoperabilityModeAPI는 mixed source 타겟의 하위 Clang module이 어떻게 import되는지를 결정합니다.PackagePlugin 변경사항
PackagePluginmodule은 현재 플러그인 저자에게 별개의SwiftSourceModuleTarget과ClangSourceModuleTarget타입을 노출하는데, 둘 다SourceModuleTarget프로토콜을 채택해요.이 제안은
SwiftSourceModuleTarget/ClangSourceModuleTarget을 deprecate하고, 예전엔 구체 타입에서만 노출되던 아래 요구사항들을SourceModuleTarget에 새로 추가해요./// Any custom compilation conditions for the Swift sources of the module. @available(_PackageDescription, introduced: 999.0) var swiftCompilationConditions: [String] { get } /// Any preprocessor definitions for the C-family sources of the module. @available(_PackageDescription, introduced: 999.0) var clangPreprocessorDefinitions: [String] { get } /// Any custom header search paths. @available(_PackageDescription, introduced: 999.0) var headerSearchPaths: [String] { get } /// The directory containing public headers @available(_PackageDescription, introduced: 999.0) var publicHeadersDirectoryURL: URL? { get }이 추가 사항들 덕분에
SourceModuleTarget은 이제 mixed source 타겟의 전체 세부사항을 노출할 수 있게 돼요.플러그인 구현은
SourceModuleTarget을 구체 타입으로 조건부 캐스팅하는 대신, 이 새 프로토콜 요구사항을 직접 쓰는 게 좋습니다.기존 플러그인과의 호환성을 최대화하기 위해, SwiftPM은 계속해서 Swift 전용 타겟엔
SwiftSourceModuleTarget을, C 계열 전용 타겟엔ClangSourceModuleTarget을 구체 타입으로 써요.mixed source 타겟은
SourceModuleTarget을 채택하지만 public API로 노출되지 않는 내부 타입으로 표현됩니다.mixed source 타겟을 아직 고려하지 않은 플러그인이라도, 이 기능을 채택하지 않은 패키지에 적용되는 한 변경 없이 계속 동작해야 해요.
mixed source 타겟을 채택한 패키지에 기존 플러그인을 적용하는 경우에도, 그 플러그인이 deprecated된 구체 타입 중 하나로 임의의 타겟을 캐스팅할 수 있다는 데 의존하지만 않는다면 계속 동작해야 합니다.
이 프로토콜 기반 접근 덕분에,
SourceModuleTarget을 쓰고 이 제안에서 추가된 새 요구사항에 의존하지 않는 한, 플러그인은 자기 tools version을 올리지 않고도 mixed source 타겟을 지원할 수 있어요.Bridging Header Configuration
Bridging header는 Swift 코드가 비-모듈러 C와 상호작용하는 쉬운 방법을 제공해요.
이 제안은 Swift 소스를 가진 일반/executable/test/macro 타겟이 bridging header를 설정할 수 있는 새
SwiftSettingAPI를 도입합니다./// The visibility of a bridging header's imported declarations. @available(_PackageDescription, introduced: 999.0) public enum BridgingHeaderVisibility: String { /// Declarations imported via the bridging header may appear in the target's public API. case \`public\` /// Declarations imported via the bridging header may only be used internally. case \`internal\` } /// Configures a bridging header for the target's Swift sources. /// /// A bridging header allows Swift code to import non-modular C/C++/Objective-C code. /// /// - Parameters: /// - path: The path of the bridging header, relative to the target's sources directory. /// - visibility: Whether declarations imported via the bridging header may appear in the /// target's public API. Library targets may only use \`.internal\` visibility. /// - condition: A condition that restricts the application of the build setting. @available(_PackageDescription, introduced: 999.0) public static func bridgingHeader( _ path: String, visibility: BridgingHeaderVisibility, _ condition: BuildSettingCondition? = nil ) -> SwiftSettingbridging header 경로는 타겟의
Sources디렉터리 기준 상대 경로로 지정해요.타겟의 public headers 디렉터리 안에 있으면 안 되고, 타겟 하나에 bridging header는 하나만 포함될 수 있어요.
bridging header visibility는
.public또는.internal일 수 있고, bridging header를 통해 import된 API가 소비하는 쪽의 public 선언에 나타날 수 있는지를 결정해요.일반(라이브러리) 타겟은 하위 타겟에 일관된 인터페이스를 제공하기 위해
.internalvisibility의 bridging header만 import할 수 있어요.기존
interoperabilityMode설정이 bridging header를 어떻게 해석할지 결정합니다.일반적인 가이드라인으로, 패키지 author는 가능하다면 bridging header 대신 C 코드를 modularize하는 걸 선호해야 해요.
다만 이미 큰 기존 C 코드베이스에 Swift를 처음 도입할 때는 modularization이 상당한 리소스를 요구할 수 있어요.
bridging header는 Swift를 점진적으로 도입하는 걸 돕는 용도로 쓰일 때, 그리고 패키지가 장기간에 걸쳐 완전히 modularize해가는 동안 빠르게 interop을 시작할 수 있게 해줄 때 잘 맞습니다.
Swift 전용 타겟의 생성된 헤더 Modularize하기
현재 SwiftPM은 Swift 전용 타겟의 생성된 헤더를 아우르는 module map을 생성해요.
이걸로 하위 C 계열 타겟이
@c/@objc가 붙은 API를 import할 수 있죠.하지만 이 module map은 현재 하위 C 타겟에만 보여요. 세 개의 타겟을 가진 패키지를 생각해볼게요.
- TargetA는 Swift 전용이고 public
@objc클래스 타입Foo를 노출해요. - TargetB는 Objective-C 전용이고 TargetA에 의존하며,
Foo타입 값을 반환하는 함수bar를 노출해요. - TargetC는 Swift 전용이고 TargetB에 의존하며,
bar호출을 담고 있어요.
이 패키지는 tools-version 6.4에서는 컴파일에 실패할 거예요.
TargetC의 Swift 소스를 컴파일할 때, TargetB의 Clang module을 import해도
Foo의 선언이 보이지 않기 때문이에요.이 제안 아래서는, 다가올 tools-version에서 생성된 module을 아우르는 module map이 모든 하위 컴파일에 보이게 되어, 이 예시가 성공적으로 빌드될 수 있게 됩니다.
예시
Swift Numerics에서 하위 Module 채택하기
swift-numerics는 컴파일러 내장 함수를 감싼
static inlinewrapper를RealModule타겟에 노출하는 게 유일한 목적인_NumericsShims타겟을 갖고 있어요.swift-numerics/ ... Sources/ _NumericsShims/ include/ _NumericsShims.h // Vends wrappers module.modulemap _NumericsShims.c RealModule/ ... Double+Real.swift // imports _NumericsShims and consumes wrappers ... ... ...// ... .target( name: "RealModule", dependencies: ["_NumericsShims"], exclude: excludedFilenames, linkerSettings: [ .linkedLibrary("m", .when(platforms: [.linux, .android])) ] ), // MARK: - Implementation details .target( name: "_NumericsShims", exclude: excludedFilenames ), // ..._NumericsShims의 wrapper들은RealModule라이브러리 타겟의 inlinable 함수 본문 안에 나타나기 때문에, bridging header는 좋은 선택이 아니에요.하지만 이건 두 타겟을 하위 Clang module을 가진 하나의 mixed source 타겟으로 합칠 수 있는 좋은 예시예요.
업데이트된 패키지는 이렇게 생겼습니다.
swift-numerics/ ... Sources/ RealModule/ include/ _NumericsShims.h ... Double+Real.swift ... ... ...// ... .target( name: "RealModule", exclude: excludedFilenames, linkerSettings: [ .linkedLibrary("m", .when(platforms: [.linux, .android])) ] ) // ...업데이트된 패키지에서
RealModule은 의존성이 없는 더 단순하고 자기완결적인 타겟이 되고, Swift 소스는 하위 Clang module에서 암묵적으로 import된 wrapper들을 직접 호출할 수 있어요.swift-format에서 Bridging Header 채택하기
Swift Numerics처럼, swift-format도 C interop을 위해 여러 타겟으로 나뉘어 있어요.
swift-formatexecutable은 Swift 코드에 함수 하나를 노출하는_SwiftFormatInstructionCounterC 타겟에 의존해요.이 인터페이스는 modularize할 수도 있었지만, 여기서는 bridging header 채택의 단순화된 사례를 보여주기 위해 그대로 씁니다. (실제로 bridging header를 채택하는 대부분의 경우는 손쉽게 modularize할 수 없는 훨씬 크고 복잡한 코드베이스에서 일어날 가능성이 높아요).
현재 Swift Format은 이렇게 조직되어 있어요.
swift-format/ Sources/ _SwiftFormatInstructionCounter/ include/ InstructionsExecuted.h module.modulemap src/ InstructionsExecuted.c swift-format/ Subcommands/ PerformanceMeasurement.swift // import _SwiftFormatInstructionCounter ... ... ...// ... .target( name: "_SwiftFormatInstructionCounter", exclude: ["CMakeLists.txt"]), .executableTarget( name: "swift-format", dependencies: [ "_SwiftFormatInstructionCounter", "SwiftFormat", // ... ], exclude: ["CMakeLists.txt"], linkerSettings: swiftformatLinkSettings), // ...bridging header를 채택한 뒤, 업데이트된 패키지는 이렇게 조직됩니다.
swift-format/ Sources/ swift-format/ InstructionsExecuted.c swift-format-bridging-header.h Subcommands/ PerformanceMeasurement.swift ... ... ...// ... .executableTarget( name: "swift-format", dependencies: [ "SwiftFormat", // ... ], exclude: ["CMakeLists.txt"], swiftSettings: [.bridgingHeader("swift-format-bridging-header.h", visibility: .internal)], linkerSettings: swiftformatLinkSettings), // ...마찬가지로 이 재구성은 더 단순하고 유지보수하기 쉬운 패키지로 이어집니다.
Security
이 제안은 패키지가 소스를 조직하고 빌드하는 방식에만 영향을 줘요. 패키지 매니저의 보안에 의미 있는 영향은 없습니다.
Impact on Existing Packages
- 기존 tools version을 쓰는 패키지는 지금처럼 그대로 빌드됩니다.
- 새 tools version으로 업그레이드하면, "Modularizing the Swift Generated Header"에서 설명한 변경 사항이 생성된 헤더의 API를 새로운 의존 대상에 노출시켜서 드물게 소스 호환성 엣지 케이스를 만들 가능성이 있어요.
- "PackagePlugin Changes" 섹션에서 설명한 대로, 새 기능을 채택한 패키지에 적용되려면 기존 패키지 플러그인이 업데이트가 필요할 수 있어요.
- mixed source 타겟이나 bridging header 지원 같은 이 제안의 다른 동작 변경들은 새 tools version 채택을 필요로 합니다.
Conclusion
그동안 SwiftPM 패키지는 하나의 타겟에 Swift와 C 소스를 섞을 수 없어서, C API 하나를 감싸는 것만으로도 별도 shim 타겟을 만들어야 했어요.
이 제안 덕분에 mixed source 타겟, bridging header, 그리고 Swift 생성 헤더의 가시성 확장까지 갖추게 되면서, swift-numerics나 swift-format 예시처럼 불필요하게 쪼개져 있던 타겟 구조를 훨씬 단순하게 정리할 수 있게 됐어요 🙌
References
swift-evolution/proposals/0541-flexible-swift-c-interoperability-for-packages.md at main · swiftlang/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution
github.com
'Swift' 카테고리의 다른 글
[SE-0548] resignRemoteID for remote distributed actor references (1) 2026.09.26 [SE-0544] Mutation and consumption in non-Copyable type deinits (6) 2026.08.31 [SE-0538] Disconnected (0) 2026.08.22 [SE-0537] Section Placement Control for Functions (2) 2026.08.15 [SE-0535] Add CLI for editing global mirrors configuration (0) 2026.08.08 - Swift 코드는 Clang module로 조직된 코드를 직접 import할 수 있어요.