ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SE-0541] Flexible Swift/C Interoperability for Packages
    Swift 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 메커니즘이에요.
      예를 들어 기존 패키지 중 상당수가 Swift Library 타겟과, C API를 import하기 위한 Clang module을 정의하는 C LibraryCShims 타겟을 함께 갖고 있어요.
    • 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 기능을 최대한 활용할 수 있도록 세 가지 변경을 포함해요.

    1. 기존 제약을 풀어서 패키지가 하나의 타겟 안에서 Swift와 C 소스를 자유롭게 섞을 수 있게 함
    2. mixed source 타겟을 위한 bridging header를 지정할 수 있는 새 매니페스트 API 추가
    3. 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을 확장하려면 다음 중 하나가 필요할 거예요.

    1. 헤더/module map을 설치 위치로 복사하고 그 위치에서 참조하는 방식을 기술하는 메커니즘을 도입하는 것.
      이러면 빌드 시스템이 복사되는 시점에 이를 처리하고 생성된 헤더를 주입할 수 있어요.
    2. 의존성 컴파일에 VFS를 주입해서 커스텀 module map 조회를, 생성된 헤더를 주입한 사본으로 리다이렉트하는 것.

    Mixed Source Target을 위한 C 계열 언어 설정

    기존 publicHeadersPath, cSettings, cxxSettings API로 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

     

    기존 .interoperabilityMode API는 mixed source 타겟의 하위 Clang module이 어떻게 import되는지를 결정합니다.

     

    PackagePlugin 변경사항

    PackagePlugin module은 현재 플러그인 저자에게 별개의 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를 설정할 수 있는 새 SwiftSetting API를 도입합니다.

    /// 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
    ) -> SwiftSetting

     

    bridging header 경로는 타겟의 Sources 디렉터리 기준 상대 경로로 지정해요.

    타겟의 public headers 디렉터리 안에 있으면 안 되고, 타겟 하나에 bridging header는 하나만 포함될 수 있어요.

    bridging header visibility는 .public 또는 .internal일 수 있고, bridging header를 통해 import된 API가 소비하는 쪽의 public 선언에 나타날 수 있는지를 결정해요.

     

    일반(라이브러리) 타겟은 하위 타겟에 일관된 인터페이스를 제공하기 위해 .internal visibility의 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 inline wrapper를 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-format executable은 Swift 코드에 함수 하나를 노출하는 _SwiftFormatInstructionCounter C 타겟에 의존해요.

    이 인터페이스는 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

Designed by Tistory.