portico/Sources/Portico/MainLoop/DarwinMainQueuePump.swift

67 lines
3.6 KiB
Swift

#if canImport(Darwin)
import Darwin
import CoreFoundation
// MARK: - Darwin main-queue pump
/// Drains Swift's default `MainActor` executor by periodically running the
/// process's `CFRunLoop` from inside the GLib main loop.
///
/// ## Why this exists
///
/// On Linux, ``GLibMainExecutor`` is installed as the literal process main
/// executor via the stdlib's `@_spi(ExperimentalCustomExecutors)` API, so
/// every `Task { }` and `await` resumption is driven directly by GLib idle
/// sources. That SPI has no accessible implementation on Apple platforms:
/// `_Concurrency` is an OS-vended, library-evolution (resilient) framework
/// there, and Apple ships only its public `.swiftinterface` - `@_spi`
/// declarations require a `.private.swiftinterface`, which no distributed
/// Xcode or Command Line Tools install carries for this module. There is no
/// stable substitute for overriding what backs `MainActor` on Darwin today.
///
/// So on Darwin, `MainActor` keeps the stdlib's default executor, which is
/// backed by the main dispatch queue. GTK's own macOS backend
/// (`gdkmacoseventsource.c`) keeps GLib's poll-based main loop primary and
/// pulls native Cocoa events into it via a replaced `GPollFunc` - it does not
/// hand control to `CFRunLoop`. That means `Adw.Application.run` blocks the
/// thread inside GLib's loop exactly as it does on Linux, and the main
/// dispatch queue is never given a chance to run on its own.
///
/// This pump closes that gap without reaching for any private or
/// experimental API: a low-priority, always-repeating `Timeout` source drains
/// `CFRunLoopRunInMode` a few times per GLib iteration. Draining the run loop
/// in `.defaultMode` is what services the main dispatch queue on every Apple
/// platform (it is the same mechanism `RunLoop.main.run()` relies on), so any
/// `Task { @MainActor in }` continuation queued by the stdlib's default
/// executor gets a chance to run within one pump interval. Synchronous GTK
/// signal/source callbacks are unaffected either way: those already reach
/// `@MainActor` code through `MainActor.assumeIsolated`, which asserts
/// same-thread isolation rather than routing through an executor, so they
/// have never depended on which executor backs `MainActor`.
@_spi(Portico) @MainActor public enum DarwinMainQueuePump {
/// How often the pump drains the run loop. Short enough that `await`
/// resumption feels immediate against a 60-120Hz UI; long enough that the
/// idle cost is negligible (`CFRunLoopRunInMode` returns immediately when
/// nothing is pending).
private static let interval = Duration.milliseconds(4)
/// Guards the `Timeout` attach; the lazy static makes repeat calls free
/// so it is safe to call ``install()`` more than once.
private static let installOnce: Bool = {
Timeout(interval: DarwinMainQueuePump.interval, priority: .default, repeats: true) {
// `.defaultMode` is the mode `RunLoop.main`/`CFRunLoopRun()` use
// by default and the one the main dispatch queue's run-loop
// source is registered against. Loop until the run loop reports
// nothing left to do, so a burst of queued continuations drains
// in one GLib tick rather than trickling out one per `interval`.
while CFRunLoopRunInMode(.defaultMode, 0, true) == .handledSource {}
}
return true
}()
/// Starts draining the main dispatch queue from the GLib main loop if it
/// has not already been started this process lifetime. Idempotent; call
/// before ``Adw/Application/run(argv:)`` blocks the thread.
@_spi(Portico) public static func install() { _ = installOnce }
}
#endif