Swift Digest
SE-0075 | Swift Evolution

Adding a Build Configuration Import Test

Proposal
SE-0075
Authors
Erica Sadun
Review Manager
Chris Lattner
Status
Implemented (Swift 4.1)

01 何が問題だったのか

Swiftにはこの提案以前から、プラットフォームやアーキテクチャに応じてコードを切り替えるためのビルド設定がありました。具体的には次のようなものです。

  • os(...): OSX, iOS, watchOS, tvOS, Linux, Windows, FreeBSD などの OS を判定
  • arch(...): x86_64, arm, arm64, i386, powerpc64, powerpc64le などの CPU アーキテクチャを判定
  • swift(>=...): Swift 言語のバージョンを判定

しかし、「特定のモジュール(フレームワーク)が今のターゲットで利用できるかどうか」を直接判定する手段がありませんでした。

OS で代用するとプラットフォーム追加に弱い

モジュールの有無を判定したいときは、これまで OS 判定で代用するしかありませんでした。たとえば、UIKit が使えるかどうかを判定したい場合、次のように OS の組み合わせで書きます。

#if os(iOS) || os(tvOS) || os(watchOS)
    // UIKit が使える前提のコード
#endif

このような包含的(inclusive)な OS 判定は、Swift が新しいプラットフォームに対応するたびに見直しが必要になります。逆に、Linux を除外することで Apple プラットフォーム全般を狙うような排他的(exclusive)な判定は、Windows や FreeBSD のような新しい OS ビルド設定が追加された時点で意図が崩れてしまいます。

// 排他的な OS 判定はもろい
#if !os(Linux)
    // OSX, iOS, watchOS, tvOS のつもりでも
    // Windows, FreeBSD まで含んでしまう
#endif

判定したいのは API の有無

実際に分岐させたいのは「この OS か」ではなく「この API(モジュール)が使えるか」であることがほとんどです。同じ UIKit でも iOS と tvOS の両方で使えますし、SpriteKit や Metal のように、将来的にシミュレータや別のプラットフォームへ対応範囲が広がる可能性のあるモジュールもあります。OS で間接的に判定していると、こうした対応範囲の変化に追従するためにコードの書き換えが必要になります。

また、サードパーティのフレームワークが特定のプラットフォームや OS バージョンでしか提供されないケースもあり、Swift だけでは「存在すれば使い、なければフォールバックする」という切り替えを書く手段がなく、Objective-C 側で動的にロードするといった回避策が取られていました。

結局のところ、問題は「モジュールの利用可否」を直接問い合わせる方法が Swift のビルド設定に用意されていなかったことです。

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

新しいビルド設定 canImport(...) を追加し、指定したモジュールが現在のビルド環境でインポート可能かどうかをコンパイル時に判定できるようにします。C/Clang の __has_include に相当する仕組みを、Swift のモジュール単位で提供するものです。

基本的な使い方

#if canImport(モジュール名) の形で記述します。コンパイル時にそのモジュールがリンク可能であれば、分岐が有効になります。

#if canImport(UIKit)
    import UIKit
    // UIKit を使うコード
#elseif canImport(Cocoa)
    import Cocoa
    // macOS 向けのコード
#else
    // どちらも使えない環境向けのフォールバック
#endif

canImport 自体は モジュールをインポートしません。あくまで「インポートできるかどうか」を調べるだけで、実際に使うには従来どおり import が必要です。これにより、判定の分岐と、実際にそのモジュールの API を使う分岐を別々に書くことができます。

#if canImport(Metal)
    // Metal API を使った解法を提供
#else
    // Metal が使えない環境向けの代替解法
#endif

OS 判定とのちがい

canImport はプラットフォームの判定を目的とした仕組みではなく、あくまで API の利用可否 を判定するためのものです。そのため、次のような利点があります。

  • 条件が API の可用性そのものに紐づくので、対応プラットフォームが増減してもコードの意図が崩れにくい
  • os(iOS) || os(tvOS) || os(watchOS) のような長い条件式を書かずに済み、読みやすく誤りにくい
  • ファースト/サードパーティを問わず、任意のモジュール名を指定できる

モジュール名は固定のリストに縛られておらず、任意の識別子を渡せます。コンパイラはコンパイル時にそのモジュールが当該ターゲットでリンクできるかどうかを調べ、結果に従ってビルド対象のコードを決めます。

使いどころ

canImport は、同じソースを複数プラットフォーム向けに共有したいときに特に役立ちます。たとえば、Apple プラットフォームでは Foundation・UIKit・Cocoa などのフレームワークを使い、Linux ではそれらが使えないためフォールバックを用意する、といったケースです。OS 名を列挙する代わりに、使いたいフレームワーク自体の有無で分岐できるため、Swift が新しいプラットフォームに対応したときにも条件式をそのまま流用しやすくなります。