Swift Digest
ST-0002 | Swift Evolution

ツール連携のための安定したJSONベースのABI

A stable JSON-based ABI for tools integration

Proposal
ST-0002
Authors
Jonathan Grynspan
Status
Implemented (Swift 6.0)

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

Swift Testing は、Xcode・VS Code・コマンドラインなどさまざまなツールから利用されることを前提に設計されています。Swift Testing は Swift がサポートするすべてのプラットフォームでオープンソース化されており、Swift ツールチェインに同梱される形でも、Swift Package Manager のパッケージ依存として追加される形でも使えます。

このような多様な使われ方をするため、ツールとの連携には安定したインターフェースが必要です。とくに次のようなパターンへの対応が求められていました。

  • Swift Testing を直接ビルド・リンクする IDE(例: Xcode 16)。IDE 側のコピーとテスト側のコピーが同一なら問題ありませんが、テスト側がパッケージ依存として別バージョンの Swift Testing をリンクしている場合、両者は別のバイナリになるため、内部シンボルに依存した連携はできません。

  • Swift Testing にリンクできない IDE(例: VS Code)。VS Code は TypeScript で書かれているため、Swift のシンボルに直接アクセスできません。テスト実行を構成・起動し、「テストが開始された」「issue が記録された」といったイベントを受け取るには、Swift シンボルに頼らない言語非依存の経路が必要です。

つまり、Swift Testing 側のバージョンや内部実装が変わっても壊れない、入出力フォーマットと呼び出し口の両方を提供しなければなりません。Swift Testing の普及はツール連携の充実に大きく依存しており、これを欠くと「Xcode 以外では Swift Testing を快適に使えない」「サードパーティのツールが人間向けのコマンドライン出力を無理にパースする」といった状況に陥ってしまいます。

02 どのように解決されるのか

ツール連携のために、Swift Testing に安定した ABI を導入します。具体的には次の2つを定義します。

  • 動的ローダ(Darwin・Linux・Windows)から実行時に解決できる安定したエントリポイント関数。シグネチャは将来にわたって変更されず、入力を受け取り、出力を非同期に返します。
  • ツールが解釈できる安定した JSON ベースのスキーマ。入力(テスト構成・オプション)と出力(イベント)の両方に用います。

本 Proposal では出力側の JSON スキーマを定義し、入力側の JSON スキーマは後続の Proposal で扱います。

JSON スキーマによる出力

出力は、改行区切りの JSON オブジェクト列(JSON Lines)として、ファイルや名前付きパイプ、あるいはイベントハンドラに書き出されます。

テスト実行が始まると、まず計画されたテストの一覧が kind: "test" のレコードとして書き出されます。

{
  "kind": "test",
  "payload": {
    "displayName": "Can get stdout",
    "id": "TestingTests.FileHandleTests/canGetStdout()/FileHandleTests.swift:33:4",
    "isParameterized": false,
    "kind": "function",
    "name": "canGetStdout()",
    "sourceLocation": {
      "column": 4,
      "fileID": "TestingTests/FileHandleTests.swift",
      "line": 33
    }
  },
  "version": 0
}

その後、テスト実行中の各種イベント(テスト開始・issue 記録・テスト終了など)が kind: "event" のレコードとして順次書き出されます。

{
  "kind": "event",
  "payload": {
    "instant": { "absolute": 266418.545786299, "since1970": 1718302639.76747 },
    "kind": "testStarted",
    "messages": [
      { "symbol": "default", "text": "Test \"Can get stdout\" started." }
    ],
    "testID": "TestingTests.FileHandleTests/canGetStdout()/FileHandleTests.swift:33:4"
  },
  "version": 0
}

{
  "kind": "event",
  "payload": {
    "instant": { "absolute": 266636.524236724, "since1970": 1718302857.74857 },
    "issue": {
      "isKnown": false,
      "sourceLocation": {
        "column": 7, "fileID": "TestingTests/FileHandleTests.swift", "line": 29
      }
    },
    "kind": "issueRecorded",
    "messages": [
      { "symbol": "fail", "text": "Expectation failed: (EOF → -1) == (feof(fileHandle) → 0)" }
    ],
    "testID": "TestingTests.FileHandleTests/canGetStdout()/FileHandleTests.swift:33:4"
  },
  "version": 0
}

各イベントには 0 個以上の messages が含まれ、ツールが利用者向けに表示するための人間可読なテキストと、抽象化されたシンボル名(faildefault など)が入ります。これは Swift Testing 自身が標準エラーに書き出す出力に対応するもので、ツールは独自の UI に合わせて表示できます。

