Tuesday, October 1, 2024

Swift Concurrency and Objective-C

Paul Haddad:

Anyone know why calling the following in a MainActor class/func

MyTest.increment(1) { result in
    NSLog("result=\(result)")
}

crashes (asserts) when building with Swift 6?

I get that its not happy that the completion is coming in on another dispatch_queue but it should complain about it at compile time, or ignore it at run time.

Unfortunately, it seems to be designed this way.

OneSadCookie:

SE-0423 “Dynamic actor isolation enforcement from non-strict-concurrency contexts” adds the crash (otherwise your “safe” Swift 6 code is unsafe).

It’s not that the code is unsafe but that neither the Swift compiler nor the Swift runtime can prove that it’s safe because the MyTest class is written in Objective-C. You are supposed to annotate your Objective-C code so that Swift Concurrency can understand it, though this is not really documented.

Doug Gregor:

In Swift 5 mode, this code silently introduces a data race into the program.

In Swift 6 mode, the data race is caught by the dynamic isolation check. That’s the first point at which the data race can be detected, and the check is there to prevent this race from becoming weird runtime behavior.

This is all as designed. If that Objective-C code were Swift 6 code, we’d catch the error at compile time. As Objective-C, runtime is the earliest it’s possible to detect the race. Enabling Swift 6 language mode means turning previously-unobserved or undiagnosed data races into ones that fail predictably to flush out any contract violations outside of the Swift 6 code. As more code enables Swift 6, the runtime checks get replaced with compile-time.

Swift 6 mode is being “helpful” by proactively crashing the app even though there’s not necessarily a problem. It may be that it just doesn’t understand that GCD is being used to call everything on the right thread.

I get why Swift 6 is designed this way, but I don’t understand how you’re supposed to make the transition. Swift 5 mode gives no errors at compile time and doesn’t even log any errors at runtime. Swift 6 mode gives no errors at compile time and crashes at runtime. To get from one to the other you’re supposed to go through the code line-by-line and not make any mistakes.

Personally, I’m skeptical of the benefit of switching to Swift 6 mode with a hybrid codebase. If you started off with good code, it seems more likely that you’ll get some annotation wrong and have Swift 6 trigger an unnecessary crash than that you actually discover a latent concurrency bug that matters. I think it makes more sense to migrate the code to Swift before flipping the switch. Then you can get errors at compile time instead of at runtime.

Mike Apurin:

I think that Swift 5 mode doing nothing is part of the problem here. I’ve run in similar issues in Combine and was very blindsided by it. There is no way to progressively discover and deal with such isolation violations, just enabling 6 mode and praying.

But this illustrates that porting your code to Swift doesn’t fully solve the problem, either. It seems that you still have to annotate your closures because of Apple’s code.

Matt Massicotte:

The core problem, in my opinion, was Combine was not updated and that’s bananas.

Previously:

Update (2024-10-02): See also: further discussion on Mastodon.

Update (2024-11-11): Aleksandar Vacić:

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[…]

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.

6 Comments RSS · Twitter · Mastodon


Do app developers want this? I don’t.

the people making these decisions, do they write GUI apps at all or do they just work on the Swift language and compiler?

They should be required to use the tools they are trying to force on everyone else IMO.

Why aren’t they enforcing these safety features quietly in the OS first…before pushing it on app devs?


Pierre Lebeaupin

Apple is reaping what they've sown here. When Swift initially shipped I remarked (ten years ago!) that they provided absolutely no guidance, much less rules, no warning, no nothing on the concurrent use of Swift. Now, it was clear at the time already they themselves were figuring it out, and that a model of Swift concurrency would come eventually; the issue is not only that it took years, but that in the meantime there was clear encouragement to insert Swift everywhere in existing Objective-C codebases, buidling up the time bomb that is exploding today. Especially when combined with Objective-C APIs whose semantics are not all that well defined (Dispatch is fine; Combine, not so much). Had they told us from the outset "only use Swift from the main thread; unless you're adventurous, in which case at the very least do not transfer references to Swift objects between threads/queues/etc; unless you're willing to be a pioneer with arrows planted on your back, in which case only do so through Dispatch and keep track of which classes can be accessed in which contexts", most of those could have been avoided. But here we are, and Apple only has itself to blame. Not even the Swift team is to blame, as the issues are entirely related through the interaction with Objective-C APIs, which are 100% Apple responsibility, including their Swift wrappers.


Why not adopt Swift 6, fix statically detected issues, and build with `-disable-dynamic-actor-isolation` to avoid runtime landlines? Seems like the win/win approach if you’re using Swift concurrency.


@Person Sounds good in theory, but Haddad said that -disable-dynamic-actor-isolation didn’t actually work for him.


Well that’s a bummer :-/


From [Bincompat.h](https://github.com/swiftlang/swift/blob/fa8852bf47820a59db4840e897b7e0673a95e344/include/swift/Runtime/Bincompat.h#L50-L80), setting `SWIFT_UNEXPECTED_EXECUTOR_LOG_LEVEL=1` or `SWIFT_IS_CURRENT_EXECUTOR_LEGACY_MODE_OVERRIDE=nocrash` will opt-in to the legacy Swift 5 behavior and prevent the crash.

Leave a Comment