slidenumbers: true autoscale: true
- コンパイラの「ドライバ」について知る
swift-driver
の詳細について知る- 実際に コード読ませながら進行します。
- わかんなくなったら遠慮せず、適宜止めてください
- うまく説明できないのでとりあえずググったらなんかいい回答出てきた
- What is a driver, for example, a compiler driver? at Quora
- Ken Gregg (Founder and CEO, Bytellect LLC) さんが説明
- There are several uses of the term “driver” in the world of computer software and hardware.
- コンピュータの世界やと、ハードウェアとソフトウェア(開発)の世界で違うんやで
- The most common is device driver (sometimes just shortened to driver), which is a piece of software that allows an operating system to communicate with a hardware device.
- 大体の場合「ドライバ」というとハードウェアの世界を指してて、OSとデバイスのやりとりをサポート(許可)するやつやで
- バスドライバ・フィルタドライバ・ソフトウェアドライバ(ソフトウェアの世界の説明ではない)...
- In software development in general, the term driver is sometimes used to refer to a piece of software that controls or drives some other piece of software.
- ソフトウェア開発の世界やと一般的には、ドライバという言葉はいくつかのパーツになっているソフトウェアを参照したり、呼び出したりするソフトウェアのことを指すやで
- For example, the LLVM Compiler Driver (llvmc) is a tool that can be configured to invoke several compiler development tools and related tools, acting as a single point of access for users of the LLVM set of tools.
- たとえば、LLVM Compiler Driver はいくつかのコンパイラ(とその関連)ツールの呼び出しを構成するものやで
- ユーザから見たらLLVMのツール群にアクセスできる、一つのアクセスポイントとして振る舞うやで
- The llvmc tool is a configurable compiler driver. As such, it isn't a compiler, optimizer, or a linker itself but it drives (invokes) other software that perform those tasks. If you are familiar with the GNU Compiler Collection's gcc tool, llvmc is very similar.
llvmc
自体は コンパイラ・Optimizer・リンカ自身 じゃないでllvmc
はドライバとして コンパイラ・Optimizer・リンカ自身 を動かす(drives・呼び出す)やで
- みなさんがコンパイラだと思っている
gcc
は、 実は、コンパイラドライバ(compiler driver)と呼ばれるプログラムであり、 Cなどのプログラミング言語で書かれたソースプログラムから実行形式を 作り出すための処理を行う。 gcc
は、「必要に応じてコンパイラや アセンブラ、リンケージエディタなどのプログラムを呼び出す」という 処理を行っている。gccコマンド自体が直接コンパイルを行っているわけではない。- 「コンパイル」と呼ばれている処理は
cc1
という別のコマンドがやっている
- 「コンパイル」と呼ばれている処理は
- ドライバは、コンパイラ及びその関連ツール(Optimizer・リンカなど)を呼び出すツール
gcc
やllvmc
、swift
と言ったコマンドは、ドライバであって、コンパイラ自身ではない
swift-frontend
コマンドからシンボリックリンクでswift
とswiftc
を作っている- のちのち、とある分岐でこのシンボリックリンクの名前がドライバとしての挙動の分岐になるが後述
- ちなみに、ドライバが呼び出す仕事の単位(リンクなど)をジョブ と呼ばれる
- main/lib/Driver にドライバのコードが有る
- 今回は C++ で書かれた本体の Driver ではなく、Swiftで書かれた
swift-driver
を見る
- Swift ドライバの Swift による書き換え
- Appleのエンジニアによる、SNSでの初出の言及
- メリットはこの Twitter スレで詳しく説明されている
- アホみたいな質問かもしれへんけど、これ使う良い点何?
- アホみたいな質問じゃないで。一つの大きな利点は、このドライバはSwiftPMの外側のビルドシステムからは不透明な、インクリメンタルビルドのための内部ビルドシステムを備えているやで
- leading to oversubscribing the machine when (say) building lots of targets in parallel. がよくわからん
- このドライバ使うと、SwiftPM はドライバのビルドタスクを自身のビルドグラフに統合できるし、一つのシングルキューで並行タスクを管理できるやで。
- これで、マシンのコアを最低限の競合で有効活用できるやで。
※ 補足: 競合 … CPUのリソースをタスクが取り合うことを指す
- 他だとアーキテクチャの面でもメリットがあるんや
- このドライバで動的スケジューリングができるんやで、これで、ワイたちが色々やってきた改善の実装がやりやすくなるんや。
- あと個人的にSwiftで書けるのが嬉しいやで
- 実際に環境構築をして動かしてみよう
- Xcode 12 (試した環境)
swift-driver
のコード- なるべく新しめの Swift Trunk ToolChain(SnapShot)
- Clone する
$ git clone https://github.com/apple/swift-driver
$ cd swift-driver
swift build
でビルドする
# swift-driver にて
$ swift build
- ビルドされた
swift-driver
をswift
swiftc
という名前でシンボリックリンクを貼る
$ ln -s .build/debug/swift-driver swift
$ ln -s .build/debug/swift-driver swiftc
- 試しに
swift
を動かす
$ ./swift
error: failed to retrieve frontend target info
- 動かない
- 動かすには swift-frontend の情報が必要
$ /Library/Developer/Toolchains/{先程インストールしたToolChainのパス}/usr/bin/swift-frontend -frontend -print-target-info
{
"compilerVersion": "Apple Swift version 5.3-dev (LLVM 2685951d827b16e, Swift ec72db1d8ab63fa)",
"target": {
"triple": "x86_64-apple-macosx10.15",
"unversionedTriple": "x86_64-apple-macosx",
"moduleTriple": "x86_64-apple-macos",
"swiftRuntimeCompatibilityVersion": "5.1",
"compatibilityLibraries": [
{
"libraryName": "swiftCompatibility51",
"filter": "all"
}
],
"librariesRequireRPath": false
},
"paths": {
"runtimeLibraryPaths": [
"/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2020-09-22-a.xctoolchain/usr/lib/swift/macosx",
"/usr/lib/swift"
],
"runtimeLibraryImportPaths": [
"/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2020-09-22-a.xctoolchain/usr/lib/swift/macosx"
],
"runtimeResourcePath": "/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2020-09-22-a.xctoolchain/usr/lib/swift"
}
}
- Xcode 12 同封の Swift だと、
swift -frontend -print-target-info
で同様のものが出てくる - ただし、こちらは
swift-driver
では動かない
- 動かすには
swift-frontend
の情報が必要 - -> 環境変数
SWIFT_DRIVER_SWIFT_FRONTEND_EXEC
で先程のswift-frontend
を指定して run すれば良い
$ export SWIFT_DRIVER_SWIFT_FRONTEND_EXEC=/Library/Developer/Toolchains/{ToolChainのパス}/usr/bin/swift-frontend
$ ./swift
Type :help for assistance.
1>
- これでREPLが起動する
swiftc
も動く
$ ./swift test.swift
- Package.swift を直接開いて作業しても、なぜか Break Point が
main.swift
だけ効かない - 僕の環境だけかも知れない・・・・
- これでも無理だった。他の関数では行ける
swift package generate-xcodeproj
で xcodeproj 上で作業 + 上記設定だとうまく効く
- 出力されたもののうち、
paths
の情報にフォーカスしてみる
{
"runtimeLibraryPaths": [
"/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2020-09-22-a.xctoolchain/usr/lib/swift/macosx",
"/usr/lib/swift"
],
"runtimeLibraryImportPaths": [
"/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2020-09-22-a.xctoolchain/usr/lib/swift/macosx"
],
"runtimeResourcePath": "/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2020-09-22-a.xctoolchain/usr/lib/swift"
}
runtimeLibraryImportPaths
- "/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2020-09-22-a.xctoolchain/usr/lib/swift/macosx"
- CoreGraphics とか Foundation とかの
dylib
がある
- 要するに、CoreGraphics とか Foundation とかの
dylib
のパスを集めたりしている- リンカとかのジョブを生成するときに、適宜引き渡す
- LinkJob.swift のときに説明
- Sources/SwiftDriver/Jobs で定義されている
- 今日見るのは ...
- Job.swift
- LinkJob.swift
- Planning.swift
- どういうジョブかを表す
Job
構造体が定義されている
- ライブラリのリンクのジョブ を返す関数
- ***Job.swift というファイルはだいたいそのジョブを返す関数の実装
- ドライバ (
Driver
型) の状態から、適切なジョブ群を返すplanBuild()
の実装があるDriver
のextension
- ***Job.swift の中の関数を適宜呼び出してJobを作ったり
planBuild()
関数がDriver
の状態から適切なジョブ群を返すJob
方は、ジョブを表す構造体が定義されている- ***Job.swift というファイルはだいたいそのジョブを返す関数の実装
planBuild()
関数が呼ばれるのは?
- テストを除くと
main.swift
のみから呼び出されている
- ある程度読む材料が整ったので、
main.swift
を覗く swift-driver
の プログラムの始まり- ここをざっと読むと、ドライバの役割が何となく分かる
import SwiftDriverExecution
import SwiftDriver
import TSCLibc
import TSCBasic
import TSCUtility
var intHandler: InterruptHandler?
let diagnosticsEngine = DiagnosticsEngine(handlers: [Driver.stderrDiagnosticsHandler])
do {
let processSet = ProcessSet()
intHandler = try InterruptHandler {
processSet.terminate()
}
let (mode, arguments) = try Driver.invocationRunMode(forArgs: CommandLine.arguments)
if case .subcommand(let subcommand) = mode {
// We are running as a subcommand, try to find the subcommand adjacent to the executable we are running as.
// If we didn't find the tool there, let the OS search for it.
let subcommandPath = Process.findExecutable(arguments[0])?.parentDirectory.appending(component: subcommand)
?? Process.findExecutable(subcommand)
if subcommandPath == nil || !localFileSystem.exists(subcommandPath!) {
fatalError("cannot find subcommand executable '\(subcommand)'")
}
// Execute the subcommand.
try exec(path: subcommandPath?.pathString ?? "", args: arguments)
}
let executor = try SwiftDriverExecutor(diagnosticsEngine: diagnosticsEngine,
processSet: processSet,
fileSystem: localFileSystem,
env: ProcessEnv.vars)
var driver = try Driver(args: arguments,
diagnosticsEngine: diagnosticsEngine,
executor: executor)
// FIXME: The following check should be at the end of Driver.init, but current
// usage of the DiagnosticVerifier in tests makes this difficult.
guard !driver.diagnosticEngine.hasErrors else { throw Diagnostics.fatalError }
let jobs = try driver.planBuild()
try driver.run(jobs: jobs)
if driver.diagnosticEngine.hasErrors {
exit(EXIT_FAILURE)
}
} catch Diagnostics.fatalError {
exit(EXIT_FAILURE)
} catch let diagnosticData as DiagnosticData {
diagnosticsEngine.emit(.error(diagnosticData))
exit(EXIT_FAILURE)
} catch {
print("error: \(error)")
exit(EXIT_FAILURE)
}
- 比較的短い!
- 上から読んでいく
- まずははじめから
var intHandler: InterruptHandler?
let diagnosticsEngine = DiagnosticsEngine(handlers: [Driver.stderrDiagnosticsHandler])
do {
let processSet = ProcessSet()
intHandler = try InterruptHandler {
processSet.terminate()
}
...
var intHandler: InterruptHandler?
let diagnosticsEngine = DiagnosticsEngine(handlers: [Driver.stderrDiagnosticsHandler]) // これ
do {
let processSet = ProcessSet()
intHandler = try InterruptHandler {
processSet.terminate()
}
...
DiagnosticsEngine
- 見た感じ、error や warning といった診断の結果をハンドリングする
main.swift
だとDriver.stderrDiagnosticsHandler
を渡しているswift-driver
のコードではなく、apple/swift-tools-support-core のコード
public static let stderrDiagnosticsHandler: DiagnosticsEngine.DiagnosticsHandler = { diagnostic in
let stream = stderrStream
if !(diagnostic.location is UnknownLocation) {
stream <<< diagnostic.location.description <<< ": "
}
switch diagnostic.message.behavior {
case .error:
stream <<< "error: "
case .warning:
stream <<< "warning: "
...
- error とか warning を 標準エラー出力として出すようにハンドリングしている
var intHandler: InterruptHandler? // これ
let diagnosticsEngine = DiagnosticsEngine(handlers: [Driver.stderrDiagnosticsHandler])
do {
let processSet = ProcessSet()
intHandler = try InterruptHandler { // これ
processSet.terminate()
}
...
InterruptHandler
- 割り込みシグナル (
Ctrl + C
) 等のシグナルが走ったときのハンドラ main.swift
ではProcessSet
を terminate するようにハンドルしているswift-driver
のコードではなく、apple/swift-tools-support-core のコード
- 割り込みシグナル (
var intHandler: InterruptHandler?
let diagnosticsEngine = DiagnosticsEngine(handlers: [Driver.stderrDiagnosticsHandler])
do {
let processSet = ProcessSet() // これ
intHandler = try InterruptHandler {
processSet.terminate() // これ
}
...
ProcessSet
- 簡単に言えばプロセスのコレクション
Job
は最終的にこのコレクションにプロセスとして登録されるswift-driver
のコードではなく、apple/swift-tools-support-core のコード
- ジョブのプロセスとしてのハンドリング
- error、warning 等々 をエラー標準出力としてハンドリング
- 上記の前処理のようなことをやっている
- 次はここから読む
...
let (mode, arguments) = try Driver.invocationRunMode(forArgs: CommandLine.arguments)
if case .subcommand(let subcommand) = mode {
// We are running as a subcommand, try to find the subcommand adjacent to the executable we are running as.
// If we didn't find the tool there, let the OS search for it.
let subcommandPath = Process.findExecutable(arguments[0])?.parentDirectory.appending(component: subcommand)
?? Process.findExecutable(subcommand)
if subcommandPath == nil || !localFileSystem.exists(subcommandPath!) {
fatalError("cannot find subcommand executable '\(subcommand)'")
}
// Execute the subcommand.
try exec(path: subcommandPath?.pathString ?? "", args: arguments)
}
...
...
let (mode, arguments) = try Driver.invocationRunMode(forArgs: CommandLine.arguments) // ここ
if case .subcommand(let subcommand) = mode {
// We are running as a subcommand, try to find the subcommand adjacent to the executable we are running as.
// If we didn't find the tool there, let the OS search for it.
let subcommandPath = Process.findExecutable(arguments[0])?.parentDirectory.appending(component: subcommand)
?? Process.findExecutable(subcommand)
if subcommandPath == nil || !localFileSystem.exists(subcommandPath!) {
fatalError("cannot find subcommand executable '\(subcommand)'")
}
// Execute the subcommand.
try exec(path: subcommandPath?.pathString ?? "", args: arguments)
}
...
swift
かswiftc
で呼び出しているのか、どんなオプションが付いているか、などで Driver のモードを変化- 例えば、ここで REPL かどうかも判定している
- サブコマンドを見るモードを返すこともある
- 例)
swift build
とかはサブコマンドを見る対象になる
...
let (mode, arguments) = try Driver.invocationRunMode(forArgs: CommandLine.arguments)
if case .subcommand(let subcommand) = mode { // ここ
// We are running as a subcommand, try to find the subcommand adjacent to the executable we are running as.
// If we didn't find the tool there, let the OS search for it.
let subcommandPath = Process.findExecutable(arguments[0])?.parentDirectory.appending(component: subcommand)
?? Process.findExecutable(subcommand)
if subcommandPath == nil || !localFileSystem.exists(subcommandPath!) {
fatalError("cannot find subcommand executable '\(subcommand)'")
}
// Execute the subcommand.
try exec(path: subcommandPath?.pathString ?? "", args: arguments)
}
...
-
サブコマンドを探索する。
-
見つからなければ error
fatalError("cannot find subcommand ...
-
見つかったらサブコマンドを呼び出す
try exec(path:, args:)
swift-driver
がどのように呼ばれるか調べるswift
orswiftc
orswift
+ サブコマンド
- サブコマンドがあれば、そのサブコマンドを探して呼び出す
...
let executor = try SwiftDriverExecutor(diagnosticsEngine: diagnosticsEngine,
processSet: processSet,
fileSystem: localFileSystem,
env: ProcessEnv.vars)
var driver = try Driver(args: arguments,
diagnosticsEngine: diagnosticsEngine,
executor: executor)
// FIXME: The following check should be at the end of Driver.init, but current
// usage of the DiagnosticVerifier in tests makes this difficult.
guard !driver.diagnosticEngine.hasErrors else { throw Diagnostics.fatalError }
let jobs = try driver.planBuild()
try driver.run(jobs: jobs)
...
...
// ここ
let executor = try SwiftDriverExecutor(diagnosticsEngine: diagnosticsEngine,
processSet: processSet,
fileSystem: localFileSystem,
env: ProcessEnv.vars)
var driver = try Driver(args: arguments,
diagnosticsEngine: diagnosticsEngine,
executor: executor)
// FIXME: The following check should be at the end of Driver.init, but current
// usage of the DiagnosticVerifier in tests makes this difficult.
guard !driver.diagnosticEngine.hasErrors else { throw Diagnostics.fatalError }
let jobs = try driver.planBuild()
try driver.run(jobs: jobs)
...
DriverExecutor
プロトコルに準拠してる- ジョブを動かすことがメインの働き
- さっきの
ProcessSet
とかDiagnosticsEngine
、環境変数のデータなどを渡している- ジョブをプロセスにして
ProcessSet
に渡したり、error をDiagnosticsEngine
で ハンドリングしたりしているっぽい
- ジョブをプロセスにして
...
let executor = try SwiftDriverExecutor(diagnosticsEngine: diagnosticsEngine,
processSet: processSet,
fileSystem: localFileSystem,
env: ProcessEnv.vars)
// ここ
var driver = try Driver(args: arguments,
diagnosticsEngine: diagnosticsEngine,
executor: executor)
// FIXME: The following check should be at the end of Driver.init, but current
// usage of the DiagnosticVerifier in tests makes this difficult.
guard !driver.diagnosticEngine.hasErrors else { throw Diagnostics.fatalError }
let jobs = try driver.planBuild()
try driver.run(jobs: jobs)
...
- 言わずもがなドライバ本体
- イニシャライザで、ジョブ実行に必要な
ProcessSet
や error ハンドリングのためのDiagnosticsEngine
を渡す - ドライバに渡されたオプションも渡す (
arguments
)
...
let executor = try SwiftDriverExecutor(diagnosticsEngine: diagnosticsEngine,
processSet: processSet,
fileSystem: localFileSystem,
env: ProcessEnv.vars)
var driver = try Driver(args: arguments,
diagnosticsEngine: diagnosticsEngine,
executor: executor)
// FIXME: The following check should be at the end of Driver.init, but current
// usage of the DiagnosticVerifier in tests makes this difficult.
guard !driver.diagnosticEngine.hasErrors else { throw Diagnostics.fatalError }
// ここ
let jobs = try driver.planBuild()
try driver.run(jobs: jobs)
...
- 渡されたオプション(イニシャライザで渡した
arguments
)をもとに、さっき説明したplanBuild()
でジョブ群を生成する
...
let executor = try SwiftDriverExecutor(diagnosticsEngine: diagnosticsEngine,
processSet: processSet,
fileSystem: localFileSystem,
env: ProcessEnv.vars)
var driver = try Driver(args: arguments,
diagnosticsEngine: diagnosticsEngine,
executor: executor)
// FIXME: The following check should be at the end of Driver.init, but current
// usage of the DiagnosticVerifier in tests makes this difficult.
guard !driver.diagnosticEngine.hasErrors else { throw Diagnostics.fatalError }
let jobs = try driver.planBuild()
// ここ
try driver.run(jobs: jobs)
...
- 生成されたジョブを実行する
- Swift のドライバはオプションでどういうジョブを実行するかを決める
- ジョブはプロセスとして動かされる
if driver.diagnosticEngine.hasErrors {
exit(EXIT_FAILURE)
}
} catch Diagnostics.fatalError {
exit(EXIT_FAILURE)
} catch let diagnosticData as DiagnosticData {
diagnosticsEngine.emit(.error(diagnosticData))
exit(EXIT_FAILURE)
} catch {
print("error: \(error)")
exit(EXIT_FAILURE)
}
- 後は
try catch
のエラーハンドリングだけです - 説明しなくてもだいたいわかると思います
- とりあえず好評だったら Vol.2 やります
- @kateinoigakukun san
- ちなみに彼はすでにContributeしています
- What is a driver, for example, a compiler driver? - Quora
- Introduction - The LLVM Compiler Driver (llvmc)
- コンパイラは何をしているのか - 大瀧 研究室 茨城大学
- apple/swift-driver
- Harlan Haskins - Twitter
- Javi - Twitter