[Prepitch] Using @MainActor and DispatchQueue.main.async without Foundation

We currently use DispatchQueue.main... and @MainActor a lot throughout our codebases on non-Darwin platforms.

Since we need to interact with the "real" main thread on platforms like Android, it's important to us that these "main thread" things really do run on the main thread. However, we cannot take over the main thread entirely via something like CFRunLoopRun() or dispatch_main() etc., because then the entire process will essentially freeze. i.e., we need to cooperate with the process' existing scheduler.

To do this, we run CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.01, true) periodically, which works great. The issue is: that function is vended by Foundation, or CoreFoundation, both of which are monolithic frameworks with very large and complex dependencies (e.g. libxml, libcurl, ICU). In short: I find it really really unfortunate to have to bundle over 30MB of library dependencies just so we can use @MainActor.

I understand this is more of a niche topic because on many platforms the use of Foundation (etc.) is not problematic. But I have to say I'm disappointed that even a "brand new" language feature seems to depend on a huge, seemingly-unrelated, external dependency that has over 20 years of legacy attached to it (via CoreFoundation).

We could use some other serial queue to achieve a similar effect, but the @MainActor / DispatchQueue.main are well established paradigms and it would greatly reduce the amount of code we could re-use between Darwin and other platforms if we had to write all this differently. It would also make it significantly more difficult to coordinate with the "parent"/platform main thread.

So, what I'd like to propose is a function MainActor._processTasks(for: Duration) that does whatever is needed to drain the main queue. Ideally that function would drain Dispatch's main queue, but if that is too difficult for whatever reason I'd personally also be fine to remove all uses of dispatch entirely and just use Swift-native concurrency. Alternatively, if that is inverting the dependency too much, something like void dispatch_main_for_duration(float) would also be great!

I would appreciate some input from someone more experienced with concurrency/dispatch in order to shape the API and better understand what it needs to do internally.

2022-11-08: Edited this post to make my point clearer

1 Like

Actually here's maybe a simpler question: if we're not "adding" sources or ports or timers etc. to the CFRunLoop (noting that we are trying to remove uses of CF entirely), is there anything that speaks against calling dispatch's _dispatch_main_queue_callback_4CF like this:

import Dispatch
@_silgen_name("_dispatch_main_queue_callback_4CF")
public func dispatchMainQueueCallback(_ msg: UnsafeMutableRawPointer?) -> Void


// in render loop (main thread):
func handleEventsAndRender() {
  handleEvents(...)
  render(...)
  dispatchMainQueueCallback(nil)
}

?

I just tried that and it "works", but there seems to be a lot more going on in _CFRunLoopRun then just this, so I'm worried I might be missing something.

If nothing really speaks against that, can we get a more "sanctioned" / stable version of this function? It appears to be exactly what we need.

1 Like