SwiftコンパイラのAuto-linkingとそれを直した話
前回のエントリでAuto-linkingについて解説しました。今回はSwiftコンパイラにおけるAuto-linkingの使われ方と、最近それを直した話をします。
kateinoigakukun.hatenablog.com
用語定義
- モジュール: Swiftのimportできる単位。
.swiftmodule
、.swiftinterface
またはmodule.modulemap
が実態。Cで言うヘッダ - ライブラリ:
libfoo.a
とかlibfoo.dylib
。大抵モジュールと1対1になってる。
SwiftのAuto-linking
C言語では以下のようなpragmaを書くことでリンクするライブラリを指定していました。
#include <math.h> #pragma comment(lib, "m")
一方で、Swiftでは import
文を書くだけで、インポートしたモジュールに紐づくライブラリがリンクされます。
XcodeでiOSアプリを書く際、明示的にUIKitのリンク設定をすることなくビルドが成功しているのは、Auto-linkingの仕組みのおかげです。
import UIKit
モジュールにライブラリを紐付ける為には、モジュールをビルドする際に -module-link-name
オプションを付ける必要があります。このオプションを明示的に指定しない場合Auto-linkingは有効になりません。
// foo.swift public func inc(_ v: Int) -> Int { v + 1 } // main.swift import foo _ = inc(1)
# モジュール "foo" をコンパイル $ swiftc -emit-module -emit-library foo.swift -module-link-name foo $ ls foo.swift foo.swiftmodule libfoo.dylib foo.swiftdoc foo.swiftsourceinfo # -lfoo 無しで動く $ swiftc main.swift -I. -L.
llvm-bcanalyzer -dump foo.swiftmodule
でモジュールファイルの中身を眺めると、 LINK_LIBRARY
に "foo"
が指定されていることがわかると思います。
コンパイラはimport文で読み込んだモジュールファイルからLINK_LIBRARY
エントリを探し、得られたライブラリの情報をオブジェクトファイルに格納します。
// foo.swiftmodule <MODULE_BLOCK NumWords=610 BlockCodeSize=2> <INPUT_BLOCK NumWords=23 BlockCodeSize=4> <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'Swift' <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'SwiftOnoneSupport' <LINK_LIBRARY abbrevid=6 op0=0 op1=0/> blob data = 'foo' </INPUT_BLOCK> </MODULE_BLOCK>
以下にmacOS上でのMach-Oオブジェクトファイルの内容を示します。前回のエントリで紹介した LC_LINKER_OPTION
に -lfoo
が埋め込まれていますね。これをリンカに渡すことでユーザはリンカオプションを渡すこと無く、ライブラリをリンクできます。実は標準ライブラリも同様の仕組みでリンクされています。
$ swiftc -c main.swift -I. -o main.o $ llvm-objdump -m -x main.o ... Load command 4 cmd LC_LINKER_OPTION cmdsize 24 count 1 string #1 -lfoo Load command 5 cmd LC_LINKER_OPTION cmdsize 40 count 1 string #1 -lswiftSwiftOnoneSupport Load command 6 cmd LC_LINKER_OPTION cmdsize 24 count 1 string #1 -lswiftCore
間接依存するライブラリのAuto-linking
さて、モジュールとライブラリの依存関係が次のようになっていた場合を考えてみましょう。 mainはbarだけをインポートしていて、barを介してfooに間接依存していますね。
この場合、最終的な成果物にはfooとbar両方がリンクされていて欲しいです。
main -> bar -> foo
// foo.swift public func inc(_ v: Int) -> Int { v + 1 } // bar.swift import foo public func dec(_ v: Int) -> Int { inc(v) - 2 } // main.swift import bar _ = dec(1)
コンパイラが main.swift
を処理する中で import bar
から、bar.swiftmodule
を読み込みます。
直接依存するbar.swiftmodule
には ライブラリbar
をリンクするLINK_LIBRARY
エントリと、barモジュールがfooモジュールに依存していることを示す IMPORTED_MODULE
エントリが含まれています。
コンパイラは IMPORTED_MODULE
によるモジュールの依存グラフを辿っていき、全てのモジュールから LINK_LIBRARY
をかき集めます。
// bar.swiftmodule <MODULE_BLOCK NumWords=612 BlockCodeSize=2> <INPUT_BLOCK NumWords=25 BlockCodeSize=4> <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'Swift' <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'SwiftOnoneSupport' <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'foo' <LINK_LIBRARY abbrevid=6 op0=0 op1=0/> blob data = 'bar' </INPUT_BLOCK> </MODULE_BLOCK>
最終的にオブジェクトファイルには期待通りfooとbar両方のリンカオプションが埋め込まれます。
Load command 4 cmd LC_LINKER_OPTION cmdsize 24 count 1 string #1 -lbar Load command 5 cmd LC_LINKER_OPTION cmdsize 24 count 1 string #1 -lfoo
プライベートな依存を表現する @_implementaionOnly
Swiftにはimport文にいくつかのバリエーションがあり、その内の1つに@_implementaionOnly
というアトリビュートがあります。
セマンティクスは「importしたモジュールの型が自身のpublicなAPI/ABIに露出していないことを保証する」です。 言い換えると、「API/ABIは再エクスポートせず実装のみをライブラリ内で使う」になると思います。
これにより、ライブラリ作成者は自身の依存をプライベートにでき、ライブラリのABIを壊さずに依存ライブラリを変更出来るようになります。
詳しい議論については以下のフォーラム投稿を見てください。
ここで、次のケースを考えます。モジュールfooに依存するモジュールbarはfooをプライベートにインポートします。
依存をプライベートにする、ということはライブラリの利用者mainは間接依存するfooの存在について関知せず、mainのリンク時にfooをリンクする必要があるか分かりません。
たとえば、barが共有ライブラリとして配布されている場合、そこにfooが静的にリンクされている場合もあります。その場合mainをリンクする際には -lbar
だけあれば良いです。むしろ libfoo.a
が配布されているとは限らないため、 -lfoo
をmainのリンク時に渡すと、ライブラリfooが見つからずリンクエラーになるかもしれません。
main -> bar -> foo
// foo.swift public func inc(_ v: Int) -> Int { v + 1 } // bar.swift @_implementaionOnly import foo public func dec(_ v: Int) -> Int { inc(v) - 2 } // main.swift import bar _ = dec(1)
ということで、 @_implementaionOnly
付きでインポートされると、その先のモジュール依存グラフを追わなくなります。
つまり、bar
をインポートする環境にfoo.swiftmodule
が配布されていなくても良いわけです。
@_implementaionOnly
の有無によるbar.swiftmoduleの差分を以下に示します。なにやら IMPORTED_MODULE
エントリにフラグが立っていますね。
これが@_implementaionOnly
の印です。
このbarをインポートしたmain.swiftをコンパイルすると、-lbar
だけが埋め込まれたオブジェクトファイルができます。fooへの依存が隠蔽されてますね。
// bar.swiftmodule <MODULE_BLOCK NumWords=612 BlockCodeSize=2> <INPUT_BLOCK NumWords=25 BlockCodeSize=4> <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'Swift' <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'SwiftOnoneSupport' - <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'foo' + <IMPORTED_MODULE abbrevid=4 op0=2 op1=0 op2=0/> blob data = 'foo' <LINK_LIBRARY abbrevid=6 op0=0 op1=0/> blob data = 'bar' </INPUT_BLOCK> </MODULE_BLOCK>
@_implementaionOnly
によって壊れた静的Auto-linking
さて、@_implementaionOnly
付きでもAuto-linkingはうまく動いているように思われましたが、実はこれだけでは上手く行かないケースがあります。
ここでは実際に壊れてしまった Foundation (apple/swift-corelibs-foundation)のケースを紹介します。 Foundationのモジュール依存関係は以下のようになっています。(説明のため簡略化してます)
Foundation -> CoreFoundation
さらに、CoreFoundationモジュールは module.modulemap
で定義されており、依存ライブラリとして icui18n
を指定しています。
そのためライブラリの依存関係としては、次のようになっています。
Foundation -> CoreFoundation -> icui18n
CoreFoundationはツールチェインユーザーから直接使われることを想定しておらず、Foundationのプライベートな依存であるため、ある日全ての import CoreFoundation
が @_implementaionOnly
付きに変更されました。これにより、ツールチェインにCoreFoundationのヘッダやmodulemapを含める必要が無くなります。(実際はまだ含まれていますが究極的には必要ないです。)
しかし、よく考えてみてください。「Foundationのプライベートな依存CoreFoundation」が依存するICUのicui18nは本当にプライベートな依存ライブラリでしょうか? icui18nはSwift標準ライブラリでも使われており、プログラム全体で共有すべき依存です。 しかし、Foundationはicui18nへの直接のライブラリ依存を持っておらず、CoreFoundationへの依存がプライベートとなってしまった状態ではリンカオプションを伝播する手段がありません。
この問題により、Swift 5.4のLinux向けツールチェインではFoundationの静的リンク時にシンボル不足エラーが出ています。
修正編
この問題を解消するためには、Foundationにicui18nへのライブラリ依存(=LINK_LIBRARYエントリ)を持たせれば良いわけです。
しかしSwift 5.4当時のコンパイラに任意のライブラリの LINK_LIBRARY
エントリをモジュールに差し込む機能はありませんでした。
そこで、Swift Forumで解決策について議論した上で、コンパイラに新しいオプションを追加実装しました。
このパッチでは、-public-autolink-library
というオプションを追加して、指定したライブラリを LINK_LIBRARY
エントリ に出力するようにしています。
コンパイラ側のパッチがマージされると、ようやくFoundation側の修正に取り組めます。
このパッチでは、追加したオプションを使って icui18n
へのライブラリ依存をFoundationに足しています。
その後リバートやリバート返しを経て、最近ようやく直りました。
まとめ
Auto-linkingを維持するために多大な努力と時間が費やされていることが伝われば嬉しいです。 結局最初に問題に気がついてから修正まで半年くらいかかってます。大半はコミュニケーションですが。
すごく頑張ったのでLinuxでSwiftを静的リンクしてる人たちが救われてほしい。