詳細な JSON スキーマは Swift Testing リポジトリの Documentation/ABI/JSON.md で定義されています。

コマンドラインからの利用

swift test に、ツール連携用の引数が3つ追加されます。引数名は Swift Package Manager 側のレビューで変わる可能性があります。

引数 値の型 説明
--configuration-path パス テスト構成・オプションを記述したファイルや名前付きパイプのパス
--event-stream-output-path パス 出力イベントを書き出す先のファイルや名前付きパイプのパス
--event-stream-version 整数 出力に使う安定 JSON スキーマのバージョン。本 Proposal の内容は 0

--event-stream-output-path を指定するとテスト実行中のイベントが JSON Lines として書き出されます。Darwin・Linux では mkfifo()、Windows では CreateNamedPipe() で作成した名前付きパイプを指定すれば、ファイルクローズを待たずにライブで結果を受け取れます。--configuration-path--no-parallel のような明示的なコマンドライン引数の両方が指定された場合は、明示的な引数が優先されます。

--event-stream-output-path のみを指定して --event-stream-version を省略した場合、現状は Swift Testing 内部型を直接 JSON エンコードした不安定なフォーマットになります。これは Xcode 16 Beta 1 を支えるための過渡的な挙動で、将来的には最新の安定スキーマがデフォルトになる予定です。ツールの作者は破壊的変更を避けるため、明示的にバージョンを指定することが推奨されます。

Swift からの利用

Swift コードから直接エントリポイントを呼び出すために、@_spi(ForToolsIntegrationOnly)ABIv0 という enum が公開されます。ABIv0.entryPoint プロパティで取得できる関数は、JSON でエンコードされた構成と、各レコードを受け取るコールバックを引数に取り、テスト実行が成功したかを返します。

@_spi(ForToolsIntegrationOnly)
public enum ABIv0 {
    public typealias EntryPoint = @convention(thin) @Sendable (
        _ configurationJSON: UnsafeRawBufferPointer?,
        _ recordHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void
    ) async throws -> Bool

    public static var entryPoint: EntryPoint { get }
}

入出力の型を Data ではなく UnsafeRawBufferPointer にしているのは、Data が Foundation の型であり、Swift Testing が Foundation に公開依存することを避けるためです。これにより、将来 Foundation 自身が Swift Testing を採用しやすくなります。

C・C++ からの利用

Swift シンボルに直接リンクできないツールでも使えるよう、ABIv0.entryPoint の getter が C・C++ から呼べるシンボルとしてエクスポートされます。

extern "C" const void *_Nonnull swt_abiv0_getEntryPoint(void);

呼び出し側は dlsym()(POSIX)や GetProcAddress()(Windows)でこのシンボルを実行時に解決し、戻り値を unsafeBitCast(_:to:) で Swift の関数型に戻して呼び出します。データポインタから関数ポインタへの変換が許されないプラットフォーム(C 標準 §6.3.2.3, §J.5.7)ではこの操作はサポートされません。

なお、Swift Testing をパッケージ依存として組み込むとメイン実行可能ファイルに静的リンクされます。ELF を用いる Linux などでは、リンカに --export-dynamic を渡さないと実行時にメイン実行可能ファイルのシンボル情報が取得できないことがあるため、ツール側で注意が必要です。

03 今後の見通し

JSON ベースの ABI については、いくつかの拡張方向が示されています。いずれも将来の構想であり、実現を約束するものではありません。

  • 入力側の JSON スキーマ定義。本 Proposal では出力スキーマのみを定義していますが、テスト構成やオプションも安定した JSON スキーマで表現できるよう、後続の Proposal で扱う予定とされています。
  • イベント情報の拡充#expect() で不一致になった具体的な値など、より豊富な情報を JSON に含める方向が検討されています。データ構造を効率的かつ明確に設計する必要があるため、慎重に進めるとされています。
  • メッセージのリッチテキスト化。イベントの messages に Markdown などのフォーマットを取り入れ、値の強調・コードボイス・アクセシビリティ向上などに活かすアイデアがあります。
  • 追加のエントリポイント。Swift 関数とコマンドラインの2系統で多くのユースケースをカバーできる見込みですが、将来的には純粋な C や Objective-C のインターフェース、WebAssembly や JavaScript の async 互換インターフェース、プラットフォーム固有のインターフェース、Rust・Go・C# など他言語への直接バインディングといった、別の呼び出し口を追加する可能性も挙げられています。