iosdev

Objective-C framework callbacks in Swift 6

Solving backward-compatibility issues in Swift 6 language mode.

After upgrading all my active projects to Swift 6 language mode, I was bound to encounter some head-scratching edge cases. One of the weirdest ones was this runtime crash:

Thread 5 Queue : com.apple.root.default-qos (concurrent)
#0	0x00000001021243f8 in _dispatch_assert_queue_fail ()
#1	0x0000000102124384 in dispatch_assert_queue ()
#2	0x00000002444c63e0 in swift_task_isCurrentExecutorImpl ()
#3	0x0000000105a895b4 in closure #1 in AppDelegate.setupObjCFramework(using:flowIdentifier:) ()

It does not happen when app is compiled and ran in Swift 5 language mode. It is not caught by Swift 6 compiler at build time. But it regularly crashed on app start (in AppDelegate) when a method in Objective-C framework that has a callback closure is executed. Code like this:

objcInstance.setupObjCFramework(using: configFile) {
	[unowned self] err, string in	
}

None of the callback’s code is executed thus it seemed like it crashes just before it executes the callback, meaning — it’s a bug in the framework. Or so I thought.

After a lot of searching, I found the answer in Apple developer forums.

The high-level summary is:
· Swift 6 has inserted a run-time check to catch a concurrency issue that Swift 5 did not.
· This isn’t caught at compile time because of a Swift / Objective-C impedance mismatch.

So crash is due to this compiler-inserted runtime check because:

  1. There is no guarantee anywhere that callback will be ran on the same thread from where it was called (main thread in my case) which is actually true here - you can see that the crash was in thread 5.
  2. There is no guarantee — as it is — that callback closure itself is thread-safe. That check force-crashes to tell you that it may theoretically cause the data race even if such scenario never happens in practice.

The simplest solution? Mark the callback Sendable so compiler knows that whatever is done inside is thread-safe:

objcInstance.setupObjCFramework(using: configFile) {
	@Sendable [unowned self] err, string in	
}

Ideally the callback should have been marked as such in the Obj-C framework itself (with NS_SWIFT_SENDABLE) but that’s unlikely to happen in practice as most of these frameworks are relics that are not maintained anymore.