Skip to content

Instantly share code, notes, and snippets.

@freddi-kit
Last active September 24, 2020 16:38
Show Gist options
  • Save freddi-kit/a2d46369e91354087258bb3488b6f8ff to your computer and use it in GitHub Desktop.
Save freddi-kit/a2d46369e91354087258bb3488b6f8ff to your computer and use it in GitHub Desktop.
今日からはじめる `swift-driver`

slidenumbers: true autoscale: true

今日からはじめる swift-driver

わいわいswiftc #23 25nd/Sep/2020

@___freddi___


本日の話の流れ・趣旨

  • コンパイラの「ドライバ」について知る
  • swift-driver の詳細について知る
    • 実際に コード読ませながら進行します。
    • わかんなくなったら遠慮せず、適宜止めてください

「ドライバ」とは?


ドライバのわかりやすかった説明 at Quora

  • 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.
    • ソフトウェア開発の世界やと一般的には、ドライバという言葉はいくつかのパーツになっているソフトウェアを参照したり、呼び出したりするソフトウェアのことを指すやで

ドライバのわかりやすかった説明 ソフトウェア編 LLVMの例

  • 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のツール群にアクセスできる、一つのアクセスポイントとして振る舞うやで

llvmc の公式の説明を読む

  • 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 の例

  • みなさんがコンパイラだと思っている gccは、 実は、コンパイラドライバ(compiler driver)と呼ばれるプログラムであり、 Cなどのプログラミング言語で書かれたソースプログラムから実行形式を 作り出すための処理を行う。
  • gccは、「必要に応じてコンパイラや アセンブラ、リンケージエディタなどのプログラムを呼び出す」という 処理を行っている。gccコマンド自体が直接コンパイルを行っているわけではない。
    • 「コンパイル」と呼ばれている処理は cc1 という別のコマンドがやっている

ここまで説明したので「コンパイラの世界のドライバ」まとめ

  • ドライバは、コンパイラ及びその関連ツール(Optimizer・リンカなど)を呼び出すツール
  • gccllvmcswift と言ったコマンドは、ドライバであって、コンパイラ自身ではない

Swiftのドライバについて

  • swift-frontend コマンドからシンボリックリンクで swiftswiftc を作っている
    • のちのち、とある分岐でこのシンボリックリンクの名前がドライバとしての挙動の分岐になるが後述
    • ちなみに、ドライバが呼び出す仕事の単位(リンクなど)をジョブ と呼ばれる
  • main/lib/Driver にドライバのコードが有る

swift-driver を探索

  • 今回は C++ で書かれた本体の Driver ではなく、Swiftで書かれた swift-driver を見る

swift-driver について

inline fit


swift-driver のSwiftPMでの利用

inline


swift-driver のSwiftPMでの利用


swift-driver のSwiftPMでのメリット

inline

  • アホみたいな質問かもしれへんけど、これ使う良い点何?

swift-driver のSwiftPMでのメリット

inline

  • アホみたいな質問じゃないで。一つの大きな利点は、このドライバはSwiftPMの外側のビルドシステムからは不透明な、インクリメンタルビルドのための内部ビルドシステムを備えているやで
  • leading to oversubscribing the machine when (say) building lots of targets in parallel. がよくわからん

swift-driver のSwiftPMでのメリット

inline

  • このドライバ使うと、SwiftPM はドライバのビルドタスクを自身のビルドグラフに統合できるし、一つのシングルキューで並行タスクを管理できるやで。
  • これで、マシンのコアを最低限の競合で有効活用できるやで。

※ 補足: 競合 … CPUのリソースをタスクが取り合うことを指す


swift-driver のSwiftPMでのメリット

inline

  • 他だとアーキテクチャの面でもメリットがあるんや
  • このドライバで動的スケジューリングができるんやで、これで、ワイたちが色々やってきた改善の実装がやりやすくなるんや。

swift-driver のSwiftPMでのメリット

inline

  • あと個人的にSwiftで書けるのが嬉しいやで

swift-driver を触る

  • 実際に環境構築をして動かしてみよう

swift-driver の環境構築に必要なもの


