パッケージにおける柔軟な Swift/C 相互運用
Flexible Swift/C Interoperability for Packages
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Swift は C / C++ / Objective-C との双方向の相互運用を手厚くサポートしています。ところが SwiftPM は歴史的に、パッケージの作者が使える相互運用機能をかなり制限してきました。Swift が提供している相互運用の手段には次のようなものがあります。
- Clang モジュールとして整理された C コードを Swift から直接 import する。これは現在のパッケージで最も一般的な方法で、Swift の
Libraryターゲットと、C API を Clang モジュールとして公開するLibraryCShimsターゲットに分ける構成がよく見られます。 - Swift モジュールが生成ヘッダ(generated header)を通じて
@c/@objcを付けた API を C 側へ公開する。パッケージでも部分的にサポートされていますが、いくつか制限があります。 - Swift モジュールが同名の下層 Clang モジュールを再エクスポートし、1 つの import で Swift と C の API をまとめて提供する。パッケージのターゲットは Swift と C 系のソースを同居させられない ため、これは今のパッケージではできません。
- Swift モジュールが
@implementationで C ヘッダの実装を提供する。これも Swift と C のソースを混在できないため、パッケージではうまく扱えません。 - Swift モジュールが bridging header を使って、モジュール化されていない C コードを import する。これもパッケージではサポートされていません。
このように、パッケージのターゲットが「Swift ソースと C 系ソースを混在できない」という制約が、多くの相互運用機能をパッケージから使えなくしている根本的な原因になっていました。その結果、たとえば数個の static inline ラッパーを公開したいだけでも、専用の C ターゲットを別に切り出して依存させる、といった回りくどい構成を強いられていました。この提案は、そうした制限の大半を取り払い、パッケージの作者が用途に最も合った相互運用の手段を選べるようにすることを目的としています。
02 どのように解決されるのか
この提案は、パッケージが Swift の相互運用機能をフルに活用できるよう、次の 3 つの変更を導入します。
- これまでの制限を撤廃し、1 つのターゲット内で Swift ソースと C 系ソースを自由に混在できるようにする(mixed source target)
- mixed source target に bridging header を指定するためのマニフェストAPIを追加する
- Swift-only ターゲットの生成ヘッダを C ターゲットのインターフェースで使えるようにする soundness の問題を修正する
mixed source target
これまで SwiftPM は、1 つのターゲットに Swift ソースと C 系ソースが両方含まれるとエラーにしていました。この提案はその制限を撤廃し、ソースディレクトリに Swift / C / C++ / Objective-C を任意に組み合わせて置けるようにします。通常・実行可能・テスト・マクロの各ターゲットが mixed source target になれます(バイナリターゲットとシステムライブラリターゲットはソースを持たないため対象外、プラグインターゲットは軽量に保つため Swift のみに制限されます)。
ヘッダの整理ルールは C-only ターゲットとほぼ同じで、既定では sources ディレクトリ内の include サブディレクトリが公開ヘッダのパスになります。これは publicHeadersPath パラメータでカスタマイズできます。SwiftPM は多くの場合、C-only ターゲットと同様に module map を自動生成し、その Clang モジュールに Swift の生成ヘッダを含めます。これにより、下流の C 系ターゲットが Swift ソースで宣言された API を利用できます(一方、mixed source target 自身の Swift ソースをコンパイルするときは、依存の循環を避けるため生成ヘッダを下層 Clang モジュールから除外します)。
なお、パッケージが独自の module map を提供している場合、現時点では生成ヘッダは下層 Clang モジュールに含められません。これは header の設置方法に関わる将来の設計余地を狭めないための、当面の制限です。
macro と testTarget のマニフェストAPIには、C 系ソースの設定に必要な publicHeadersPath / cSettings / cxxSettings を追加するオーバーロードが用意されます。
static func testTarget(
name: String,
// ...
publicHeadersPath: String? = nil, // 追加
cSettings: [CSetting]? = nil,
cxxSettings: [CXXSetting]? = nil,
// ...
) -> Target
例: Swift Numerics の下層モジュール化
swift-numerics の _NumericsShims ターゲットは、コンパイラ組み込みの static inline ラッパーを RealModule に公開することだけが目的の C ターゲットでした。ラッパーは RealModule の inlinable な関数本体に現れるため bridging header は不向きですが、まさに 2 つのターゲットを 1 つの mixed source target にまとめられるケースです。
// 変更前: C ターゲットを分けて依存させる
.target(name: "RealModule", dependencies: ["_NumericsShims"], /* ... */),
.target(name: "_NumericsShims", /* ... */),
// 変更後: RealModule 単体にまとめる(include/_NumericsShims.h を同居)
.target(name: "RealModule", /* ... */)
変更後の RealModule は依存のない自己完結したターゲットになり、Swift ソースから下層 Clang モジュールのラッパーを直接呼べます。
bridging header の設定
bridging header は、モジュール化されていない C と Swift コードを手早く連携させる手段です。この提案は、Swift ソースを持つ通常・実行可能・テスト・マクロの各ターゲットに bridging header を設定する SwiftSetting API を追加します。
public enum BridgingHeaderVisibility: String {
case `public` // bridging header 経由の宣言をターゲットの公開APIに現せる
case `internal` // 内部でのみ使える
}
public static func bridgingHeader(
_ path: String,
visibility: BridgingHeaderVisibility,
_ condition: BuildSettingCondition? = nil
) -> SwiftSetting
パスはターゲットの Sources ディレクトリからの相対で指定し、公開ヘッダディレクトリの中に置くことはできません。1 つのターゲットに bridging header は 1 つだけです。visibility は、bridging header 経由で import した API を利用側の公開宣言に現せるかどうかを決めます。通常(ライブラリ)ターゲットは、下流に一貫したインターフェースを見せるため .internal しか使えません。
// swift-format の例: 専用 C ターゲットを bridging header に置き換える
.executableTarget(
name: "swift-format",
dependencies: ["SwiftFormat" /* ... */],
swiftSettings: [.bridgingHeader("swift-format-bridging-header.h", visibility: .internal)],
linkerSettings: swiftformatLinkSettings)
一般的な指針としては、可能なら bridging header よりも C コードのモジュール化が推奨されます。ただし大規模な既存 C コードベースに Swift を段階的に導入する初期段階では、bridging header がすぐに相互運用を立ち上げる良い手段になります。
Swift-only ターゲットの生成ヘッダのモジュール化
SwiftPM は Swift-only ターゲットの生成ヘッダをカバーする module map を生成し、下流の C ターゲットが @c / @objc API を import できるようにしています。しかし現状、この module map は「直接依存する」C ターゲットからしか見えません。たとえば次の 3 ターゲットを考えます。
- TargetA(Swift-only)は公開
@objcクラスFooを公開する。 - TargetB(Objective-C-only)は TargetA に依存し、
Fooを返す関数barを公開する。 - TargetC(Swift-only)は TargetB に依存し、
barを呼び出す。
これは tools-version 6.4 ではコンパイルに失敗します。TargetC の Swift ソースをコンパイルするとき、TargetB の Clang モジュールを import しても Foo の宣言が見えないためです。この提案では、生成モジュールをカバーする module map を新しい tools-version で すべての下流のコンパイルから見える ようにし、この例がビルドできるようにします。
既存パッケージへの影響
既存の tools version を使うパッケージはこれまでどおりビルドできます。mixed source target や bridging header は新しい tools version の採用が必要です。生成ヘッダのモジュール化により、新しい依存へ API が露出することでソース互換性のエッジケースがまれに生じる可能性があります。また、プラグイン側では PackagePlugin の変更に伴い、mixed source target を扱えるよう更新が必要になる場合があります(PackagePlugin は従来 SwiftSourceModuleTarget / ClangSourceModuleTarget の具体型を公開していましたが、これらを deprecated とし、SourceModuleTarget プロトコルに必要な要件を追加する方向です)。