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:

Comments RSS · Twitter · Mastodon

Leave a Comment