kateinoigakukunのブログ

思考垂れ流しObserver

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 文を書くだけで、インポートしたモジュールに紐づくライブラリがリンクされます。 XcodeiOSアプリを書く際、明示的に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を壊さずに依存ライブラリを変更出来るようになります。

詳しい議論については以下のフォーラム投稿を見てください。

forums.swift.org

ここで、次のケースを考えます。モジュール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の静的リンク時にシンボル不足エラーが出ています。

[SR-14536] Importing FoundationNetworking with -static-stdlib is broken with missing symbols on Linux - Swift

修正編

この問題を解消するためには、Foundationにicui18nへのライブラリ依存(=LINK_LIBRARYエントリ)を持たせれば良いわけです。 しかしSwift 5.4当時のコンパイラに任意のライブラリの LINK_LIBRARY エントリをモジュールに差し込む機能はありませんでした。

そこで、Swift Forumで解決策について議論した上で、コンパイラに新しいオプションを追加実装しました。

このパッチでは、-public-autolink-library というオプションを追加して、指定したライブラリを LINK_LIBRARYエントリ に出力するようにしています。

forums.swift.org

github.com

コンパイラ側のパッチがマージされると、ようやくFoundation側の修正に取り組めます。 このパッチでは、追加したオプションを使って icui18n へのライブラリ依存をFoundationに足しています。

github.com

その後リバートやリバート返しを経て、最近ようやく直りました。

まとめ

Auto-linkingを維持するために多大な努力と時間が費やされていることが伝われば嬉しいです。 結局最初に問題に気がついてから修正まで半年くらいかかってます。大半はコミュニケーションですが。

すごく頑張ったのでLinuxでSwiftを静的リンクしてる人たちが救われてほしい。