mattie
1
I'm a heavy user of NSDocument, which has a pretty unfortunate API when it comes to concurrency. There are a few places where I had to resort to using MainActor(unsafe) to do what I needed. However, that appears to no longer work as of Swift 5.9. It looks like the (unsafe) argument to MainActor is being ignored.
And while I do love the new MainActor.assumeIsolated API, I need something that will work on older OSes. Does anyone have any ideas?
Here's what I was doing:
extension MainActor {
@MainActor(unsafe)
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
dispatchPrecondition(condition: .onQueue(.main))
return try body()
}
}
Usage of this API now fails to compile with:
Call to main actor-isolated static method 'runUnsafely' in a synchronous nonisolated context
2 Likes
tgoyne
(Thomas Goyne)
2
assumeIsolated is implemented as a check followed by an unsafeBitCast to remove the actor confinement, which is something you can also do manually:
extension MainActor {
@_unavailableFromAsync
static func runUnsafely<T>(_ body: @MainActor () throws -> T) rethrows -> T {
#if swift(>=5.9)
if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) {
return try MainActor.assumeIsolated(body)
}
#endif
dispatchPrecondition(condition: .onQueue(.main))
return try withoutActuallyEscaping(body) { fn in
try unsafeBitCast(fn, to: (() throws -> T).self)()
}
}
}
4 Likes
hborla
(Holly Borla)
3
Sorry to bump a topic from a few months ago; I've been digging into concurrency recently and just came across this post. I think it's a great question.
I think MainActor.assumeIsolated should have @backDeploy applied, and writing a nonisolated function that uses MainActor.assumeIsolated should be the blessed way of accomplishing what you're trying to do with @MainActor(unsafe). There's a PR open to back deploy MainActor.assumeIsolated, but there are a few issues with the implementation of @backDeploy to work out before this can be merged. In the meantime, dispatchPrecondition(condition: .onQueue(.main)) is probably the closest substitute that's nice to call and has the same result in the case of asserting @MainActor. The concurrency runtime has functions (that back deploy) for asserting that code is running on a given expected executor, but writing a call to one of these functions in Swift code is not very straightforward.
More broadly, I'm very interested in enabling general dynamic actor executor enforcement for actor-isolated functions, which can cover the cases where it isn't possible to prove data-race safety statically, such as calling through Objective-C code. Ideally, calling those concurrency runtime functions to assert you're on an expected executor would happen upon entry for every actor-isolated function in your Swift code. There's an experimental implementation of this dynamic checking behind -enable-actor-data-race-checks. I'm working on figuring out work must be done in order to enable dynamic enforcement by default (e.g. I'm not sure whether we need a way to downgrade runtime errors to warnings / suppress the errors entirely in some cases; I'll most likely write a pitch for Swift evolution to specify the behavior). I suspect there might be a way to leverage dynamic actor executor enforcement to make the nonisolated + assumeIsolated pattern unnecessary to write explicitly when you're working with certain @preconcurrency APIs, such as conforming @MainActor-isolated types to @preconcurrency nonisolated protocols (i.e. the default for protocols imported from Objective-C).
I also want to note that this error message:
is a bug that I just fixed for 5.10
The @MainActor(unsafe) spelling is a vestige of an early implementation of @preconcurrency @MainActor; they mean the same thing. Though I mentioned Swift 5 mode in the PR description, the @preconcurrency diagnostics should remain downgraded to warnings in Swift 6 as well. However, this means there will be a warning at every call-site of your API. I think you want this function to be nonisolated, and cast away the @MainActor isolation on body in the implementation of runUnsafely like @tgoyne suggested.
9 Likes
mattie
4
I also want to apologize for not seeing this answer sooner.
I love absolutely everything about this, and I really want to thank you for both working on them and letting me know. I'm extremely excited about seeing a back-deployed MainActor.assumeIsolated, as it is a pretty much daily problem for me.
1 Like
hborla
(Holly Borla)
5
7 Likes