この記事では、Swiftコンパイラを直接使って、コードを実行したり、モジュールを分割ビルドする方法を解説します。
この内容を理解すると、Swiftのビルドの仕組みの理解が深まるでしょう。また、コンパイラに対していろいろな実験をする上でも役に立つでしょう。
単一モジュールの場合
以下では単一モジュールの場合について書きます。
単一のソースを直接実行する
単一のソースを直接実行するには、下記のようにswift
コマンドを実行します。
// a.swift
print(1)
$ swift a.swift
1
単一のソースから実行ファイルを作成する
直接実行せず、一度実行ファイルを作成する場合は下記のようにswiftc
コマンドを実行します。
// a.swift
print(1)
$ swiftc -emit-executable a.swift
# aができる
$ ./a
1
複数ファイルに分割したソースから実行ファイルを作成する。
ソースが複数ファイルの場合は直接実行はできません(多分)。
複数のソースファイルから実行ファイルを作成する場合、メインソースは1つだけ使えます。メインソースというのは、トップレベルの文として実行コードが書けるファイルの事です。
メインソースは、入力ファイルがただ1つである場合のそれか、もしくは、ファイル名がmain.swift
になっているものです。先程の例では単一ソースだったのでそれがメインソースでした。今回は複数ファイルの例なので、ファイル名をmain.swift
にします。
// a.swift
struct Cat {
var name = "tama"
}
// main.swift
print(Cat())
$ swiftc -emit-executable a.swift main.swift
# mainができる
$ ./main
Cat(name: "tama")
複数モジュールの場合
以下では、複数モジュールの場合について書きます。
複数モジュールの場合は、ライブラリモジュールと、そのモジュールを使用するモジュールに、ビルドの手順が分かれます。また、使用される方のモジュールについては、ヘッダーの生成と、バイナリの生成に更に手順が分かれます。ヘッダーの生成については、バイナリ形式ヘッダ(.swiftmodule
)かテキスト形式ヘッダ(.swiftinterface
)を選べます。バイナリについては、スタティックリンクするかダイナミックリンクするか選べます。また、スタティックリンクの場合に、オブジェクトファイル(.o
)を直接リンクするか、スタティックライブラリ(.a
)を作成するか選べます。
以下では、モジュールa
をa1.swift
とa2.swift
から作成し、そのモジュールa
をb.swift
から使用するという構成にします。ソースファイルは下記のとおりとします。
// a1.swift
public struct Cat {
public var name = "tama"
public init() {}
}
// a2.swift
public struct Dog {
public var name = "pochi"
public init() {}
}
// b.swift
import a
print(Cat())
print(Dog())
オブジェクトファイルを直接リンクする
ここでは、オブジェクトファイルを直接リンクする手順を解説します。まずは、ヘッダーファイルを作成します。モジュールのビルドでは-module-name
オプションでモジュール名を与える必要があります。
$ swiftc -emit-module a1.swift a2.swift -module-name a
# a.swiftmodule, a.swiftdocができる
次にバイナリを作成します。ヘッダーと同じモジュール名を与えます。
$ swiftc -emit-object a1.swift a2.swift -module-name a
# a1.o, a2.oができる
最後に利用側をビルドします。モジュールのバイナリオブジェクトを全て個別に指定します。ヘッダーは-I
オプションでディレクトリを指定するとそこから探索されます。
$ swiftc -emit-executable b.swift a1.o a2.o -I .
# mainができる
$ ./main
Cat(name: "tama")
Dog(name: "pochi")
モジュールが単一ファイルの場合のメインソースの無効化
今回はモジュールaに2つのファイルを与えましたが、もし1つの場合は、先述したとおりこれがメインソースになってしまいます。ライブラリの場合はメインソースが入っていると困るので、それを無効化するために以下のように-parse-as-library
オプションを与えます。
$ swiftc -emit-object -parse-as-library a1.swift -module-name a
テキスト形式のヘッダーを作成する
ヘッダーの作成手順のところで、テキスト形式のヘッダーを採用する事ができます。ただしその場合は同時にLibrary Evolutionも有効化する必要があります。
$ swiftc -emit-module-interface -enable-library-evolution a1.swift a2.swift -module-name a
# a.swiftinterface, a.swiftmodule, a.swiftdocができる
$ swiftc -emit-object -enable-library-evolution a1.swift a2.swift -module-name a
# a1.o, a2.oができる
利用側のビルドをオブジェクトの作成と実行ファイルの作成に分ける
利用側をビルドする時に、オブジェクトの作成と実行ファイルの作成の2段階に分けることができます。こうすると、ビルドをコンパイルとリンクに分けられます。
オブジェクトの作成のときには、ライブラリのヘッダーが必要なので、-I
でディレクトリを指定します。
$ swiftc -emit-object b.swift -I .
# b.oができる
なおここで、b.swift
が単一ファイルなのでメインソースになっていますが、利用側モジュールも複数ファイルの場合は、メインソースの名前をmain.swift
にします。
次に、オブジェクトファイルから実行ファイルを作成します。ここではモジュールのオブジェクトファイルも全て指定します。
$ swiftc -emit-executable b.o a1.o a2.o
# mainができる
$ ./main
Cat(name: "tama")
Dog(name: "pochi")
スタティックライブラリを作成して利用する
オブジェクトファイルを全て指定するのは面倒です。複数のオブジェクトファイルを1つのスタティックライブラリにまとめることで簡単になります。
まずはオブジェクトファイルを作成します。
$ swiftc -emit-object a1.swift a2.swift -module-name a
# a1.o, a2.oができる
次に、ar
コマンドを使ってスタティックライブラリにまとめます。
$ ar -rcs a.a a1.o a2.o
# a.aができる
利用側のビルドではオブジェクトファイルの代わりにスタティックライブラリを指定します。
$ swiftc -emit-executable b.swift a.a -I .
なお、ヘッダーに関してはこれまで同様に作成する必要があります。スタティックライブラリはバイナリをまとめただけのものであり、ヘッダーは含みません。
ダイナミックライブラリを作成して利用する
ダイナミックライブラリを作成することもできます。
$ swiftc -emit-library a1.swift a2.swift -module-name a
# liba.dylibができる
利用側をビルドする時には、ダイナミックライブラリを指定します。
$ swiftc -emit-executable b.swift liba.dylib -I .
# mainができる
$ ./main
Cat(name: "tama")
Dog(name: "pochi")
ただし、ダイナミックライブラリなので、ここではシンボルの解決をしているだけで、実際のリンクは行われていません。
otool
コマンドを使って、実行時のライブラリのロードの情報が入っている事が確認できます。
$ otool -L main
main:
liba.dylib (compatibility version 0.0.0, current version 0.0.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1281.0.0)
/usr/lib/swift/libswiftCore.dylib (compatibility version 1.0.0, current version 1100.8.255)
このように、liba.dylib
の指定があるので、実行ファイルと同じディレクトリにliba.dylib
が存在している必要があります。実際下記のようにディレクトリに移動すると起動できなくなります。
$ mkdir lib
$ mv liba.dylib lib
$ ./main
dyld: Library not loaded: liba.dylib
Referenced from: /Users/omochi/xxx/./main
Reason: image not found
Abort trap: 6
例えば実行時のライブラリのサーチパスを環境変数で指定することでこれでも起動したりできます。
$ DYLD_LIBRARY_PATH=./lib ./main
Cat(name: "tama")
Dog(name: "pochi")
ヘッダのみ使う
余談ですが、コンパイラが出力するSILやLLVM-IRを調べる場合など、最終的にオブジェクトファイルを作らないときは、リンクフェーズまで作業を進めないので、ライブラリモジュールのオブジェクトファイルは必要ありません。そういうときはヘッダファイルだけしか必要ないので、覚えておくと便利です。
詳細なコマンドを見る
ここまで紹介したコンパイラの使用方法は、コンパイラのドライバというインターフェースを使用しています。これは人間に使いやすいように用意された上層のインターフェースです。コンパイラ内部には下層のインターフェースとしてフロントエンドというものがあります。
ドライバは、自身が受け取った引数に基づいて、フロントエンドのコマンドを構築して内部で実行するようになっていて、コンパイラが実際に起動するのはそのときになります。
ドライバに-driver-print-jobs
オプションを与えると、実際にフロントエンドを実行することなく、構築させたコマンドを見るだけができます。
例えば、最初の単一実行のケースは以下のようになります。
$ swift -driver-print-jobs a.swift
/Applications/Xcode11.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -interpret a.swift -enable-objc-interop -sdk /Applications/Xcode11.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -color-diagnostics -module-name a # DYLD_LIBRARY_PATH=/Applications/Xcode11.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx:/Applications/Xcode11.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/usr/lib/swift:/usr/lib/swift
フロントエンドの起動では、このように最初の引数として-frontend
を指定されます。システムライブラリが-sdk
オプションで渡されているなど、暗黙に解決されていた事がわかります。コメントで書かれているDYLD_LIBRARY_PATH
は、これを環境変数に設定した上でコマンドを実行する事を意味しているのだと思います。
モジュールヘッダの生成は以下のようになります。
$ swiftc -driver-print-jobs -emit-module a1.swift a2.swift -module-name a
/Applications/Xcode11.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -emit-module -primary-file a1.swift a2.swift -emit-module-doc-path /var/folders/8v/h2t3_g7j6zj51cqjtx7x0r500000gn/T/a1-9e452a.swiftdoc -target x86_64-apple-darwin19.0.0 -enable-objc-interop -sdk /Applications/Xcode11.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -color-diagnostics -module-name a -o /var/folders/8v/h2t3_g7j6zj51cqjtx7x0r500000gn/T/a1-9e452a.swiftmodule
/Applications/Xcode11.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -emit-module a1.swift -primary-file a2.swift -emit-module-doc-path /var/folders/8v/h2t3_g7j6zj51cqjtx7x0r500000gn/T/a2-07c268.swiftdoc -target x86_64-apple-darwin19.0.0 -enable-objc-interop -sdk /Applications/Xcode11.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -color-diagnostics -module-name a -o /var/folders/8v/h2t3_g7j6zj51cqjtx7x0r500000gn/T/a2-07c268.swiftmodule
/Applications/Xcode11.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -merge-modules -emit-module /var/folders/8v/h2t3_g7j6zj51cqjtx7x0r500000gn/T/a1-9e452a.swiftmodule /var/folders/8v/h2t3_g7j6zj51cqjtx7x0r500000gn/T/a2-07c268.swiftmodule -parse-as-library -sil-merge-partial-modules -disable-diagnostic-passes -disable-sil-perf-optzns -target x86_64-apple-darwin19.0.0 -enable-objc-interop -sdk /Applications/Xcode11.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -color-diagnostics -emit-module-doc-path a.swiftdoc -module-name a -o a.swiftmodule
かなり複雑な事になっていて、3つのフロントエンドコマンドに分かれているのがわかります。よく見ると、最初の2つはa1.swift
とa2.swift
のそれぞれのヘッダーを生成していて、3つ目のコマンドでそれらを結合して一つのヘッダーにしている事がわかります。
オブジェクトファイルの生成は以下のようになります。
$ swiftc -driver-print-jobs -emit-object a1.swift a2.swift -module-name a
/Applications/Xcode11.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -c -primary-file a1.swift a2.swift -target x86_64-apple-darwin19.0.0 -enable-objc-interop -sdk /Applications/Xcode11.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -color-diagnostics -module-name a -o a1.o
/Applications/Xcode11.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -c a1.swift -primary-file a2.swift -target x86_64-apple-darwin19.0.0 -enable-objc-interop -sdk /Applications/Xcode11.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -color-diagnostics -module-name a -o a2.o
a1.o
の生成と、a2.o
の生成が別々のコマンドに分かれていることがわかります。
このように、-driver-print-jobs
を使うことで、より詳細な内部動作の事がわかります。もちろん、ここで表示されたコマンドを一つずつ実行する事もできます。
ドライバとフロントエンドについては以下の記事が参考になります。
-Xfrontend
オプション
-Xfrontend
オプションを使うと、ドライバの後段にあるフロントエンドにオプションを渡す事ができます。
例えば、-debug-constraints
を渡すと型推論のログがでます。
// a.swift
let a = 3
$ swift -Xfrontend -debug-constraints a.swift
---Constraint solving for the expression at [a.swift:1:9 - line:1:9]---
---Initial constraints for the given expression---
(integer_literal_expr type='$T0' location=a.swift:1:9 range=[a.swift:1:9 - line:1:9] value=3 builtin_initializer=**NULL** initializer=**NULL**)
Score: 0 0 0 0 0 0 0 0 0 0 0 0
Type Variables:
$T0 literal=3 bindings={(subtypes of) (default from ExpressibleByIntegerLiteral) Int} @ locator@0x7fb2128bd800 [IntegerLiteral@a.swift:1:9]
Active Constraints:
Inactive Constraints:
$T0 literal conforms to ExpressibleByIntegerLiteral [[locator@0x7fb2128bd800 [IntegerLiteral@a.swift:1:9]]];
($T0 literal=3 bindings={(subtypes of) (default from ExpressibleByIntegerLiteral) Int})
Initial bindings: $T0 := Int
(attempting type variable $T0 := Int
(found solution 0 0 0 0 0 0 0 0 0 0 0 0)
)
---Solver statistics---
Total number of scopes explored: 2
Maximum depth reached while exploring solutions: 2
Time: 3.896000e+00ms
---Solution---
Fixed score: 0 0 0 0 0 0 0 0 0 0 0 0
Type variables:
$T0 as Int @ locator@0x7fb2128bd800 [IntegerLiteral@a.swift:1:9]
Overload choices:
Constraint restrictions:
Disjunction choices:
---Type-checked expression---
(integer_literal_expr type='Int' location=a.swift:1:9 range=[a.swift:1:9 - line:1:9] value=3 builtin_initializer=Swift.(file).Int.init(_builtinIntegerLiteral:) initializer=**NULL**)
例えば、-enable-experimental-static-assert
を渡すと、開発中のコンパイル時アサートが使えます。
// a.swift
#assert(1 + 1 == 3)
$ swift -Xfrontend -enable-experimental-static-assert a.swift
a.swift:1:1: error: assertion failed
#assert(1 + 1 == 3)
^
フロントエンドオプションは以下で定義されているので調べてみると面白いです。
このように後段のコマンドに渡すオプションを埋め込むためのオプションは-X
シリーズとして統一されています。例えばこの他にも-Xllvm
, -Xcc
, -Xlinker
などがあります。
incrementalモード
ここまで紹介したビルド方法は、残念ながらXcodeでもSwiftPMでももはや使用されていません。これらのビルド環境では、incrementalモードというものが使われています。
これは、プロジェクトの中で一部のファイルだけが変更された時に、必要なところだけリビルドするための仕組みです。それを実現するためにソース間の依存関係を解析したファイルを生成したりするので、使用するコマンドの種類や与えるオプションもより複雑になります。
Xcodeのビルドログを見たり、swift build
に-v
オプションを与えることで、これらのビルドで使われるコマンドを知る事ができます。
この記事で紹介したものは簡単な実験などに適したものです。高機能なビルドシステムを作ったりする場合は、incrementalモードについても調べたほうが良いです。
そのあたりの情報は下記にも書かれています。