Swift Digest
SE-0339 | Swift Evolution

Module Aliasing For Disambiguation

Proposal
SE-0339
Authors
Ellie Shin
Review Manager
John McCall
Status
Implemented (Swift 5.7)

01 何が問題だったのか

Swiftでは、プログラムに含まれる複数のモジュールが同じ名前を持つことが許されていません。そのため、独立に開発された複数のパッケージを組み合わせて使おうとするとき、モジュール名が衝突してビルドが通らなくなることがあります。

この問題は、Swiftパッケージのエコシステムが拡大するにつれて現実的によく起きるようになっており、代表的には次の2つのケースがあります。

  • 異なるパッケージが、たまたま同じ名前のモジュールを持っている。特に Utils のような「パッケージ内部の共通ユーティリティ」として使われがちな名前で衝突しやすく、新しい依存を追加したりパッケージを更新したりしたときに突然問題が顕在化します。
  • 同じパッケージの異なるバージョンを、同じプログラム内で同時に使う必要がある。あるライブラリが特定バージョンに pin している依存を、別の場所ではもう少し新しいバージョンに上げたい、といった段階的な更新のときにぶつかります。

いずれのケースでも、衝突するパッケージ側に手を入れずに解決できることが大切です。たとえば将来的にサブモジュールの機能が導入されたとしても、既存のパッケージ側がそれを正しく採用してくれているとは限らず、利用側が衝突のたびにパッケージを書き換えるのは現実的ではありません。また、サブモジュールのような言語機能が入ったとしても、あとから発生した名前衝突を「retroactive に」解きほぐすための仕組みは別途必要になります。

次の構成が典型例です。App が依存している Gameswift-game パッケージ)は内部的に Utils モジュールを使っており、さらに App 自身も別パッケージ swift-drawUtils モジュールを使っています。

App
  |— Module Game (from package 'swift-game')
      |— Module Utils (from package 'swift-game')
  |— Module Utils (from package 'swift-draw')

この状態ではモジュール名 Utils が衝突し、どちらかのモジュールをリネームする必要があります。しかし、swift-gameswift-draw のどちらのパッケージにも、利用側からソースコードを書き換えるような介入はしたくありません。既存のソースコードを触らずに、利用側のビルド設定だけで衝突を解消できる仕組みが求められていました。

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

本Proposalは、衝突するモジュールに別名(エイリアス)を与え、ビルド時にその別名でコンパイルすることで、ソースコードに手を入れずに名前衝突を解消する仕組みを導入します。SwiftPM 側の設定と、それを実現するコンパイラフラグの両方から構成されます。

SwiftPM での moduleAliases

利用側の Package.swift で、依存プロダクトに moduleAliases を指定します。先ほどの例であれば、swift-game 側の UtilsGameUtils にリネームし、swift-draw 側の Utils はそのまま使うことで衝突を解消できます。

targets: [
  .executableTarget(
    name: "App",
    dependencies: [
      .product(name: "Game", package: "swift-game", moduleAliases: ["Utils": "GameUtils"]),
      .product(name: "Utils", package: "swift-draw"),
    ])
]

こうすると、swift-game 側の UtilsGameUtils としてビルドされ、Game モジュール内の import UtilsUtils.Level といった参照も自動的に GameUtils のものへ読み替えられます。GameUtils のソースコードを書き換える必要はありません。

App 自身のコードで両方の Utils モジュールを参照したい場合は、リネーム後の名前を直接 import します。

// App
import GameUtils
import Utils

一方、リネーム済みモジュールの binary name(GameUtils)を import 文以外の場所でソースコードから直接参照することは基本的に禁止されます。ソースコード上では元の名前(Utils)を使い続け、あとから再度リネームしたくなったときも柔軟に対応できるようにする、という方針です。binary name を直接書いた場合はエラーと fix-it で元の名前に誘導されます。

SwiftPM は moduleAliases を解析するときに、別名のユニーク性・衝突の有無・対象モジュールがソースからビルドされる pure Swift モジュールであること(事前ビルド済みバイナリはリネームできないため)などを検証します。また、上流パッケージが独自に別名を定義していた場合、利用側(下流)で指定した別名で上書きされます。

-module-alias コンパイラフラグ

