I haven't found any definitive documentation around this - so please point me at that if it's available!
Somehow, the compiler 'knows' that
DispatchQueue.background.async {
//this does not run on the main-actor
}
DispatchQueue.main.async {
//this does run on the main-actor
}
I can't see any clues in the headers as to how the two cases are annotated differently. They both just show as DispatchQueue.
I have a similar case in my own API where the user can set the queue for a notification to callback.
I'd like to mark that the block runs on @MainActor if the queue is .main - but I can't see how to do that.
e.g.
func register(on queue:DispatchQueue,callback:<if queue is .main @MainActor> ()->Void)
There's indeed some magic inside. It's not very deep/thorough though, probably some simple heuristic in there:
func foo() {
DispatchQueue.main.async {
_ = UIApplication.shared.applicationState // ok
}
let mainQueue = DispatchQueue.main
mainQueue.async {
_ = UIApplication.shared.applicationState // error
// Class property 'shared' isolated to global actor 'MainActor' can not be referenced from a non-isolated synchronous context
}
}
Edit:
Or even this:
enum DispatchQueue { // let's cheat
static let main = Foundation.DispatchQueue.global()
}
DispatchQueue.main.async { // not real main
_ = UIApplication.shared.applicationState // ok 😀
}
Looks so. I'd prefer some annotation system like @convention(mainActor) to make the check thorough but apparently it's much harder to do. You can still rely on runtime checks though.
tl;dr: it's a hardcoded syntax check in the compiler that looks for DispatchQueue.main.async (and some related APIs). It's not something we can replicate with an annotation.
We've thought about ways to generalize this. For your API, we'd need an annotation (or a heuristic, but since this is a safety property...) to say that the parameter function will always be run on the parameter queue. That would be enough to let us at least use the same hard-coded knowledge that DispatchQueue.main isolates MainActor. That could be generalized to other global actors with some sort of annotation that we could put on any function that says that the return value is an executor that isolates a particular global actor. If we wanted to generalize that to other actors, that gets really tricky.
@ole - love that you dug into this. Thanks for sharing.
I really hate how this is hidden special-casing.
I don't know what the answer is - but I do wish it was explicit.
The main place I see this is in combine. There are a bunch of ways that combine works where I know that stuff will happen on MainActor, but the compiler doesn't
e.g.
publisher.receive(on: RunLoop.main)
.sink {
//this is MA
}
Yeah, the Combine pattern would be very hard for Swift to recognize because it relies on hidden data flow. The easiest way to make it work would be for sink to take the queue to receive events on, or even simply take an @isolated(any) function (a feature I'm currently working on). Or, I suppose, we could special-case it in the compiler, although I think you can imagine why we'd be reluctant to extend that to more and more framework-specific code patterns. The DispatchQueue.main hack, as discussed above, is fairly justifiable as an instance of a pattern that we could reasonably hope to implement in more generality. There's not really a path to that for this Combine pattern.