Swift Digest
SE-0463 | Swift Evolution

Import Objective-C completion handler parameters as @Sendable

Proposal
SE-0463
Authors
Holly Borla
Review Manager
John McCall
Status
Implemented (Swift 6.2)

01 何が問題だったのか

Swiftのデータ競合安全性モデルでは、関数の並行性に関する契約を、関数シグネチャ上の注釈として明示する必要があります。クロージャ引数に付く @Sendable は、そのクロージャが isolation boundary を越えて渡されてから呼ばれる可能性があることを示すもので、ライブラリ側にこの注釈が付いていないと、呼び出し側は知らないうちにデータ競合を混入させてしまいます。

さらにSE-0423(Dynamic actor isolation enforcement from non-strict-concurrency contexts)により、データ競合チェックが有効でないライブラリへ non-Sendable なクロージャ引数を渡す際には、ランタイムで isolation のアサーションが挿入されるようになりました。つまり、本来 @Sendable であるべきなのに注釈が付いていないクロージャ引数は、actor-isolated な文脈から呼び出したときにランタイムクラッシュを引き起こします。Swift 6 への移行中のプロジェクトにとって、これは非常に痛い挙動です。

Objective-Cのメソッドに含まれるcompletion handler引数は、この問題が特に顕著になるカテゴリです。completion handlerは別スレッドから呼び出されるのが通例で、実質的には常に @Sendable として扱うのが正しいにもかかわらず、Clangヘッダ側で明示的に @Sendable を付けて回るのは現実的ではありません。実際、Clangヘッダに対して completion handler を一括で @Sendable 扱いするオプションを求める声が以前から上がっていました。

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

Objective-Cのメソッドから取り込まれる completion handler 引数を、自動的に @Sendable 関数としてインポートします。upcoming feature flag として導入され、有効化するとObjective-C由来のAPIのcompletion handlerが既定で @Sendable になります。

インポート規則

対象となるのは、SE-0297(Concurrency Interoperability with Objective-C)に基づいて async バージョンが合成されるメソッド、かつそのメソッドが(暗黙的または明示的に)nonisolated であるものです。この条件を満たす場合、completion handler 付きのオリジナルメソッドは、completion handler 引数に @Sendable が付いた形でインポートされます。

たとえば次のObjective-Cメソッド

- (void)performOperation:(NSString * _Nonnull)operation
  completionHandler:(void (^ _Nullable)(NSString * _Nullable, NSError * _Nullable))completionHandler;

はSwift側でこう見えます。

@preconcurrency
func perform(
  operation: String,
  completionHandler: @Sendable @escaping ((String?, Error?) -> Void)?
)

この perform をアクターから呼び出すとき、non-Sendable なクロージャをそれが作られた文脈の isolation に推論する規則はもう適用されません。クロージャは nonisolated として推論され、アクターの region に属するミュータブルな状態をクロージャ内から触ると警告が出ます。

なおC/C++/Objective-Cからインポートされたすべての宣言は自動的に @preconcurrency 扱いになるため、データ競合安全性の違反はSwift 6言語モードでも警告に留まり、エラーにはなりません。

グローバルアクターに isolate された関数の扱い

グローバルアクターに isolate された関数の completion handler は、@Sendable としてインポート されません@MainActor に isolate された関数の completion handler が常に main actor 上で呼ばれる、というのはObjective-Cで非常によくあるパターンで、そこで completion handler に main actor 注釈が付いていないケースに対して @Sendable を強制してしまうと、偽陽性の警告が大量に出てしまいます。このカーブアウトによって、ランタイムの新しいアサーションが増えることはありません。

@Sendable にしたくない場合のオプトアウト

completion handler が isolation boundary を越える前に呼ばれることが保証できる場合は、Clangヘッダ側で @nonSendable 属性を付けてオプトアウトできます。

__attribute__((swift_attr("@nonSendable")))

この @nonSendable はClangヘッダ注釈専用で、Swiftコード側から使うためのものではありません。

sending ではなく @Sendable を選んだ理由

completion handler を sending として取り込む選択肢もありましたが、今回はあえて保守的に @Sendable が採用されました。SendableCompletionHandlers の試験実装は2021年から存在し、ソース互換性の観点で長く検証されてきた実績があります。また @Sendable はObjective-Cフレームワーク側でも既に広く採用されており、関連するコンパイラのバグも時間をかけて枯れてきました。一方 sending は比較的新しく、@preconcurrency と組み合わせたときにSwift 6言語モードで診断を警告に降格させる仕組みもまだありません。SE-0423由来のランタイムアサーションの痛みを、いま解消する価値の方が大きいという判断です。

将来 sending に切り替えることにはソース互換性上の問題があるため(sending でインポートされたプロトコル要件を @Sendable な completion handler で実装できなくなるなど)、region isolation の利点を取りたい場合は async/await を使うコードへ近代化するのが推奨される道筋です。