関数向けの @instrumentation 属性
@instrumentation attribute for functions
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
サニタイザ(sanitizer)は、プログラムの不正な動作を実行時に検出するための仕組みです。代表的なものに、不正なメモリアクセスを検出する ASan(AddressSanitizer)や、データ競合を検出する TSan(ThreadSanitizer)があります。これらはコンパイラがコードに計装(instrumentation)を挿入し、たとえば shadow memory と呼ばれる領域に「そのアドレスがアクセスして良い状態か」を記録しておくことで、メモリアクセスのたびに正しさを検証します。
この計装は強力ですが、常に望ましいとは限りません。
特殊なメモリ領域での誤検知
Embedded 環境などでは、MMIO(メモリマップド I/O)や commpage のような特殊なメモリ領域を扱います。サニタイザはこうした領域の状態を正しく追跡できないことがあり、本来は正当なアクセスを不正とみなして false positive(誤検知)を起こしてしまいます。
ホットパスでのオーバーヘッド
計装はメモリアクセスごとに追加のチェックを走らせるため、実行時のオーバーヘッドを伴います。頻繁に実行されるホットパスでは、このオーバーヘッドが許容できないこともあります。
これらの理由から、サニタイザを有効にしたビルド全体ではなく、特定の関数だけを計装から除外したい という需要がありました。しかし、これまでの Swift には関数単位で計装を無効化する手段がありませんでした。
02 どのように解決されるのか
関数単位でコンパイラが挿入する計装(instrumentation)を無効化する属性 @instrumentation を導入します。サニタイザはこの計装の代表例ですが、この属性はサニタイザに限らず、コンパイラが挿入する計装全般を対象とします。無効化したい計装の種類は disable: ラベルに 1 つ以上指定します。
指定できる計装の種類
初期状態で指定できる種類は次の 4 つです。
address— ASan(AddressSanitizer)の計装を抑制します。thread— TSan(ThreadSanitizer)の計装を抑制します。memtagStack— MemTag(スタックのタグ付け)の計装を抑制します。coverage— SanitizerCoverage の計装を抑制します。
サニタイザに対応する種類の名前は、対応する -sanitize= コマンドラインフラグを lowerCamelCase にしたものです(たとえば -sanitize=memtag-stack は memtagStack になります)。
各種類は独立して opt-out されます。たとえば -sanitize=thread でビルドした関数に @instrumentation(disable: address) を付けても、TSan は有効なままで何の効果もありません。
複数の種類は 1 つの属性にまとめて @instrumentation(disable: address, thread) と書けます。また、同じ宣言に @instrumentation を複数重ねて書くこともでき、両者は等価です。
適用できる場所
@instrumentation は、トップレベルの func、メソッド、イニシャライザ、デイニシャライザ、アクセサといったあらゆる関数のほか、サブスクリプトや明示的なクロージャ式に付けられます。
@instrumentation(disable: address)
func readsMMIO(_ p: UnsafePointer<UInt32>) -> UInt32 { p.pointee }
struct Device: ~Copyable {
@instrumentation(disable: address) init() { ... }
@instrumentation(disable: address) deinit { ... }
var status: UInt32 {
@instrumentation(disable: address) get { ... }
@instrumentation(disable: address) set { ... }
}
@instrumentation(disable: address)
subscript(i: Int) -> UInt8 { ... }
}
// 複数の種類をまとめて、あるいは重ねて無効化する
@instrumentation(disable: address, thread)
func hotPath() -> Int { ... }
クロージャへの伝播
@instrumentation は明示的なクロージャ式にも書けます。
registerCallback { @instrumentation(disable: address) in
readsMMIO()
}
関数に付けた @instrumentation は、その内部にネストしたクロージャにも 伝播します。これは SE-0537 の @section と同じ挙動です。
伝播を打ち消したい場合は @instrumentation(default) を使います。これは外側のスコープから伝播してきた設定をリセットし、その宣言やクロージャを現在のビルドのデフォルトどおりに計装させます。default は disable: と組み合わせて、いったんリセットしたうえで特定の種類だけを改めて無効化することもできます。
@instrumentation(disable: address)
func outer() {
withCallback { @instrumentation(default) in
// outer は計装されないが、ここは通常どおり計装される
}
withCallback { @instrumentation(default, disable: thread) in
// address の計装は元に戻り、thread の計装だけが無効化される
}
}
インライン化との関係
@instrumentation(disable: <kind>) を付けた関数の本体を、計装されたままの呼び出し元にインライン化してしまうと、除外したはずの本体が呼び出し元側で再び計装され、属性の意味が失われてしまいます。そこで Swift は、ある計装が有効なビルドにおいて、@instrumentation(disable: <kind>) を持つ関数を、同じ @instrumentation(disable: <kind>) を持たない呼び出し元へヒューリスティックにインライン化することはしません。
さらに、@inline(always)(SE-0496)と @instrumentation(disable:) を同じ宣言に併記することはできず、エラーとして診断されます。両者は矛盾する要求をするためです。@inline(always) は呼び出し元へ必ずインライン化すること(できない場合は診断すること)を求めるのに対し、@instrumentation(disable:) は呼び出し元が計装されていても本体を計装しないまま残すことを求めます。
有効な計装を判定する条件付きコンパイル
@inline(always) と @instrumentation(disable:) を併記できないため、「計装なしのビルドでは @inline(always) にし、計装ありのビルドではインライン化せず計装も外す」という書き分けをしたい場合に困ります。
そこで、コンパイル中のモジュールで指定した計装が有効かどうかを判定する moduleInstrumentation(<kind>) 条件付きコンパイルを導入します(たとえば -sanitize=address が渡されていれば moduleInstrumentation(address) が真になります)。指定できる種類は属性と同じで、moduleInstrumentation 1 つにつき種類は 1 つだけ書けますが、通常の論理演算子で組み合わせられます。
#if !moduleInstrumentation(address)
@inline(always)
#else
@instrumentation(disable: address)
#endif
func f() { ... }
これにより、計装なしのビルドではインライン化を効かせつつ、ASan 有効時には計装されない独立した関数として振る舞わせられます。
なお、この条件はモジュール全体のビルド設定だけを反映するもので、文脈には依存しません。外側の宣言に付いた @instrumentation(disable:) は考慮されない点に注意してください。
03 今後の見通し
元の Proposal では、今後の方向性として次が挙げられています。いずれも将来の構想であり、実現を約束するものではありません。
グローバル変数への適用
一部のサニタイザは、clang の no_sanitize によってグローバル変数のサニタイズを無効化できます。Swift のグローバル変数についても、同様に無効化できる属性を将来サポートすることが考えられています。
計装種別の追加
@instrumentation で指定できる計装の種類は、将来 swiftc が新しいサニタイザや、その他のコンパイラ挿入の計装に対応するのに合わせて、追加していく見込みです。