Swift Digest
SE-0492 | Swift Evolution

Section Placement Control

Proposal
SE-0492
Authors
Kuba Mracek
Review Manager
Doug Gregor
Status
Implemented (Swift 6.3)

01 何が問題だったのか

テスティングフレームワークやプラグイン機構、システム/組み込み開発の世界では、「ビルド時にコード側でメタデータを準備しておき、実行時にそれをまとめて発見・列挙する」必要のある場面が数多くあります。たとえばテストフレームワークは、ユーザーが定義したテストの一覧を実行時に拾い集めなければなりません。

Objective-C のような動的な言語では、ランタイムにクラス一覧を問い合わせて該当する型を探す、という方法が取れます。

// テスティングフレームワーク側の擬似コード
let classList = objc_copyClassList(...)
for aClass in classList {
  if aClass is XCTestCase {
    let methodList = class_copyMethodList(aClass)
    ...
  }
}

しかし Swift にとって、こうした「言語ランタイム頼み」のアプローチは必ずしも適切ではありません。Swift では、ビルド時に決まる情報はビルド時に成果物(バイナリ)へ書き出してしまい、実行時はプラットフォームのローダ API やオフラインのバイナリ解析ツール(objdump、otool など)で直接読めるほうが効率的で、Embedded Swift のように言語ランタイムに頼れない環境でも使えます。

バイナリのセクションを使いたいユースケース

オブジェクトファイル(Mach-O / ELF / COFF / Wasm など)のセクションは、こうした「ビルド時に書き出してまとめて回収する」用途と相性のよい仕組みです。同じ名前のセクションに置かれたシンボルはリンカがひとまとまりに配置してくれるので、これを使えば次のような用途を統一的に扱えます。

  • テストの発見:各モジュールがテストメタデータを既知のセクションに書き出しておけば、実行時にそのセクションをスキャンするだけでテスト一覧を構築できます。
  • プラグインシステムdlopen で読み込んだモジュール内のプラグイン情報を、dlsym や危険なポインタキャストではなく、セクション内の構造化データとして安全に取り出せます。
  • linker set:複数のソースから情報を分散して書き出し、リンク時に集約する伝統的なシステムプログラミング手法。ブート時の各サブシステムのメモリ要件記述などに使われます。
  • デバッガ向けメタデータ@DebugDescription マクロが生成するサマリ文字列を専用セクションに置き、LLDB がランタイム評価なしで読めるようにする使い方。Embedded ではこれが唯一のリッチな表示手段になることもあります。
  • 起動規約(startup contract)への準拠:プラットフォームのリンカスクリプトやハードウェアが「特定のセクションに特定のデータ構造を置くこと」を要求するケース。これまでは Swift で書けず、C/C++ に逃げる必要がありました。

既存の Swift では書けなかった

Swift にはこれまで、グローバル変数を任意のセクションに配置したり、未参照に見えるシンボルが dead code elimination(dead-strip)で消されないようにする手段が、surface syntax として存在しませんでした。コンパイラ内部にはアンダースコア付きの @_section / @_used という非公式な属性がありましたが、ユーザーが安心して使える形にはなっていませんでした。

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

グローバル変数や static メンバー変数に付けられる、@section@used という2つの属性を導入します。@section はその変数のストレージシンボルを指定したセクションへ配置し、@used はそのシンボルが dead-strip されないようにします。

// セクションに配置し、dead-strip しないようマークする。
// 初期化式は constant expression でなければならない。
// この変数は暗黙に static initialization される。
@section("__DATA,mysection")
@used
let myLinkerSetEntry: Int = 42 // OK

// 定数でない式や、static initialization 不可能な式は不可
@section("__DATA,mysection")
let bad: Int = Int.random(in: 0 ..< 10) // error

// var でも使えるが、初期化式は依然として定数式が必要
@section("__DATA,mysection")
var mutableVariable: Int = 42 // OK

@section@used は独立した属性で、どちらか一方だけ付けることもできます。@section だけ付けた場合は「セクションには置くが、参照されなければ dead-strip されてもよい」、@used だけ付けた場合は「セクション配置は通常通りだが、参照されなくても残す」という意味になります。@used を分けてあるのは、たとえばライブラリ内の大きなデータテーブルを「バイナリサイズ集計のために専用セクションへ置きたいが、未使用なら消えてほしい」というケースに対応するためです。

使える場所の制約

