portico/Sources/Portico/MainLoop/GLibMainExecutor.swift
Brendan Szymanski e9e56e28d3 Declare MainExecutor conformance explicitly for GLibMainExecutor
swift run/build failed on this toolchain (stock swift.org 6.3.3 aarch64 Linux) with:

  GLibMainExecutor.swift:227: error: value of type 'GLibMainExecutor' does
  not conform to specified type 'MainExecutor'

despite the class already implementing every method MainExecutor requires
(enqueue, run, runUntil, stop) via its separate RunLoopExecutor and
SerialExecutor conformances. Root cause: Swift protocol refinement is
one-directional. MainExecutor : RunLoopExecutor, SerialExecutor adds no
requirements of its own, but conforming to the two parent protocols
separately does not implicitly satisfy the refined protocol - the
existential conversion to 'any MainExecutor' needs an explicit conformance
declaration for MainExecutor itself, confirmed with a minimal repro against
the stdlib's private _Concurrency interface.

Fix: add 'extension GLibMainExecutor: MainExecutor {}' (no new members
needed - the class already satisfies every requirement), scoped inside the
existing '#if canImport(Glibc)' block. Kept off Darwin deliberately:
MainExecutor carries a much higher Darwin '@available' floor than this
package targets, and GLibMainExecutor is only ever installed as the process
main executor on Linux (DarwinMainQueuePump is the Apple equivalent) - on
Darwin it is used solely as a plain SerialExecutor for individual actors,
which does not require MainExecutor conformance.

Verified: 'swift build' and 'swift test' both clean (286/286 tests) in this
package standalone, and a full 'swift build' of Luminate against this
checkout via 'swift package edit' succeeds end to end.
2026-08-12 21:45:03 -04:00

254 lines
11 KiB
Swift