swift-driver をビルドする

  • Clone する
$ git clone https://github.com/apple/swift-driver
$ cd swift-driver

swift-driver をビルドする (swift build)

  • swift build でビルドする
# swift-driver にて
$ swift build

swift-driver をビルドする (swift build)

  • ビルドされた swift-driverswift swiftc という名前でシンボリックリンクを貼る
$ ln -s  .build/debug/swift-driver swift
$ ln -s  .build/debug/swift-driver swiftc

swift-driver を起動する (swift build)

  • 試しに swift を動かす
$ ./swift
error: failed to retrieve frontend target info
  • 動かない

swift-driver を起動する (swift build)

  • 動かすには 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"
  }
}

swift-frontend の情報

  • Xcode 12 同封の Swift だと、swift -frontend -print-target-info で同様のものが出てくる
  • ただし、こちらは swift-driver では動かない

swift-driver を起動する (swift build)

  • 動かすには swift-frontend の情報が必要
  • -> 環境変数 SWIFT_DRIVER_SWIFT_FRONTEND_EXEC で先程の swift-frontend を指定して run すれば良い

swift-driver を起動する (swift build)

$ 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

swift-driver を Debug する

  • Package.swift を直接開いて作業しても、なぜか Break Point が main.swift だけ効かない
  • 僕の環境だけかも知れない・・・・
  • これでも無理だった。他の関数では行ける

inline 80%

  • swift package generate-xcodeprojで xcodeproj 上で作業 + 上記設定だとうまく効く

swift-frontend はなぜ必要?

  • 出力されたもののうち、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"
}

swift-frontend はなぜ必要?

  • runtimeLibraryImportPaths
    • "/Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2020-09-22-a.xctoolchain/usr/lib/swift/macosx"
    • CoreGraphics とか Foundation とかの dylib がある

inline


swift-frontend はなぜ必要?

  • 要するに、CoreGraphics とか Foundation とかの dylib のパスを集めたりしている
    • リンカとかのジョブを生成するときに、適宜引き渡す
    • LinkJob.swift のときに説明

swift-driver のジョブについて

inline


swift-driver のジョブについて

  • 今日見るのは ...
    • Job.swift
    • LinkJob.swift
    • Planning.swift

  • どういうジョブかを表す Job 構造体が定義されている

  • ライブラリのリンクのジョブ を返す関数
  • ***Job.swift というファイルはだいたいそのジョブを返す関数の実装

  • ドライバ (Driver 型) の状態から、適切なジョブ群を返す planBuild() の実装がある
    • Driverextension
  • ***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 等々 をエラー標準出力としてハンドリング
  • 上記の前処理のようなことをやっている

Driver.invocationRunMode ~ try exec(...

  • 次はここから読む
...

  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)
  }

...

Driver.invocationRunMode(forArgs:)

...

  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)
  }

...

Driver.invocationRunMode(forArgs:)

  • swiftswiftc で呼び出しているのか、どんなオプションが付いているか、などで Driver のモードを変化
    • 例えば、ここで REPL かどうかも判定している
    • サブコマンドを見るモードを返すこともある
    • 例) swift build とかはサブコマンドを見る対象になる

if case .subcommand(let subcommand) = ...

...

  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)
  }

...

if case .subcommand(let subcommand) = ...

  • サブコマンドを探索する。

  • 見つからなければ error

    • fatalError("cannot find subcommand ...
  • 見つかったらサブコマンドを呼び出す

    • try exec(path:, args:)

ここまでのまとめ

  • swift-driver がどのように呼ばれるか調べる
    • swift or swiftc or swift + サブコマンド
  • サブコマンドがあれば、そのサブコマンドを探して呼び出す

最後のここの部分

...
  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)
...

[try driver.run(jobs: jobs)]

  • 生成されたジョブを実行する

ここまでのまとめ

  • Swift のドライバはオプションでどういうジョブを実行するかを決める
  • ジョブはプロセスとして動かされる

main.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 やります

Special Thanks

  • @kateinoigakukun san
    • ちなみに彼はすでにContributeしています

inline 80%


参考文献


参考文献

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment