Swift Digest
SE-0403 | Swift Evolution

Package Manager Mixed Language Target Support

Proposal
SE-0403
Authors
Nick Cooke
Review Manager
Saleem Abdulrasool
Status
Returned for Revision

01 何が問題だったのか

Swift Package Manager の target は、従来 Swift ソースだけか、SE-0038 で導入された C 系言語(C / Objective-C / C++)ソースだけのいずれかしか持てず、両方を同じ target に混在させること はできませんでした。ソース配置の段階で Swift ファイルと .m.h.c などが同居しているとエラーになり、混在 target を作ることができません。

歴史的経緯や技術的理由で、1 つのライブラリの中に Swift と Objective-C のソースが混ざっていることは珍しくありません。そうしたパッケージを SwiftPM で扱おうとすると、これまでは次のいずれかの回避策を取らざるを得ませんでした。

  • binary target として配布する: バイナリが対応しているプラットフォームでしか使えず、binary dependency は Apple プラットフォームに限定されます。利用者側からソースが見えずデバッグしづらく、リリースごとにバイナリを生成するためのツールも必要です。
  • 言語ごとに target を分割する: たとえば Foo(Swift)と FooObjc(Objective-C)に分け、FooFooObjc に依存するように組みます。この場合、本来は内部実装であってもモジュール境界を越えるために public API として公開せざるを得ず、Package.swift の構造も複雑になります。内部実装を Objective-C から Swift へ段階的に移行したい場合にも、言語の境界が target の境界と一致しているため、ファイル単位でインクリメンタルに置き換えていくことができません。

つまり「同じモジュールの中で Swift と Objective-C/C/C++ を自由に混在させ、内部実装から段階的に Swift へ移行していく」という、Xcode プロジェクトでは当たり前だったことが、SwiftPM では構造的に不可能でした。

02 どのように解決されるのか

SwiftPM の target が、Swift と C / Objective-C / C++ のソースを 同じ target の中に混在させる ことを許容するようにします。パッケージ作者は、target の source ディレクトリに両方のソースを並べて置くだけで混合 target を構成できます。C++ 相互運用のような高度な機能を使う場合は、SwiftSetting.InteroperabilityMode で interoperability mode を指定します。

ビルド時には SwiftPM が内部的にソースを言語別に分け、Swift コンパイラと Clang コンパイラで別々にビルドした上で、公開 API をひとつのモジュールにまとめて クライアントに提供します。クライアントから見ると、混合 target も通常の単一言語 target と同じように 1 つのモジュールとして扱えます。

パッケージの構成例

たとえば次のように、Sources/MixedPackage 以下に Swift ソース・Objective-C/C の実装と内部ヘッダ・リソース・公開ヘッダ(include/)を並べて置けます。

MixedPackage
├── Package.swift
├── Sources
│   └── MixedPackage
│       ├── Jedi.swift          // Swift ソース
│       ├── Lightsaber.swift
│       ├── Sith.m              // 実装と内部ヘッダ
│       ├── SithRegistry.h
│       ├── SithRegistry.m
│       ├── droid_debug.c
│       ├── hello_there.txt     // リソース
│       └── include             // 公開ヘッダ
│           ├── MixedPackage.h
│           ├── Sith.h
│           └── droid_debug.h
└── Tests
    └── MixedPackageTests
        ├── JediTests.swift
        ├── SithTests.m
        ├── ObjcTestConstants.h
        ├── ObjcTestConstants.m
        └── SwiftTestConstants.swift

このような混合 target で、次のことができるようになります。

  • 混在したソース全体の公開 API を、ひとつのモジュールとしてエクスポートする
  • target の Swift ソースで定義された Objective-C 互換の API を、同じ target の C / Objective-C / C++ ソースから利用する
  • target の C / Objective-C / C++ ソースで定義された Swift 互換の API を、同じ target の Swift ソースから利用する
  • target のリソースを Swift と Objective-C の両方のコンテキストから参照する

混合 target のインポート

クライアントから混合 target をインポートする方法は、インポート側の言語によって変わります。

