Friday, September 20, 2024

Unwanted Swift Concurrency Checking

I’m not adopting Swift Concurrency yet—it’s not even available on the OS versions I’m targeting—so my plan was to take advantage of the Swift 5 language mode of the Swift 6 compiler:

The Swift 6 language mode is opt-in. Existing projects will not switch to this mode without configuration changes.

I had SWIFT_VERSION set to 5, and this worked great when compiling my apps with beta versions of macOS 15 and Xcode 16 over the summer. I needed to make some minor updates to my code to compile with Swift 6 but none were related to concurrency.

However, after updating to the release version of Xcode 16 (since macOS 15 won’t run Xcode 15.4), I started getting this error:

Capture of 'buffer' with non-sendable type 'UnsafeMutableBufferPointer<T>' in a `@Sendable` closure; this is an error in the Swift 6 language mode Generic struct 'UnsafeMutableBufferPointer' does not conform to the 'Sendable' protocol (Swift.UnsafeMutableBufferPointer)

from this code:

DispatchQueue.concurrentPerform(iterations: count) { (index) in
    buffer[index] = // …
}

The GCD API is marked with @preconcurrency:

@preconcurrency public class func concurrentPerform(iterations: Int, execute work: @Sendable (Int) -> Void)

but that didn’t stop it from complaining.

I don’t know whether this is a bug, but it’s certainly not what I expected after repeatedly hearing that strict concurrency checking is opt-in. Reading the details:

You can address data-race safety issues in your projects module-by-module, and you can enable the compiler’s actor isolation and Sendable checking as warnings in the Swift 5 language mode, allowing you to assess your progress toward eliminating data races before turning on the Swift 6 language mode.

It seems that you can “enable” the checking as warnings by setting SWIFT_TREAT_WARNINGS_AS_ERRORS to NO, but I don’t want to do that because I do want other types of warnings to be treated as errors. There’s not yet a way to control warnings individually. And, also, I don’t want to see concurrency warnings with every build when I’m not going to be actively fixing them for a while. There is a separate SWIFT_STRICT_CONCURRENCY build setting, but there doesn’t seem to be a way to turn it off, only to minimal, which still reports this warning/error.

I’m not the only one to run into this exact issue. Frank Rupprecht found that the warning can be avoided by assigning the closure to a variable, rather than passing it directly:

let closure = { (index: Int) in
    buffer[index] = // …
}
DispatchQueue.concurrentPerform(iterations: count, execute: closure)

It’s not clear to me whether this is a hack exploiting a compiler bug or a designated opt-out akin to how GCC treats putting an assignment expression in parentheses. But I’m going with this for now because it seems silly to rewrite the code for Swift Concurrency when (a) I’m not using that yet, and (b) GCD is a separate world, anyway.

Jesse Squires discusses a related issue when he does have strict concurrency checking enabled:

I was confronted with this warning in a project recently and I want to share the hack for how I worked around it. The issue was the result of a combination of factors I mentioned above. I was interacting with @preconcurrency APIs and I knew my code was concurrency-safe, but I was unable to accurately express that to the compiler.

[…]

So, we have situation where the Swift compiler is telling us that the closure being captured needs to be @Sendable but we cannot make it @Sendable. It is also telling us that the closure loses its @MainActor but we know that the closure will always be called from the main queue. Because of these two problems, we need to find a way to work around the warnings and coerce the compiler into doing what we want.

I also got a similar error about capturing a non-sendable type when using these methods:

@MainActor func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String

nonisolated func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL) async throws

I was already using the queue supplied by the provider, but Swift Concurrency doesn’t know that. Here I was able to silence the error by using:

nonisolated(unsafe) let provider = // …

Finally, I got this error:

Struct 'Notification' does not conform to the 'Sendable' protocol (Foundation.Notification)

with this code:

queue.async(flags: .barrier) {
    DispatchQueue.main.sync {
        f(notification)
    }
}

I’m ensuring that the notification is only used on the main thread, but Swift Concurrency doesn’t realize that. Again, I was able to silence the error using nonisolated(unsafe). It’s not clear to me whether assumeIsolated() might be more appropriate in this situation, but I can’t use it, anyway, because that API doesn’t back deploy far enough.

Previously:

Update (2024-09-23): Sami Samhuri:

I ran into something similar and it’s fixed in Xcode 16.1 beta 2.

My issues are not fixed in the beta, so I filed a bug.

Tanner Bennett:

At Tinder we have about 2 dozen concurrency related warnings under Xcode 16 and we have concurrency checking turned off and are in swift 5 mode. Super frustrating.

Rhys Morgan:

You know about the DispatchQueue.main.async special casing [in the compiler] too, right? When you write exactly that, it infers that the contents of the async bit are MainActor-isolated.

Comments RSS · Twitter · Mastodon

Leave a Comment