@section / @used は、以下の条件をすべて満たす変数にだけ付けられます。

  • グローバル変数または static メンバー変数であること(ローカル変数や非 static メンバーは不可)。
  • ジェネリックな文脈の中に置かれていないこと(ジェネリック型の中、ジェネリック型にネストされた型の中はいずれも不可)。
  • stored property であること(computed property は不可)。
  • didSet / willSet といったプロパティ監視子を持たないこと。
  • 初期化式が constant expression であり、かつ static initialization が可能であること。
@section("__DATA,mysection") @used
var computed: Int { return 42 } // error: computed property には付けられない

struct MyStruct {
  @section("__DATA,mysection") @used
  static let staticMemberLet = 42 // OK

  @section("__DATA,mysection") @used
  let member = 42 // error: 非 static メンバーには付けられない
}

struct MyGenericStruct<T> {
  @section("__DATA,mysection") @used
  static let staticMember = 42 // error: ジェネリックな文脈では使えない
}

これらの制約はすべて、「対象の変数が、リンカから見てちょうど1つの static initialization 済みストレージシンボルとして実体化される」という条件を保証するためのものです。

@section の効果

@section が付くと、初期化式はコンパイル時に定数畳み込みされ、その値がそのままストレージシンボルの初期値として埋め込まれます。つまりその変数は実行時のレイジー初期化を経由せず、ワンタイム初期化用のヘルパーやトークンも生成されません。main.swift のトップレベルで宣言した場合も、通常のトップレベル変数のような順次初期化は行われません。

セクション名はコンパイラが内容を検証せず、LLVM IR レベルでそのままセクション指定子として渡されます。そのため、リンカやアセンブラ、最適化器がそのセクション名から導く特別な振る舞いはそのまま反映されます。

ただし、これらはあくまでストレージシンボルに対する効果です。コンパイラは let 変数の値を利用箇所にコピー伝搬する最適化などを引き続き行えるので、「セクションに置いたから値がそのセクションの外に漏れない」という保証ではありません。

@used の効果

@used が付いた変数のストレージシンボルは「dead-strip 禁止」とマークされます。アクセス制御上は外から見えないシンボル(例: 何もないと思われている型の private static メンバーなど)でも、最適化で除去されなくなります。

constant expression の定義

@section の初期化式に使える constant expression は、本提案で次のように「最小限のかたち」で定義されます。今後 Swift の compile-time プログラミング機能の進化に合わせて拡張されていく前提です。

  • 標準の整数型(Int / UInt / Int8 ~ Int128 / UInt8 ~ UInt128)の整数リテラル
  • Float / Double の浮動小数点リテラル
  • Bool のブールリテラル
  • 非ジェネリック関数への直接の名前による参照(関数自体も、それを囲む文脈もジェネリックでないこと)
  • キャプチャを持たず、ジェネリックな文脈にもないクロージャリテラル
  • 非ジェネリックかつ非 resilient な型への、型名による直接のメタタイプ参照
  • 各要素がいずれも constant expression であるタプル
  • 各要素がいずれも constant expression である InlineArray の配列リテラル

逆に、演算子やユーザー定義型、String / Dictionary / Set などの標準型、キャプチャ付きクロージャ、変数を名前で参照する式などは現時点では使えません。

@section("...") let a = 42                                     // OK
@section("...") let b = 3.14                                   // OK
@section("...") let c = 1 + 1                                  // error: 演算子は不可
@section("...") let d = Int.max                                // error: リテラルでない
@section("...") let e: UInt8 = 42                              // OK
@section("...") let f = UInt8(42)                              // error: リテラルでない

@section("...") let composition1 = (1, 2, 3, 2.718, true)      // OK
@section("...") let composition2 = (1, 2, Int.max)             // error: 要素が定数でない

func foo() -> Int { return 42 }
@section("...") let func1 = foo                                // OK: 関数参照
@section("...") let func2 = foo()                              // error: 関数呼び出しは不可

struct S { }
@section("...") let metatype1 = S.self                         // OK
import Foundation
@section("...") let metatype4 = URL.self                       // error: resilient な型は不可

関数値(クロージャや関数参照)の場合、最終的なポインタ値はリンク時または実行時のロードまで決まりませんが、static initialization の観点ではリンカ/ローダによる通常のリロケーションで解決される関数ポインタとして扱われます。

func foo() { ... }

@section("__DATA,mysection")
let a = (42, foo) // foo はリンク可能な関数ポインタとして
                  // 静的に初期化される