内部的には、SwiftPM の設定は次の2つのコンパイラオプションの組み合わせに変換されます。

  • -module-name GameUtils: もともと Utils だったモジュールを、GameUtils という名前のモジュールとしてビルドする。出力先(-o-emit-module-path など)のファイル名も GameUtils.swiftmodule に合わせます。
  • -module-alias Utils=GameUtils: ソースコード中に現れる Utils という名前を、GameUtils への参照として扱う。

この2つを合わせれば、Utils という名前で書かれたモジュールを、事実上 GameUtils という名前のモジュールとしてコンパイルできます。

上の例に当てはめると、各モジュールのビルドは次のようになります。

  • swift-game 側の Utils を、swiftc -module-name GameUtils -emit-module-path /path/to/GameUtils.swiftmodule -module-alias Utils=GameUtils ... でビルドする。モジュール本体の名前は GameUtils になり、自分自身のソース内に残っている Utils という参照も GameUtils として解決されます。
  • Game モジュールは、swiftc -module-name Game -module-alias Utils=GameUtils ... でビルドする。ソース中の import UtilsGameUtils.swiftmodule を読み込むように解決され、型参照もすべて GameUtils のものになります。
  • App については特別なフラグは不要です。App のソース中の import Utilsswift-draw の(リネームされていない)Utils を指し、swift-game 側を使いたい場合だけ import GameUtils と書きます。

-module-alias の引数は、予約語・不正な識別子・左右の書き順(Utils=GameUtils は正しく、GameUtils=Utils は誤り)・重複などがチェックされます。フラグは複数回指定でき、-module-alias Utils=GameUtils -module-alias Logging=GameLogging のように複数のエイリアスを同時に扱えます。

名前解決・意味解析・シンボルマングリング(例: $s9GameUtils5Level)・シリアライズはすべて binary name(GameUtils)を使って行われます。binary name は .swiftmodule に焼き込まれるため、-module-alias が必要になるのは「衝突するモジュール本体」と「それを直接 import しているモジュール」のビルドだけで、さらに間接的に依存しているモジュールには伝播しません。生成される .swiftinterface やインデックス・デバッグ情報にも binary name が記録され、これらの場面では binary name が source of truth として扱われます。

デバッグと LLDB

binary name はマングル済みシンボルに現れるため、デバッグ情報にもそのまま反映されます。式評価ではソースファイル中と同じ名前(Utils)で書けますが、評価結果には binary name(GameUtils)が現れます。LLDB にモジュールを直接ロードしたり REPL で扱ったりするときは、エイリアス情報を参照できないため binary name(import GameUtils のように)を使う必要があります。

利用上の制約

モジュールエイリアシングは、宣言の属するモジュール名を丸ごと差し替える仕組みであるため、現時点では次のような制約があります。

  • pure Swift モジュールのみが対象。Objective-C / C / C++ / アセンブリを含むモジュールはシンボル衝突の可能性があるため対象外で、@objc(name) の使用も推奨されません。
  • ソースからビルド可能なモジュールのみが対象。事前ビルド済みのバイナリはマングリングやシリアライズの都合からリネームできません。
  • 実行時に NSClassFromString(...) などで文字列からモジュール内の型を解決する呼び出しは、リネーム後の名前に一致しないと失敗します。
  • リソースはアセットカタログとローカライズ文字列のみサポートされます。IB や CoreData など、モジュール名を明示的に要求するリソースは当初のサポート対象外です。
  • retroactive conformance や extension の「漏れ出し」といった既存の問題を、エイリアシングと組み合わせることで踏みやすくなります。retroactive conformance は元々推奨されないプラクティスで、extension の漏れ出しは既知のバグです。

今後の方向性

本Proposalで導入される基盤は、将来のさらなる拡張の土台として位置付けられています。以下は speculative な見通しで、実現を約束するものではありません。

  • import Utils as GameUtils のような、ソースコード上で明示的にエイリアスを付ける import 構文。
  • import 宣言のアクセスレベル制御(public → internal への絞り込み)によって、extension の漏れ出し問題を軽減する方向。
  • C ターゲットへの依存や C++ interop を持つモジュールへの、限定的なエイリアシングサポート。
  • サブモジュールやネストした名前空間の導入。これらは衝突問題に対するより長期的な解決策になり得ますが、retroactive に衝突を解消するニーズは残ると考えられており、モジュールエイリアシングは直交する機能として共存可能とされています。