Swift Digest
SE-0038 | Swift Evolution

Package Manager C Language Target Support

Proposal
SE-0038
Authors
Daniel Dunbar
Review Manager
Rick Ballard
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift Package Manager(SwiftPM)は、この提案以前はSwiftソースからなるターゲットのみをサポートしていました。しかし、実際のSwiftプロジェクトでは、次のような理由からC系の言語(C / C++ / Objective-C / Objective-C++。以下まとめて「C」と呼びます)に「落ちる」必要が出てくる場面がしばしばあります。

  • Swiftへのブリッジが不十分、または品質が低いAPIに触りたい
  • 低レベルの処理をC側で書いたほうが自然
  • すでにあるC実装をSwiftプロジェクトに取り込みたい

Swiftは本来、Clang Modulesを通じてCとの相互運用性が高い言語です。にもかかわらず、SwiftPMのパッケージの中にCで書かれたターゲットを同居させ、同じパッケージ内のSwiftターゲットから直接利用する、ということができませんでした。C部分を使いたい場合は外部のビルドシステムに頼るか、パッケージ化を諦めるしかなく、「Swift主体のプロジェクトに少しだけCを混ぜたい」という素直な要求にSwiftPMが応えられていない状態でした。

この隙間を埋め、SwiftPMのパッケージの一級の構成要素としてCターゲットを扱えるようにすることが求められていました。

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

SwiftPMの規約ベースのターゲット解決を拡張して、Cソースだけで構成されたターゲット を定義できるようにします。この提案の範囲では、ひとつのターゲット内にCソースとSwiftソースを混在させることは扱いません(混在は別途のスコープです)。

Cターゲットの認識ルール

既存のターゲットディレクトリに対して、次のような規約が追加されます。

  • ターゲットディレクトリ内にCソースファイル(拡張子で判別。Objective-C / Objective-C++ の拡張子も対象)が含まれていれば、そのターゲットはCターゲットとして扱われます。Cターゲットの場合、配下のソースファイルはすべてC系の拡張子でなければなりません(Swiftとの混在不可)。
  • ターゲット内に main.c / main.cpp などの名前のソースがあれば実行ファイル、なければライブラリとして扱われます。これはSwiftターゲットと同じ考え方です。

公開ヘッダを置く include ディレクトリ

Cターゲットは、オプションで Includes または include という名前のサブディレクトリ(どちらか一方のみ)を持てます。このディレクトリに置かれたヘッダが、そのCターゲットの「公開API」 として扱われ、同一パッケージ内の別ターゲット(Swiftを含む)から参照できるようになります。

たとえば、Cライブラリ foo とSwiftターゲット bar を同居させたパッケージは、次のような構成になります。

example/src/foo/include/foo/foo.h
example/src/foo/foo.c
example/src/foo/util.h
example/src/bar/bar.swift

この場合、include/foo/foo.hfoo ターゲットの公開API、util.hfoo の内部実装用ヘッダ、という位置づけになります。include/ の下にターゲット名と同名のサブディレクトリ(ここでは foo/)を掘る構成が推奨ですが、必須ではありません。既存のCライブラリが /usr/include 直下にヘッダを置く慣習と互換にしたい場合などに備えて、フラット配置も許されます。

SwiftPMがモジュールマップを自動生成するのは、「include直下がフラット」または「include直下にサブディレクトリがひとつだけ」の場合です。それ以上複雑なレイアウトでは、後述のとおり作者が明示的にモジュールマップを用意する必要があります。

モジュールマップの扱い

Cターゲットが include/ を持つとき、SwiftPMは次のように振る舞います。

  • 基本は、include/ 以下のヘッダを列挙してモジュールマップを自動生成します。列挙順は再現性のため辞書順となりますが、利用者から見たAPIとしては「どのヘッダも単独でincludeできる」ことが期待される、とドキュメントで案内されます。
  • include/ に、ターゲット名と同名のヘッダ(例: foo/foo.hinclude/foo.h)があれば、それがアンブレラヘッダとして扱われ、モジュールマップ生成に利用されます。
  • include/module.modulemap が置かれていれば、それをそのまま使い、自動生成は行いません。複雑なレイアウトや、ターゲット作者が細かくモジュール構造を制御したい場合の逃げ道です。

ビルド時のヘッダ検索パスと #include 表記

SwiftPMは、Cターゲットに依存する(推移的に含む)他のターゲットをビルドする際、そのCターゲットの include/ を自動的にヘッダ検索パスに追加します。さらに、同じパッケージ内のターゲットと、外部パッケージのターゲットとで、渡すフラグが使い分けられます。

  • 同一パッケージ内のCターゲットのヘッダには -iquote を使います。利用側は #include "foo/foo.h" の形で書くのが自然です。
  • 外部パッケージに由来するCターゲットのヘッダには -I を使います。利用側は #include <foo/foo.h> の形で書くのが自然です。

この使い分けにより、「自分のパッケージの一部」として参照するヘッダと、「外部ライブラリ」として参照するヘッダを #include の書き方で区別できるようになっています。

Swiftターゲットとの連携

Swiftターゲットをビルドするときは、依存クロージャに含まれる各Cターゲットのモジュールマップを -fmodule-map-file=<PATH> でClangに明示的に渡します。こうすることで、Swift側のClangインポータがヘッダ検索に頼らず、目的のCモジュールを直接解決できます。結果として、SwiftコードからCターゲットのモジュールを import して、そこで定義された関数・型を通常のSwift APIのように呼び出せます。

既存の「系モジュール」パッケージとの棲み分け

SwiftPMには、システムにすでにインストールされたC系ライブラリをパッケージとしてラップする「系モジュール(system module)」の仕組みが別途存在します。本提案のCターゲットは、そのような既存ライブラリのラップではなく、Swiftプロジェクトのためにこれから書くCコードをパッケージの内側に持ち込む ことを主な用途としたものです。既存のCプロジェクト一般をそのまま取り込めるようにするものではなく、クリーンな規約に沿って書かれたCターゲットを想定しています。

非モジュラーヘッダのリスク

Cターゲットが増えていくと、複数のCターゲットが共通のシステムヘッダ(モジュールマップの用意されていないヘッダ)に間接的に依存し、それがSwift側から同時にインポートされる場面が出てきます。Clangのモジュール実装の都合で、このとき一見動いてしまうケースと壊れるケースが混在し、しかもコンパイラが明確に診断してくれません。とくにLinux環境では、システムヘッダにモジュールマップが整備されていないことが多く、このリスクが顕在化しやすい点が提案中で注意喚起されています。抜本的な対策は「使われているヘッダ側にモジュールマップを増やしていく」ことであり、本提案はその作業の優先度を上げる契機になるだろう、と位置づけられています。

今回のスコープに含まれないこと(Future Directions)

この提案は「最初の一歩」と明確に位置づけられており、次のようなテーマは後続の提案に委ねられるとされています。あくまで方向性の話で、具体的な仕様を約束するものではありません。

  • パッケージ外に公開するターゲットを選別する仕組み(例: SwiftターゲットだけをクライアントAPIとして見せ、Cターゲットは実装詳細として隠す)。
  • Cコンパイラに渡すフラグをパッケージ側で制御する仕組み。初期実装ではデバッグ/リリースの固定フラグ集合のみが使われます。
  • Clang以外のGCC互換Cコンパイラのより広範なサポート。設計上は想定されているものの、当面の実装の主眼はClangです。
  • ターゲットが独自のモジュールマップをより柔軟に提供できるようにする仕組み。