Swift Digest
SE-0274 | Swift Evolution

Concise magic file names

Proposal
SE-0274
Authors
Becca Royal-Gordon, Dave DeLong
Review Manager
Ben Cohen
Status
Implemented (Swift 5.8)

01 何が問題だったのか

Swift の magic identifier #file は、これまでそのソースファイルのフルパス(コンパイラに渡されたパス)を表す文字列リテラルに展開されていました。fatalError(_:file:line:) などのデフォルト引数で広く使われ、ログやアサーションでの位置特定に便利ですが、フルパスをそのまま埋め込むことにはいくつも欠点があります。

  • プライバシーや機密情報の漏洩: フルパスには開発者のユーザ名、ビルドファーム構成、社内プロジェクト名、外付けディスクの名前などが含まれることがあり、それがバイナリに埋め込まれていることに開発者自身気づきにくい状態でした。しかもほとんどの用途はデフォルト引数経由なので、呼び出し側のコードからも埋め込みが見えません。
  • バイナリサイズと起動性能: Swift ベンチマークでは、短い #file 文字列にするだけでコードサイズが最大 5% 減り、いくつかのベンチマークは 22% 高速化するなど、無視できない影響がありました。
  • 再現性のあるビルドの阻害: 同じコードでも、ビルドするマシンのディレクトリ構造が違えば異なるバイナリが生成されるため、ビルド成果物のキャッシュやバイナリ差分の比較が困難になります。

一方で、フルパスを保持することから期待できるメリットもあまりありません。

  • パスは他マシンに持ち出しても意味を持たないため、バイナリだけ受け取って「どのソース行か」を自動でマップできるとは限りません。
  • 絶対パスである保証もなく、XCBuild や SwiftPM は絶対パスを使う一方で、Bazel などは相対パスを使います。ディスク上のファイルに必ず辿り着ける、とは前提にできません。
  • 同一プロセス内での一意性すら保証されません。同じファイルが複数モジュールに含まれることも、別マシン/別時点で同じパスに別プロジェクトが置かれることもあります。

つまり、#file の主要ユースケース(呼び出し位置を人間にもツールにも識別させること)に対して、フルパスという形式はコストばかり大きく、得られる利点が釣り合っていない状態でした。

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

#file の展開結果を、フルパスから <module-name>/<file-name> という簡潔な形式に変更します。フルパスが本当に必要な用途のために、従来の #file と同じ挙動を持つ新しい magic identifier #filePath を導入します。どちらもデフォルト引数で使ったときに呼び出し側の位置を捕まえる挙動はそのままで、標準ライブラリのアサーションやエラー関数(fatalError など)は引き続き #file をデフォルト引数として使います。

具体的な挙動

モジュール名 MagicFile、ファイルパス /Users/becca/Desktop/0274-magic-file.swift のソースに次のコードがあるとします。

print(#file)
print(#filePath)
fatalError("Something bad happened!")

出力は次のようになります。

MagicFile/0274-magic-file.swift
/Users/becca/Desktop/0274-magic-file.swift
Fatal error: Something bad happened!: file MagicFile/0274-magic-file.swift, line 3

Swift コンパイラは同じモジュール内で同名ファイル(パスが違っても)を許さないため、<module-name>/<file-name> はフルパスに比べて大幅に短くなりながら、より確実にファイルを一意に識別できます。人間にも読みやすく、モジュールとファイルの対応さえ分かればツールでフルパスに戻すこともできます。

#file 文字列の形式

生成される文字列の形式は次のように仕様化されます。

file-string → module-name "/" file-name
file-string → module-name "/" disambiguator "/" file-name   // 将来用に予約
  • module-name: Swift の識別子と同じ文字集合。
  • disambiguator: 将来同名ファイルを区別する必要が出たとき用の予約フィールド。現状は常に省略されます。"/" を含む可能性があります。
  • file-name: "/" と NUL 文字以外。

この文字列をパースするときは、「最初の "/" までをモジュール名」「最後の "/" 以降をファイル名」として扱うのが安全です。要素が必ず 2 個になるとは仮定してはいけません(将来 disambiguator が差し込まれる余地があるため)。区切りはホスト OS のパス区切りではなく、常に "/" です。

#sourceLocation との関係

#sourceLocationfile: 引数は #filePath の内容を指定するものとして扱われ、#file 文字列はそこから計算されます(最後のパス要素が file-name になります)。モジュール名や #file 文字列そのものを直接指定する手段はありません。現状のコンパイラでは、#sourceLocation で導入したパスと物理ファイル、あるいは複数の #sourceLocation 同士で #file 文字列が衝突する可能性があり、その場合はコンパイラが警告を出します。

ラッパ関数での取り違え警告

#file#filePath のどちらをデフォルト引数に持つか間違えて取り違えると、ログやエラー情報の形式が一貫しなくなるため、コンパイラは次のようなケースを警告します。

func fn1(file: String = #filePath) { /* ... */ }
func fn2(file: String = #file) {
    fn1(file: file) // warning: #file を #filePath のデフォルト引数に渡している
}

警告を無視したい場合は、fn1(file: (file)) のように引数を括弧で囲みます。同種の取り違え(#file#function#line#column など)にも同じ診断が拡張されます。

ツールサポート

SourceKit が #file 文字列から元の #filePath に戻すためのマッピング機能を提供します。ビルドごとにどのモジュールにどのファイルが含まれるかが分かればマップできるため、コンパイラ側のツーリングが必要な情報を提供します。

移行と upcoming feature flag

挙動の変更は既存コードの動作に影響を与えうるため、#file の新しい挙動は次のメジャー言語バージョン(Swift 6)に先送りされました。それより前のバージョンでも、upcoming feature flag ConciseMagicFile を有効にすれば新しい挙動を試せます。今回の変更で困るコード(例えばフルパスを前提にしたロジック)は、呼び出し箇所を #file から #filePath に書き換えれば従来どおりの挙動を保てます。

今後の展望

disambiguator フィールドは将来、同じモジュール内で同名ファイルの衝突が起きた場合に両者を区別するためのスロットとして空けてあります。また、モジュール名だけを取り出す #moduleName を別途追加するといった派生的な拡張も、今回の #file 文字列から機械的に取り出せる前提で、将来別 Proposal として検討される余地が示されています。いずれも今回のスコープ外で、仕様として保証されるものではありません。