この記事の要点
- library evolution サポート は、バイナリフレームワークの作者が、過去のバージョンとバイナリ互換性を保ったまま API に追加的な変更を加えられるようにする機能です。Swift 5.1 で、モジュール安定性とともに導入されました。これにより、クライアントを再コンパイルしなくても、古い版に対してビルドされたクライアントが新しい版のフレームワークと動作できるようになります。
- library evolution サポートは デフォルトで無効 です。常に一緒にビルド・配布されるもの(Swift Package Manager のパッケージや、アプリ内部のフレームワークなど)には使うべきではありません。フレームワークがクライアントと別々にビルド・更新される場合にのみ 有効にします。
- 有効化すると、過去のバージョンと互換な変更(resilient な変更)と、そうでない変更が区別されます。どの変更が resilient かを理解しておくことが、安全にフレームワークを進化させる鍵になります。
@frozenや@inlinableといった属性を使うと、特定の宣言について「将来も変化しない」ことをコンパイラに伝え、柔軟性と引き換えに性能を最適化できます。
背景: バイナリフレームワークを安全に進化させる
Swift 5.0 では Apple プラットフォームで ABI 安定性 が達成され、アプリは OS に同梱された Swift ランタイム・標準ライブラリを利用できるようになりました。続く Swift 5.1 では、配布・共有できるバイナリフレームワークを実現するために、バイナリ安定性に関わる 2 つの機能が追加されました。
- モジュール安定性(module stability) は、異なるコンパイラバージョンでビルドされた Swift モジュールを 1 つのアプリ内で一緒に使えるようにします。
- library evolution サポート は、バイナリフレームワークの作者が、過去のバージョンとバイナリ互換性を保ったまま API に追加的な変更を加えられるようにします。
モジュール安定性は通常 library evolution サポートを必要とし、配布用のバイナリフレームワークをビルドするときは両方をまとめて有効にするのが一般的です。
library evolution サポートを有効にすべき場合
library evolution サポートはデフォルトで無効です。常に一緒にビルド・配布されるフレームワーク(Swift Package Manager のパッケージや、アプリ内部のバイナリフレームワークなど)では、有効にすべきではありません。
library evolution サポートを使うべきなのは、フレームワークがクライアントとは別々にビルド・更新される場合だけです。 このとき、古い版のフレームワークに対してビルドされたクライアントは、再コンパイルなしで新しい版のフレームワークとともに実行できます。
このようなフレームワークを出荷する予定があるなら、遅くとも最初のリリースから、できれば開発・テストの早い段階から library evolution を有効にしておきます。理由は次のとおりです。
- library evolution サポートを有効にすると、フレームワークの性能特性が変わります。
enumに対するswitchの exhaustive 性に関して、ソース互換性を壊す言語上の変更が入ります(後述)。- library evolution サポートを有効にすること自体がバイナリ非互換な変更です。library evolution なしでビルドされたフレームワークは、いかなるバイナリ互換性も保証しないためです。
有効化の方法
Xcode
Apple プラットフォーム向けに Xcode で開発する場合は、フレームワークのターゲットに BUILD_LIBRARY_FOR_DISTRIBUTION ビルド設定を指定します。この設定で library evolution とモジュール安定性の両方が有効になります。Debug・Release の両ビルドで設定してください。
コンパイラを直接呼び出す場合
コマンドラインや別のビルドシステムから swiftc を直接呼び出す場合は、-enable-library-evolution と -emit-module-interface フラグを渡します。
$ swiftc Tack.swift Barn.swift Hay.swift \
-module-name Horse \
-emit-module -emit-library -emit-module-interface \
-enable-library-evolution
これにより、module interface ファイル Horse.swiftinterface と、共有ライブラリ libHorse.dylib(macOS)または libHorse.so(Linux)が生成されます。
resilient な変更とは
library evolution は、バイナリ互換性を壊さずに行えるフレームワークの変更を可能にします。新しい版が古い版とソース互換かつバイナリ互換であるとき、その変更を resilient であると言います。
どの変更が resilient かを理解するには、ABI-public な宣言 という概念が必要です。これは別の Swift モジュールから参照できる宣言のことです。
- すべての
public宣言は ABI-public です。 @usableFromInline属性を付けた宣言は、ソース上は public ではないものの ABI-public です。@inlinableなコードからは参照できますが、ソースから直接は参照できません。
ABI-public でない宣言を特に区別したいときは ABI-private と呼びます。@usableFromInline の付かない private / fileprivate / internal の宣言がこれにあたります。
また、@frozen 属性も library evolution に関わります。これは ABI-public な struct や enum のバイナリインターフェースを、より多くの実装詳細を公開する形に変えます。将来 resilient に行える変更の幅を制限する代わりに、柔軟性と引き換えに追加の性能を得られます。
resilient な変更の例
- ABI-private な宣言は、自由に追加・削除・変更できます。バイナリインターフェースの一部になるのは、明示的に ABI-public とされたものだけです。
-
ソースファイル先頭の宣言の並び替えや、同じフレームワーク内の別ソースファイルへの移動ができます。型やエクステンション内のメンバーも並び替えられます。ただし、
@frozenな struct の stored property と@frozenなenumのケースは並び替えられません。たとえば次の
@frozenなenumでは、2 つのケースは並び替えられませんが、2 つのメソッドは並び替えられ、ケースとメソッドの相対的な順序も変えられます。@frozen public enum Shape { // これらのケースは並び替えられない。 // ケース同士の順序はバイナリインターフェースの一部。 case rect(w: Int, h: Int) case circle(radius: Int) // これらのメソッドの宣言順は変えてよい。 // 順序はバイナリインターフェースの一部ではない。 public func area() -> Int {...} public func circumference() -> Int {...} } - ソースファイル先頭への宣言の追加ができます。
@frozenでない限り、class / struct /enum型へのメンバー追加ができます。@frozenな型では stored property やenumのケースは追加できませんが、それ以外のメンバーは制限なく追加できます。- イミュータブルなプロパティをミュータブルにできます。プロパティのバイナリインターフェースは一組のアクセサ関数なので、ミュータブルにすること(setter の追加)は新しい宣言を追加するのと同等です。
-
プロトコルへの新しい要件の追加ができます。ただし、その要件のデフォルト実装がプロトコルエクステンションに定義されている場合に限ります。新しい associated type の追加も、プロトコル自身にデフォルトが指定されていればバイナリ互換です。
public protocol PointLike { var x: Int { get } var y: Int { get } var z: Int { get } } extension PointLike { public var z: Int { 0 } }ここで 1 つ重要な注意があります。associated type や
Self要件を持たないプロトコルは 型 として使えますが、associated type を追加するとこの性質が失われます。上の例で新しい associated type を加えると、バイナリ互換ではあってもソース互換ではなくなります。このため、新しい associated type やSelf要件を追加するのは、もともとそれらを持つプロトコル に限るのが安全です。 - ABI-private な宣言・メンバーの削除ができます(
@frozenな型の stored property・enumケースを除く)。 private/internalの宣言・メンバーをpublicや@usableFromInlineにできます。publicな class やそのメンバーをopenにもできます。- 既存の挙動と互換である限り、
publicな宣言の実装を変更できます。たとえば関数本体をより効率的なアルゴリズムに差し替えたり、観測可能な挙動が同じであれば stored property を computed property に変えたりできます(@frozenな型では不可)。 - class / struct /
enumへの新しいプロトコル適合の追加ができます(@frozenな型でも可)。 - ABI-private なプロトコルへの適合の削除ができます。
- 既存の 2 つの class の間に superclass を挿入できます。たとえば
Widget : Gadgetの間にGizmo : Gadgetを挟んでWidget : Gizmoにする、といった変更です。
より網羅的な一覧は、Swift コンパイラのソースリポジトリにある LibraryEvolution.rst に記載されています。
resilient でない変更の例
次のような変更はバイナリ互換性を壊すため、避ける必要があります。
- ABI-public な宣言の削除。既存のクライアントがソースから、あるいはクライアント側に展開された
@inlinable関数を通じて参照しうるためです。 - ミュータブルな ABI-public プロパティをイミュータブルにすること(ABI-public な setter の削除にあたります)。
@frozenな struct への stored property の追加・削除(その property がprivate/fileprivate/internalであっても不可)。- struct や
enumへの@frozen属性の追加・削除。 - プロトコルが継承するプロトコル一覧の変更。
- 宣言の インターフェース の変更。具体的には、プロパティの型の変更、関数の戻り値型・引数型の変更、引数の追加(デフォルト値があっても不可)・削除、ジェネリックな型・関数の
where句への制約の追加・削除など。 - デフォルト引数式の変更は厳密にはバイナリ互換性を壊しませんが、デフォルト引数式は呼び出し側にインライン展開されるため、再コンパイルされるまで既存のクライアントは古い値を使い続けます。
library evolution を部分的にオプトアウトする
library evolution は、コンパイル済みのクライアントとフレームワークの間に抽象化の層を入れることで、性能と引き換えに柔軟性を得ます。多くの場合は将来の柔軟性を残すのが正しいデフォルトですが、ごく単純で将来も変化しようがないデータ型もあります。たとえば 2 次元グラフィックスの Double 型の x / y を持つ点を表す struct などです。
そうした宣言が将来も変化しないことをコンパイラに伝えると、クライアントがその宣言を扱うコードをより効率的に生成できます。これを担うのが @inlinable と @frozen です。これらの属性は慎重に使うべきものですが、特定の文脈では非常に有用です。
@inlinable 関数
@inlinable 属性は、「現在の関数定義が将来のライブラリバージョンでも正しく動作し続ける」というライブラリ作者からの約束です。この約束により、コンパイラはクライアントコードのビルド時に関数本体を参照できます。名前に反して、インライン化が必ず起きるわけではありません。コンパイラは特殊化したコピーをクライアント内に出力することも、フレームワーク内の元の関数を呼び続けることも選べます。
コンパイラは @inlinable 関数の本体に重要な制約を課します。本体からは ABI-public な宣言(public または @usableFromInline)しか参照できません。@usableFromInline 属性は、inlinable なコードから使うためのヘルパー関数を、public なインターフェースとして直接呼べないようにしつつ定義するために存在します。もし @inlinable 関数が private な関数や型を参照できてしまうと、それらがバイナリインターフェースの一部になり、将来の進化を妨げてしまうためです。
バイナリ互換性の観点では、@usableFromInline 宣言は実質的に public 宣言と同じです。一度公開したら、削除も非互換なインターフェース変更もできません。@inlinable の詳細は SE-0193: Cross-module inlining and specialization を参照してください。
@frozen な struct
@frozen 属性を struct に付けると、その stored property のレイアウトがクライアントに公開されます。@frozen な struct の stored property の追加・削除・並び替えはバイナリ非互換な変更です。柔軟性を失う代わりに、コンパイラはモジュール境界をまたいで一定の最適化を行えます。
@frozen な struct には 2 つの言語上の制約があります。
- stored property 自体は ABI-public でなくてよいものの、その 型 は ABI-public でなければなりません。これにより、ABI-private な struct や
enumがフレームワークのバイナリインターフェースに含まれることはなくなります。 - stored property に初期値式がある場合、その式は
@inlinableであるかのようにコンパイルされます。つまり初期値は他の ABI-public な宣言への参照だけで表現する必要があります。
@frozen が制約するのは stored property の集合だけで、メソッドや computed property の追加・並び替えは自由です。ただし computed property と stored property を相互に変えてはいけません。property wrapper や lazy プロパティは内部的に stored property として実装される点にも注意が必要です。
さらに、struct への @frozen の追加・削除自体がバイナリ非互換な変更です。struct は「最初から frozen」であるか、「ずっと resilient のまま」のどちらかでなければなりません。詳細は SE-0260: Library evolution for stable ABIs を参照してください。
@frozen な enum
enum も @frozen にできます。これはケースを追加・削除・並び替えしないという約束です(ABI-public な enum からのケース削除は、@frozen でなくてもバイナリ互換性を壊す点に注意してください。すべてのケースが ABI-public だからです)。@frozen な enum の追加・削除もバイナリ非互換です。
frozen な enum に対する switch は、すべてのケースを網羅していれば exhaustive とみなされます。一方、non-frozen な enum に対する switch は常に default か @unknown のケースを用意しなければなりません。これが、library evolution サポートを有効にすることで導入される唯一のソース非互換性です。switch の exhaustive 性の挙動は SE-0192: Non-exhaustive enums で詳述されています。
プラットフォームのサポート
Swift コンパイラが異なるコンパイラバージョン間のバイナリ互換性を保証するのは、現時点では Apple プラットフォームのみです。Linux などでは、異なるバージョンのコンパイラでビルドされたアプリとライブラリは、必ずしも正しくリンク・動作するとは限りません。
ただし、安定した module interface と library evolution 自体は Swift がサポートするすべてのプラットフォームで使えます。Apple 以外のプラットフォームでも、すべてのバイナリが同じバージョンのコンパイラでビルドされていれば、クライアントを再コンパイルせずに同じライブラリの複数バージョンを使えます。
Objective-C との相互運用
以下は Apple プラットフォームのみに当てはまります。フレームワークが open な class を定義し、クライアントがそれをサブクラス化する場合、基底クラスへの resilient な変更(stored property の追加や superclass の挿入など)に対応するため、サブクラスは実行時の初期化を必要とします。この初期化は Swift ランタイムが裏側で処理します。
ただし、実行時初期化を要する class が Objective-C ランタイム から見えるのは、十分新しいプラットフォームバージョンで動作している場合に限られます。実用上の帰結として、古いプラットフォームでは NSClassFromString() などに基づく機能がこうした class に対して期待どおり動作しません。フレームワークの class が Objective-C の動的機能と組み合わせて使われないと確信できる場合を除き、フレームワークとクライアントの両方で最小デプロイターゲットを macOS 10.15 / iOS 13.0 / tvOS 13.0 / watchOS 6.0 以降にしておくのが最も安全です。
-enable-testing との関係
-enable-testing フラグでビルドすると、他のモジュールが @testable 属性付きでフレームワークを import できるようになり、internal な宣言が見えるようになります。-enable-library-evolution は -enable-testing と併用でき、テスト用にフレームワークをビルドするときは両方を渡すのが推奨されます。
ただし、その場合に resilient なのは public API への変更に対してのみです。通常どおりフレームワークを import するクライアントはバイナリ互換のままですが、@testable import を使うコード(フレームワーク自身のユニットテストなど)はアクセス制御を回避し、ビルド対象となった特定バージョンの非 resilient な実装詳細に依存します。このため、テストは常にフレームワークと一緒にビルドすべきです。
関連リンク
- ABI 安定性とその先へ — ABI 安定性・モジュール安定性・library evolution の関係を整理した同時期の解説
- ABI 安定性以後の Apple プラットフォームでの Swift の進化 — ABI 安定性に伴う、新機能と OS 要件のトレードオフの解説
- SE-0192: Non-exhaustive enums — non-frozen な
enumのswitchexhaustive 性を定めた Proposal - SE-0193: Cross-module inlining and specialization —
@inlinable/@usableFromInlineを導入した Proposal - SE-0260: Library evolution for stable ABIs —
@frozenを含む library evolution の言語機能を導入した Proposal