はじめに
みなさん、こんにちは。freddi と申します。福岡在住のiOSエンジニアで、たまにSwiftコンパイラの勉強をしていたり、Swiftコンパイラに関する記事の執筆や登壇をしています。
本日、Swiftアドベントカレンダー 2020の12/15の記事として、AppleがOSSとして出しているSwift製のSwiftコンパイラのドライバ swift-driver の紹介と、その swift-driver
が、どこで、どのよう使われているかについて軽く解説します。
Swiftコンパイラとドライバ
swiftc
コマンド
皆さん、Xcodeを使わずにTermnal上で swiftc
コマンドを使って、Swiftコードをコンパイルしたことはありますか?
例えば hello.swift
という名前のSwiftで書かれたソースファイルをコンパイルして、Terminal上などで実行できる形式にしたい場合、
$ swiftc hello.swift
というふうに書きます。これで、同じディレクトリに hello
という名前の、実行可能な形式のファイルが出来上がります。
私達は、このように swiftc
コマンドを使ってプログラムをコンパイル/ビルドしています。なので、普段 swiftc
というコマンド(プログラム)そのものがコンパイラ本体だと思われがちかもしれません。
例えば、C言語をコンパイルするための gcc
コマンドも、同じようなノリで gcc
そのものがコンパイラプログラム全てとして捉えられてしまうことが多々あると思います。
ですが、みなさんが触っている swiftc
や gcc
といったコマンドは、実はコンパイラすべてを包括したものではない、コンパイラドライバ と呼ばれるプログラムです。さて、ドライバとは一体何でしょうか?
ドライバ とは
まず、ドライバ について一体何かを解説します。ここでいうドライバとは、ハードウェア等の導入に必要なドライバとはまた別のものを指しています1。ドライバと調べても、多分こちらのハードウェアのほうが多く出るので、この記事の解説対象になっている方の意味を知ることは、なかなか少ないかもしれません。
ドライバプログラムとは、一言で言えば、いくつかのパーツとしてのプログラムのを適切に参照・呼び出しをするプログラム を指します。さて、この意味について、gcc
コマンドを例に掘り下げていきましょう。1
まず、gcc
でC言語のソースファイルを実行可能な形式のものにする際に、いくつかフェーズを踏むのはご存知でしょうか。順に大まかに列挙すると、
- プリプロセス
- コンパイル
- アセンブル
- リンク
という風になっています。
実は、それぞれのフェーズは別のコマンド(プログラム)が行っており、全て gcc
が担っているわけではありません。こちらも列挙すると、
gcc
は単に 細かいオプション引数と言ったユーザーの要求を元に、それらのプログラムをフェーズ順に呼び出ている だけです。これが コンパイラと呼ばれるもののドライバプログラムに当たります。これは swiftc
についても同様です。
ドライバに関する詳しい働き等は、この項目を執筆する際に情報元になった http://nenya.cis.ibaraki.ac.jp/TIPS/compiler.html を見てもらえればと思います。
swiftc
/swift
コマンドの正体
ここでは、swiftc
/swift
コマンドのドライバとしての機能について少々フォーカスします
前項目では、swiftc
はコンパイラそのものではなくドライバプログラムであることと、ドライバプログラムとは一体何かについて説明しました。また、ご存じの方もいると思いますが、swiftc
だけでなく、swift
コマンド 2 も存在しますがこちらもドライバです。
実は、最新のSwiftコンパイラのブランチでは、swift
と swiftc
はほぼ同じコマンドであり、それらは swift-frontend
という名前のコマンドのシンボリックリンク3となっています。 4 5
なぜswift
/ swiftc
に分かれているかというと、Swiftコンパイラのドライバとしての働き(実行可能形式なものを生成するか、REPLモード etc ...)は、付与されるオプション引数にもよりますが、swift-frontend
のシンボリックリンクが、どのような名前で呼び出されるかによっても変わります。
https://github.com/apple/swift/blob/main/lib/Driver/Driver.cpp#L98-L105 をみると、どのコマンドとして呼ぶと、ドライバがどの様に働くようになるかがわかります。
swift-driver
さて、ここまではSwiftコンパイラのドライバの話をしましたが、SwiftコンパイラはほとんどがC++で開発されており、ドライバ周りもC++で実装されています。
そのSwiftコンパイラのドライバプログラムをSwift言語で実装したものが、タイトルにもある swift-driver です。swift-driver
はすでに Swift Package Manager 6 の実装にも取り入れられているなど、登場後すぐに活用はされていますが、そもそもなぜ登場したのでしょうか?
swift-driver
とは
そもそも、swift-driver
とは何でしょうか?
swift-driver
は Apple製のOSSで、先程申し上げた swift
/ swiftc
の働きをC++実装からSwiftになるべく移植したもの、及び、それを実現するためのライブラリ (SwiftDriver
など) からなっています。理由は後述しますが、SwiftDriver
という内部のライブラリがこのOSSの肝だと、私は思っています。
swift-driver
はコンパイルの各フェーズをなるべくSwiftでラップしているだけで、最終的には裏で swift-frontend
などを通じて各フェーズを呼び出したり、リンクする標準ライブラリの情報などが必要になるため、既存の Swiftコンパイラ一式 は事前に準備が必要です。
swift-driver
によるドライバ実装
swift-frontend
の動きをSwift言語で実装したものが こちら で、README.md どおりに進めると、これをまず動かすことになります。説明通りビルドすると、swift-driver
という名前のコマンド(プログラム)が生成され、これが本家 swift-frontend
と同じ動きをするようになっています。
こちらも、先程 swift-frontend
で言及したやり方と同様、swift
または swiftc
という名前のシンボリックリンクを作って呼び出すなどして、呼び出し名による機能の分岐などを行ったりします。また本家同様のオプション引数も付けれます。
個人的には、この swift-frontend
は所謂サンプルコードのようなものであると思っており、ここでの SwiftDriver
などのライブラリの使い方が肝になっていると思います。
なぜ Swift言語でドライバを再実装するのか
このドライバのメリットに関する詳しい議論などはなかなか見つからないのですが、かなり参考になるものとして、Javiさんという方がTwitterで質問し、Doug Gregor さんというAppleのエンジニアが答えたTwitterのスレッドがあります。要約すると、
- このドライバは Swift Package Manager の外側のビルドシステムからは不透明な、インクリメンタルビルドのための内部ビルドシステムを備えている
- このドライバを使うと、Swift Package Manager はドライバのビルドタスクを自身のビルドグラフに統合できるし、一つのシングルキューで並行タスクを管理できるようになる
- このドライバで動的スケジューリングができるようになる
ということをメリットに上げています。見ての通り少々難しいので、Swift Package Manager に swift-driver
が導入されたときのPull Request の Description 6 も参考にしつつ、言い換えると、
- Swift言語製のビルド関係のツールで、Swiftコンパイラを、
swift
/swiftc
といった外部のコマンド・プロセスとして扱うのではなく、できる限り内部のSwiftプログラムの一部として扱うことができる - 各フェーズの呼び出しなども
swift-driver
を通して細かく扱えるので、今まで手の届かなかった上記の要約のような改善ができるようになる(なった)
というのが一番簡単な説明になると思います。
swift-driver
はSwiftのライブラリとして扱える
前項で言及したことや、Swift Package Manager の実装に取り入れられていることからわかりますが、swift-driver
は、SwiftDriver
という名前のSwiftのライブラリとして扱えます。なので、Swift言語製のSwiftに関するビルドツールを作るとき、より高度なビルド・コンパイルの設定・構築・管理を行う場合に役に立ちます。
しかし、うまく利用するには、ある程度のコンパイラの知識、Swiftコンパイラ本体のドライバの実装などについて知る必要があるかもしれません。もしくは、先程言及した swift-frontend の動きを swift-driver(SwiftDriver) を利用して実装したもの を読んで理解を深めるのも良いと思います。
この実装に関しては、私が以前 わいわいswiftc という勉強会で解説した のでこちらもご一読すると良いかもしれません。
SwiftDriver
に少々入門する
さて、ここまで説明した中で、SwiftDriver
に入門しないのはもったいないので、ついでにこのライブラリの使い方について、少々入門してみましょう。
Swift Package Manager が SwiftDriver
を利用している部分を見てみましょう。フォーカするコードのURLは こちら です。その中から、いくつか特徴的なものを抜粋します。
注意ですが、このコードはSwift Package Manager が導入された当時のコードであり、現在はほぼ改変されています。この時期のコードを選んだのは、このときが一番 SwiftDriver
をピュアにわかりやすく利用しているからです。
まず最初の2行ですが、これは一体何をしているのでしょうか。
var driver = try Driver(args: target.emitCommandLine())
let jobs = try driver.planBuild()
まず一行目は、SwiftDriver
にある、Driver
型のオブジェクトを生成しています。こちらは、Swift Package Manager が構築した、本来 swift-frontend
に渡すべきコマンドやオプション引数を Driver
のイニシャライザに渡しています。分かり易い例として上げると swiftc hello.swift
を渡しても構いません。
次の try driver.planBuild()
ですが、こちらは Driver
に先程渡された情報から、コンパイラのどのコマンドやプログラムを、どのような順序で呼び出すべきかを構築して、その情報を Job
という型の配列を返します。
Job
型はコンパイラのタスクを表しており、こちら を見るとどのような物があるかを Job
の派生型として知ることができます。例えば、コンパイルは CompileJob、リンクは LinkJob とその他の Link
という単語のついた Job
であることがわかります。
どのような Job
であるかは、kind
という変数で知ることができます。下記は、先程の Swift Package Manager の 207行目以降のコードですが、先程生成された jobs
をfor文で回して、それぞれのタスクの種類の情報から適当な処理を行おうとしています。(FIXMEがついていますが、現在はだいぶ改変されているので消えています)
// https://github.com/DougGregor/swift-package-manager/blob/c818cdb7d9cbef9d8bff6e0d96cbce9bd77b6bd7/Sources/Build/ManifestBuilder.swift#L207
for job in jobs {
// Figure out which tool we are using.
// FIXME: This feels like a hack.
var datool: String
switch job.kind {
case .compile, .mergeModule, .emitModule, .generatePCH,
.generatePCM, .interpret, .repl, .printTargetInfo,
.versionRequest:
datool = buildParameters.toolchain.swiftCompiler.pathString
case .autolinkExtract, .generateDSYM, .help, .link, .verifyDebugInfo:
datool = try resolver.resolve(.path(job.tool))
}
...
このように、SwiftコンパイラのタスクをSwiftのプログラム上で簡単に扱うことができます。
まとめ
本日は、SwiftUIやアプリ開発といった話題ではなくディープな話題になりましたが、いかがだったでしょうか?
swift-driver
は見たところまだ発展途上のOSSであり、至るところに FIXME:
といったコメントがあるので、コントリビュートのチャンスがたくさんあると思います。もし興味があれば、Swiftコンパイラ本体の実装と合わせて深く覗いてみて、ぜひコントリビュートチャレンジしてみてください。
-
情報ソースはこちら(https://www.quora.com/What-is-a-driver-for-example-a-compiler-driver) こちらではハードウェア方面の意味のドライバも解説があります ↩ ↩2
-
ご存じの方が多いと思いますが、こちらは REPLモード(http://blog.andgenie.jp/articles/639) の呼び出しの目的で主に使われています ↩
-
シンボリックリンクの説明としては、 http://e-words.jp/w/%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AA%E3%83%83%E3%82%AF%E3%83%AA%E3%83%B3%E3%82%AF.html#:~:text=%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AA%E3%83%83%E3%82%AF%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%A8%E3%81%AF%E3%80%81%E3%82%AA%E3%83%9A%E3%83%AC%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0,%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B%E4%BB%95%E7%B5%84%E3%81%BF%E3%80%82 ここがわかり易いです ↩
-
https://github.com/apple/swift からSwiftコンパイラをビルドすると、以前のように
swiftc
およびswift
コマンドは生成されません。代わりに説明にある通りswift-frontend
という名前のコマンドが生成されるようになります。https://github.com/apple/swift/pull/28003 ↩ -
https://swift.org/download/#releases の Trunk Development (main) からToolchain をインストールすることでも確認ができます。ちなみに現在の公式リリースの Xcode12.* では、まだ
swift-frontend
コマンドはないです ↩ -
https://github.com/apple/swift-package-manager/pull/2736 ↩ ↩2