なお、constant expression であることと static initialization 可能であることは将来的に別概念になり得ます。たとえば将来、辞書リテラルが定数として認められたとしても、ハッシュシードのランダム化のため、その辞書をそのままセクションに置くことはできない可能性があります。@section が要求するのはより強い「static initialization 可能」のほうです。

オブジェクトファイル形式ごとの違いを書き分ける

セクション名の規約はオブジェクトファイル形式ごとに異なるため、ポータブルなコードでは形式ごとに書き分ける必要があります。OS で分岐できる場合は #if os(...) でも構いませんが、Embedded Swift のようにベアメタル向けで OS が存在しないケースも想定し、新しい条件付きコンパイル指示子 #if objectFormat(...) が追加されます。指定できる値は現在のところ COFF / ELF / MachO / Wasm です(大文字小文字を区別)。

#if objectFormat(MachO)
@section("__DATA_CONST,mysection")
#elseif objectFormat(ELF)
@section("mysection")
#endif
let value = ...

このような書き分けの煩雑さは、通常はマクロで隠蔽されることが想定されています。アプリケーション側のコードは @section / @used を直接使うのではなく、@RegisterPlugin のような高レベルなマクロ越しに使うのが期待されている使い方です。

ELF 向けの section index

ある名前のセクションに置かれたシンボルの開始・終端アドレスは、リンカが提供する encapsulation symbol(ELF/Wasm の __start_<name> / __stop_<name>、Mach-O の section$start$<segname>$<secname> / section$end$...、COFF のグループ化セクションを使った構成)から取得できます。動的リンクの場合は、Mach-O や COFF ではイメージヘッダがアドレス空間に存在し、ローダ API でモジュールごとのセクション境界を取れます。

ところが ELF では、動的リンク時にセクション境界がアドレス空間に出てこないことが普通で、複数モジュールを実行時に横断する手段が標準では存在しません。これを解決するため、Swift コンパイラは ELF 向けにビルドする際、カスタムセクションを使っている各コンパイルから「section index」と呼ぶ情報を swift5_sections という専用セクションに出力します。各エントリは次のような構造です。

struct SectionIndexEntry {
  const char *name;
  const void *start; // 実質的に __start_<name>
  const void *stop;  // 実質的に __stop_<name>
};

エントリは linkonce_odr 扱いなのでリンク後はセクション名ごとに1つにまとまります。Swift ランタイム側のヘルパー(ELF では swiftrt.o が常に各モジュールにリンクされます)がこの index を辿ってセクション境界を登録することで、ELF + 動的リンクという厳しい組み合わせでも実行時にセクション内容へ到達できるようになります。

想定される使い方の方向性

提案の本体はあくまでビルド時の仕組みで、「セクションに置かれたデータを実行時にどう列挙するか」は別途必要になります。提案では、現実的にはこの後続として次のような Future Directions が示されています。いずれも本提案のスコープ外で、実現を約束するものではありません。

  • 関数のセクション配置:起動コードを特定のセクションに置きたいといった用途のため、@section / @used を関数宣言にも拡張する方向性。
  • constant expression / 定数値の一般化@section のために定義した最小限の constant expression を、@const を含む compile-time プログラミング機能の一部として広げていく方向性(別途 [Swift Compile-Time Values] のピッチが進行中)。
  • セクション名に定数文字列の参照を許す:現在は文字列リテラルしか書けないセクション名を、コンパイル時定数の変数で書けるようにし、繰り返しを減らす方向性。
  • クロスプラットフォームなセクション読み出し API:オブジェクトファイル形式・リンクの種類・OS を抽象化した、ランタイム/標準ライブラリ/外部パッケージとしての低レベル API。
  • linker set / プラグインの高レベル API:マクロベースで型安全な @DefineLinkerSet / @LinkerSetEntry / @PluginRecord のような仕組みを構築する方向性。
  • 安定したアドレスへのアクセス:static initialization された変数は実体としてはバイナリ上に固定アドレスを持つので、#address(x) のような形でその安定アドレスを取り出せるようにする方向性。

注意

@section / @used は低レベルな道具で、通常のアプリケーションコードから直接使われることは想定されていません。テスト発見やプラグイン、デバッガ向けメタデータといったユースケースを実装する側のライブラリやマクロが内部で利用し、それを高レベルな API として提供する、という階層関係を想定して設計されています。