Replacement for MainActor(unsafe)?

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

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)()
        }
    }
}
2 Likes

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.

8 Likes

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

This is resolved in [5.10][SE-0392] Back-deploy assertIsolation/assumeIsolation by tshortli · Pull Request #70474 · apple/swift · GitHub

7 Likes