#if canImport(Glibc)
@_spi(ExperimentalCustomExecutors) import _Concurrency
#endif
import Synchronization
#if canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#endif
// MARK: - Raw GLib entry points
/// Creates a new idle source. The source will not initially be associated with any
/// `GMainContext` and must be added to one with `portico_g_source_attach`.
@_silgen_name("g_idle_source_new")
nonisolated func portico_g_idle_source_new() -> UnsafeMutableRawPointer
/// Decreases the reference count of `source`. When the reference count reaches
/// zero the source is freed. Call after `portico_g_source_attach` - the context
/// holds its own reference.
@_silgen_name("g_source_unref")
nonisolated func portico_g_source_unref(_ source: UnsafeMutableRawPointer)
/// Creates a new `GMainLoop` for the given main context. Pass `nil` for the
/// default context. `isRunning` should be `0` to create the loop in a stopped
/// state.
@_silgen_name("g_main_loop_new")
nonisolated func portico_g_main_loop_new(
_ context: UnsafeMutableRawPointer?, _ isRunning: Int32
) -> UnsafeMutableRawPointer
/// Runs a main loop until ``portico_g_main_loop_quit`` is called on it.
/// Blocks the calling thread. Portico never calls this; `g_application_run`
/// owns the blocking loop and the executor is only installed into it.
@_silgen_name("g_main_loop_run")
nonisolated func portico_g_main_loop_run(_ loop: UnsafeMutableRawPointer)
/// Stops the given main loop. Idempotent; safe from any thread.
@_silgen_name("g_main_loop_quit")
nonisolated func portico_g_main_loop_quit(_ loop: UnsafeMutableRawPointer)
/// Decreases the reference count on the main loop. When the count reaches
/// zero the loop is freed.
@_silgen_name("g_main_loop_unref")
nonisolated func portico_g_main_loop_unref(_ loop: UnsafeMutableRawPointer)
/// Runs a single iteration for the given main context, blocking if `mayBlock`
/// is nonzero. Returns `1` if events were dispatched.
@_silgen_name("g_main_context_iteration")
nonisolated func portico_g_main_context_iteration(
_ context: UnsafeMutableRawPointer?, _ mayBlock: Int32
) -> Int32
// MARK: - Main-thread predicate
/// Returns `true` only on the process's initial thread - the thread that runs
/// `main`, owns Swift's `MainActor`, and blocks inside `g_application_run`.
///
/// Deliberately does not accept ownership of the default `GMainContext`: a
/// worker can acquire that context, but it is not the GTK main thread.
#if canImport(Darwin)
nonisolated func porticoIsMainThread() -> Bool { pthread_main_np() != 0 }
#else
@_silgen_name("gettid")
nonisolated func portico_gettid() -> Int32
nonisolated func porticoIsMainThread() -> Bool { portico_gettid() == getpid() }
#endif
// MARK: - Executor drain trampoline
/// `GSourceFunc` trampoline for the idle source that drains Swift jobs.
///
/// The source owns a +1 on the executor, released by
/// ``portico_executor_destroy_notify``. Returns `G_SOURCE_REMOVE` (0) because
/// the source is one-shot - each batch arms a fresh source.
@_cdecl("portico_executor_drain")
nonisolated func portico_executor_drain(_ data: UnsafeMutableRawPointer?) -> Int32 {
guard let data else { return 0 }
Unmanaged<GLibMainExecutor>.fromOpaque(data)
.takeUnretainedValue()
.drainPendingJobs()
return 0
}
/// `GDestroyNotify` for the executor drain source. Releases the +1 handed to
/// GLib by ``GLibMainExecutor/arm()``.
@_cdecl("portico_executor_destroy_notify")
nonisolated func portico_executor_destroy_notify(_ data: UnsafeMutableRawPointer?) {
guard let data else { return }
Unmanaged<GLibMainExecutor>.fromOpaque(data).release()
}
// MARK: - Main executor
/// 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
/// queue and runs each job on the main actor; a task that re-enqueues on every
/// resumption therefore yields to the GTK main loop between batches instead of
/// monopolising it.
///
/// 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:
SerialExecutor, Sendable
{
/// Pending jobs and the armed flag, guarded by a `Mutex` because `enqueue`
/// is called from cooperative-pool threads.
private struct Pending {
var jobs: [UnownedJob] = []
var armed = false
}
private let pending = Mutex(Pending())
/// Bit-pattern of the `GMainLoop` pointer set by `run()`, stored in an
/// `Atomic` because `UnsafeMutableRawPointer` is not `Sendable` under
/// `StrictConcurrency=complete`.
private let loopAddress = Atomic<UInt>(0)
/// Creates a main executor draining through the default `GMainContext`.
@_spi(Portico) public init() {}
/// Enqueues `job` for execution on the main actor. Thread-safe; may be
/// called from any cooperative-pool thread.
public func enqueue(_ job: consuming ExecutorJob) {
let unowned = UnownedJob(job)
let needsArm = pending.withLock { p -> Bool in
p.jobs.append(unowned)
if p.armed { return false }
p.armed = true
return true
}
if needsArm { arm() }
}
/// Attaches a one-shot idle source to the default main context. Safe from
/// any thread: `g_source_attach` takes the context lock and wakes any
/// blocking `g_main_context_iteration` / `g_main_loop_run`.
private func arm() {
let source = portico_g_idle_source_new()
portico_g_source_set_priority(source, SourcePriority.defaultIdle.rawValue)
let notify: @convention(c) (UnsafeMutableRawPointer?) -> Void =
portico_executor_destroy_notify
portico_g_source_set_callback(
source, portico_executor_drain,
Unmanaged.passRetained(self).toOpaque(), notify
)
_ = portico_g_source_attach(source, nil)
portico_g_source_unref(source)
}
/// Runs the jobs queued when this source was armed - a snapshot, never a
/// drain-until-empty loop, so a task that re-enqueues on every resumption
/// yields to the main loop between batches instead of monopolising it.
func drainPendingJobs() {
let batch = pending.withLock { p -> [UnownedJob] in
p.armed = false
let batch = p.jobs
p.jobs.removeAll(keepingCapacity: true)
return batch
}
for job in batch { job.runSynchronously(on: asUnownedSerialExecutor()) }
}
/// Runs a blocked `GMainLoop` on the default context. Not reentrant.
/// Portico never calls this - ``PorticoRuntime/run(_:)`` blocks inside
/// `g_application_run` instead - but the executor remains usable as a
/// standalone loop for testing or a non-GTK tool.
public func run() throws {
precondition(
loopAddress.load(ordering: .acquiring) == 0,
"GLibMainExecutor.run() is not reentrant"
)
let loop = portico_g_main_loop_new(nil, 0)
loopAddress.store(UInt(bitPattern: loop), ordering: .releasing)
portico_g_main_loop_run(loop)
loopAddress.store(0, ordering: .releasing)
portico_g_main_loop_unref(loop)
}
/// Iterates the default main context until `condition` returns `true`.
/// Common in tests: create some idle/timeout sources, then
/// `runUntil { gotExpectedResult }`.
public func runUntil(_ condition: () -> Bool) throws {
while !condition() { _ = portico_g_main_context_iteration(nil, 1) }
}
/// Quits the `GMainLoop` started by ``run()``. Idempotent; does nothing
/// unless `run()` is currently blocking. Portico never calls this -
/// app shutdown is GTK's business.
public func stop() {
let address = loopAddress.load(ordering: .acquiring)
guard address != 0, let loop = UnsafeMutableRawPointer(bitPattern: address)
else { return }
portico_g_main_loop_quit(loop)
}
/// Precondition that the calling thread is the GLib main thread.
/// This is the gate that keeps `MainActor.assumeIsolated` in Portico's
/// GLib trampolines sound.
public func checkIsolated() {
precondition(porticoIsMainThread(), "not on the GLib main thread")
}
/// Returns `true` on the GLib main thread, `false` otherwise.
public func isIsolatingCurrentContext() -> Bool? { porticoIsMainThread() }
}
#if canImport(Glibc)
/// `MainExecutor` refines `RunLoopExecutor & SerialExecutor` without adding
/// further requirements, but Swift protocol refinement is one-directional:
/// conforming to the two parent protocols separately does not implicitly
/// satisfy the refined protocol, so the existential conversion to
/// `any MainExecutor` below needs this explicit (requirement-free)
/// conformance declaration. Scoped to Linux because `MainExecutor` carries a
/// high Darwin `@available` floor that `GLibMainExecutor` should not
/// inherit on Apple platforms, where it is used only as a plain
/// `SerialExecutor` and never installed as the process main executor.
extension GLibMainExecutor: MainExecutor {}
// MARK: - Factory
/// 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
/// Guards the `_createExecutors` call; the lazy static makes repeat calls
/// free so it is safe to call ``install()`` more than once.
private static let installOnce: Bool = {
_createExecutors(factory: GLibExecutorFactory.self)
return true
}()
/// Installs ``GLibMainExecutor`` as the process main executor if it has
/// not already been installed this process lifetime. Idempotent; safe to
/// call from any thread before `g_application_run`.
static func install() { _ = installOnce }
}
#endif