Swift 側からはこれまでどおり import するだけです。

// MyClientTarget.swift

import MixedPackage

テストターゲットからは @testable import MixedPackage も可能で、モジュール内の internal な Swift 型が見えるようになります。ただし非公開の C 言語型が公開されるわけではありません。

C / Objective-C / C++ 側からは、Clang module がサポートされている環境であればモジュールインポートが使え、テキストインクルードも併用できます。混合 target の公開ヘッダディレクトリ(例: include/)は、Clang 専用 target の場合と同じようにクライアントのヘッダ検索パスに追加されます。加えて、Swift 側で書いた Objective-C 互換 API を宣言した自動生成ヘッダ MixedPackage-Swift.h も、ここから取り込めるようになっています。

// MyClientTarget.m

// モジュールインポートをサポートする環境であれば、自動生成された Swift
// ヘッダの内容も含めた公開 API をモジュールとしてインポートできます。
@import MixedPackage;
// OldCar.h で定義された型をインポート。
#import "OldCar.h"
// MixedPackage の Swift ソースで定義された Objective-C 互換の型を
// インポート。
#import "MixedPackage-Swift.h"

制限事項

初期サポートでは、いくつか制限があります。

  • 混合 target にできるのは ライブラリ target とテスト target のみ です。実行可能 target などその他の種類については、ユースケースが明確になってから追加を検討する位置づけになっています。
  • カスタム module map を含める場合、その中に $(ModuleName).Swift という名前の submodule を含めてはいけません。SwiftPM が自動生成する Swift 相互運用ヘッダを、この名前の submodule として公開する module map を内部で合成するためです。

ツールバージョンがこの機能に対応していないのに混合 target をビルドしようとした場合や、ライブラリ/テスト以外の target を混合にしようとした場合、カスタム module map に .Swift submodule が含まれている場合には、それぞれ専用のエラーメッセージが表示されます。

plugin からの扱い

SwiftPM plugin からも混合 target を扱えるよう、PackagePlugin モジュールに MixedSourceModuleTarget 型が追加されます。これは既存の SwiftSourceModuleTargetClangSourceModuleTarget が持っていたプロパティを合わせ持つ SourceModuleTarget で、Swift 側のコンパイル条件(swiftCompilationConditions)と Clang 側のプリプロセッサ定義(clangPreprocessorDefinitions)、ヘッダ検索パス、公開ヘッダディレクトリなどを個別に取得できます。

テスト target での使い方

ライブラリ target と同じく、テスト target でも Swift と Objective-C のソースを混在できます。たとえば Tests/MixedPackageTests 以下に JediTests.swiftSithTests.m を同居させ、両者から共有するテストユーティリティを ObjcTestConstants.h / ObjcTestConstants.mSwiftTestConstants.swift に分けて持たせる、といった構成が取れます。Objective-C テストから Objective-C のユーティリティはヘッダをインポートして使え、Swift テストから Swift のユーティリティはそのまま使えます。

既存パッケージへの影響

混合 target のビルド経路は、既存の Swift 専用 target / C 系専用 target とは別の経路として実装されます。そのため、既存パッケージの挙動が変わることはありません。また、この機能はツールバージョンのマイナーアップデートでゲートされるため、古いツールチェインで混合 target をビルドしようとした場合は従来どおりエラーになります。

今後の見通し

将来的な方向性として、次のような拡張が挙げられています(speculative で、実現が約束されているものではありません)。

  • 非公開のヘッダを、同じ target の Swift 実装にだけ公開できるようにする
  • ライブラリとテスト以外、たとえば実行可能 target にも混合 target を拡張する
  • 最終的には ClangTarget / SwiftTarget / MixedTarget といった言語別の型を一本化し、すべての target をデフォルトで混合 target として扱う 設計に寄せていく

ステータスについて

この提案はレビューの結果 Returned for Revision となっており、現時点で Swift Package Manager には取り込まれていません。コンセプトと全体像は上記のとおりですが、最終的な仕様は再レビューで調整される可能性があります。