Package Manager C Language Target Support
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.h が foo ターゲットの公開API、util.h は foo の内部実装用ヘッダ、という位置づけになります。include/ の下にターゲット名と同名のサブディレクトリ(ここでは foo/)を掘る構成が推奨ですが、必須ではありません。既存のCライブラリが /usr/include 直下にヘッダを置く慣習と互換にしたい場合などに備えて、フラット配置も許されます。
SwiftPMがモジュールマップを自動生成するのは、「include直下がフラット」または「include直下にサブディレクトリがひとつだけ」の場合です。それ以上複雑なレイアウトでは、後述のとおり作者が明示的にモジュールマップを用意する必要があります。
モジュールマップの扱い
Cターゲットが include/ を持つとき、SwiftPMは次のように振る舞います。
- 基本は、
include/以下のヘッダを列挙してモジュールマップを自動生成します。列挙順は再現性のため辞書順となりますが、利用者から見たAPIとしては「どのヘッダも単独でincludeできる」ことが期待される、とドキュメントで案内されます。 include/に、ターゲット名と同名のヘッダ(例:foo/foo.hやinclude/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です。
- ターゲットが独自のモジュールマップをより柔軟に提供できるようにする仕組み。