Bridge MainActor to GLib main loop on Darwin without SPI
This commit is contained in:
parent
e52f9c06e8
commit
e0dd29297a
3 changed files with 97 additions and 12 deletions
|
|
@ -15,10 +15,14 @@ import Darwin
|
|||
/// `connectActivate`, runs the GTK main loop, and propagates the exit
|
||||
/// code to the process.
|
||||
///
|
||||
/// Installing ``GLibMainExecutor`` as the process main executor here means
|
||||
/// that every `Task { }`, `await`, and main-actor resumption in a Portico
|
||||
/// app is drained by `g_application_run` - no boilerplate in user code and
|
||||
/// no change to the GTK application lifecycle.
|
||||
/// On Linux, installing ``GLibMainExecutor`` as the process main executor
|
||||
/// here means that every `Task { }`, `await`, and main-actor resumption in a
|
||||
/// Portico app is drained by `g_application_run` directly. On Darwin, where
|
||||
/// that install path needs a stdlib SPI Apple does not ship,
|
||||
/// ``DarwinMainQueuePump`` drains the stdlib's default main executor instead
|
||||
/// by periodically running `CFRunLoop` from a GLib timeout source - see its
|
||||
/// doc comment for why. Either way, no boilerplate in user code and no
|
||||
/// change to the GTK application lifecycle.
|
||||
@_spi(Portico) @MainActor public enum PorticoRuntime {
|
||||
/// The environment an ``App``'s ``DynamicProperty`` declarations resolve against.
|
||||
///
|
||||
|
|
@ -182,7 +186,11 @@ import Darwin
|
|||
/// must conform to ``MountableScene``.
|
||||
/// - Parameter app: The ``App`` instance to run.
|
||||
public static func run<A: App>(_ app: A) {
|
||||
#if canImport(Glibc)
|
||||
GLibExecutorFactory.install()
|
||||
#elseif canImport(Darwin)
|
||||
DarwinMainQueuePump.install()
|
||||
#endif
|
||||
PorticoRuntime._resolveRootProperties(app)
|
||||
|
||||
let adwApp = Adw.Application(
|
||||
|
|
|
|||
67
Sources/Portico/MainLoop/DarwinMainQueuePump.swift
Normal file
67
Sources/Portico/MainLoop/DarwinMainQueuePump.swift
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#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
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
#if canImport(Glibc)
|
||||
@_spi(ExperimentalCustomExecutors) import _Concurrency
|
||||
#endif
|
||||
import Synchronization
|
||||
|
||||
#if canImport(Glibc)
|
||||
|
|
@ -92,7 +94,7 @@ nonisolated func portico_executor_destroy_notify(_ data: UnsafeMutableRawPointer
|
|||
|
||||
// MARK: - Main executor
|
||||
|
||||
/// A `MainExecutor` that drains Swift MainActor jobs through the GLib main loop.
|
||||
/// A `SerialExecutor` that drains Swift jobs through the GLib main loop.
|
||||
///
|
||||
/// Each call to `enqueue` posts a one-shot `G_PRIORITY_DEFAULT_IDLE` source onto
|
||||
/// the default `GMainContext`. The source's callback snapshots the pending job
|
||||
|
|
@ -100,12 +102,15 @@ nonisolated func portico_executor_destroy_notify(_ data: UnsafeMutableRawPointer
|
|||
/// resumption therefore yields to the GTK main loop between batches instead of
|
||||
/// monopolising it.
|
||||
///
|
||||
/// ``GLibMainExecutor`` is installed as the process main executor by
|
||||
/// ``PorticoRuntime/run(_:)``. After that, every `Task { }`, `await`, and
|
||||
/// main-actor resumption is drained by `g_application_run` with no change to
|
||||
/// the GTK application lifecycle.
|
||||
/// On Linux, ``GLibExecutorFactory`` installs this as the process main
|
||||
/// executor via a stdlib SPI unavailable on Apple platforms (see
|
||||
/// ``DarwinMainQueuePump``), so every `Task { }`, `await`, and main-actor
|
||||
/// resumption is drained by `g_application_run` with no change to the GTK
|
||||
/// application lifecycle. On every platform it also works as a plain
|
||||
/// `SerialExecutor` for an individual actor, which is how the test suite
|
||||
/// exercises it without depending on the Linux-only install path.
|
||||
@_spi(Portico) nonisolated public final class GLibMainExecutor:
|
||||
MainExecutor, Sendable
|
||||
SerialExecutor, Sendable
|
||||
{
|
||||
/// Pending jobs and the armed flag, guarded by a `Mutex` because `enqueue`
|
||||
/// is called from cooperative-pool threads.
|
||||
|
|
@ -210,10 +215,14 @@ nonisolated func portico_executor_destroy_notify(_ data: UnsafeMutableRawPointer
|
|||
public func isIsolatingCurrentContext() -> Bool? { porticoIsMainThread() }
|
||||
}
|
||||
|
||||
#if canImport(Glibc)
|
||||
// MARK: - Factory
|
||||
|
||||
/// Installs ``GLibMainExecutor`` as the process main executor. The concurrent
|
||||
/// pool is left as the stdlib's platform default.
|
||||
/// Installs ``GLibMainExecutor`` as the process main executor via the
|
||||
/// stdlib's `@_spi(ExperimentalCustomExecutors)` API. Linux only - see
|
||||
/// ``DarwinMainQueuePump`` for the Darwin equivalent, which cannot use this
|
||||
/// mechanism because Apple does not ship the private module interface the
|
||||
/// SPI needs. The concurrent pool is left as the stdlib's platform default.
|
||||
nonisolated struct GLibExecutorFactory: ExecutorFactory {
|
||||
static let mainExecutor: any MainExecutor = GLibMainExecutor()
|
||||
static let defaultExecutor: any TaskExecutor = PlatformExecutorFactory.defaultExecutor
|
||||
|
|
@ -230,3 +239,4 @@ nonisolated struct GLibExecutorFactory: ExecutorFactory {
|
|||
/// call from any thread before `g_application_run`.
|
||||
static func install() { _ = installOnce }
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue