#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.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.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(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