191 lines
7.6 KiB
Swift
191 lines
7.6 KiB
Swift
@_spi(SGTKInternal) import GLib
|
|
|
|
// MARK: - Callback box
|
|
|
|
/// Retained owner of a GLib source callback. GLib holds exactly one strong
|
|
/// reference to this box, released by the source's `GDestroyNotify`.
|
|
@MainActor final class SourceCallbackBox {
|
|
private let body: @MainActor (SourceId) -> Void
|
|
private let repeats: Bool
|
|
private let owner: SourceId
|
|
|
|
init(owner: SourceId, repeats: Bool, body: @escaping @MainActor (SourceId) -> Void) {
|
|
self.owner = owner
|
|
self.repeats = repeats
|
|
self.body = body
|
|
}
|
|
|
|
/// Runs the callback. Returns `true` for `G_SOURCE_CONTINUE`, `false` for
|
|
/// `G_SOURCE_REMOVE`.
|
|
///
|
|
/// Pins locals before calling `body` so that a body which removes its own
|
|
/// source (triggering synchronous `g_source_destroy` -> `GDestroyNotify` ->
|
|
/// box deallocation) does not cause use-after-free when this method reads
|
|
/// `repeats` and `owner.isRemoved` afterward.
|
|
func invoke() -> Bool {
|
|
let owner = self.owner // local strong ref survives box dealloc
|
|
let repeats = self.repeats
|
|
body(owner)
|
|
return repeats && !owner.isRemoved
|
|
}
|
|
}
|
|
|
|
// MARK: - Trampolines
|
|
|
|
/// GLib `GSourceFunc` trampoline. Bridges the C callback back to the main
|
|
/// actor via `MainActor.assumeIsolated`. Dispatch runs on the thread pumping
|
|
/// the GLib main context, which must be the process's main thread.
|
|
@_cdecl("portico_source_dispatch")
|
|
nonisolated func portico_source_dispatch(_ data: UnsafeMutableRawPointer?) -> Int32 {
|
|
guard let data else { return 0 }
|
|
let box = Unmanaged<SourceCallbackBox>.fromOpaque(data).takeUnretainedValue()
|
|
return withExtendedLifetime(box) {
|
|
MainActor.assumeIsolated { box.invoke() ? 1 : 0 }
|
|
}
|
|
}
|
|
|
|
/// GLib `GDestroyNotify` trampoline. Releases the +1 retain that GLib holds
|
|
/// on the callback box.
|
|
///
|
|
/// Destroy-notify may run on any thread, so this is a plain nonisolated
|
|
/// refcount release rather than a `MainActor.assumeIsolated` call.
|
|
@_cdecl("portico_source_destroy_notify")
|
|
nonisolated func portico_source_destroy_notify(_ data: UnsafeMutableRawPointer?) {
|
|
guard let data else { return }
|
|
Unmanaged<SourceCallbackBox>.fromOpaque(data).release()
|
|
}
|
|
|
|
// MARK: - Duration conversion
|
|
|
|
/// Converts a `Duration` to whole GLib milliseconds. Negative durations clamp
|
|
/// to `0`; a non-zero sub-millisecond remainder rounds up to `1`; values beyond
|
|
/// `UInt32.max` milliseconds clamp to `UInt32.max`.
|
|
@_spi(Portico) public func _clampedMilliseconds(_ duration: Duration) -> UInt32 {
|
|
let c = duration.components
|
|
if c.seconds < 0 || c.attoseconds < 0 { return 0 }
|
|
let attosecondsPerMillisecond: Int64 = 1_000_000_000_000_000
|
|
guard c.seconds <= Int64(UInt32.max) / 1000 else { return UInt32.max }
|
|
var total = c.seconds * 1000 + c.attoseconds / attosecondsPerMillisecond
|
|
if c.attoseconds % attosecondsPerMillisecond > 0 { total += 1 }
|
|
return total > Int64(UInt32.max) ? UInt32.max : UInt32(total)
|
|
}
|
|
|
|
// MARK: - Attach helper
|
|
|
|
/// Sets the priority and callback on `source`, attaches it to the default main
|
|
/// context, and returns its handle.
|
|
@MainActor
|
|
private func attachSource(
|
|
_ source: GLib.Source,
|
|
priority: SourcePriority,
|
|
repeats: Bool,
|
|
body: @escaping @MainActor (SourceId) -> Void
|
|
) -> SourceId {
|
|
let id = SourceId(source: source)
|
|
let box = SourceCallbackBox(owner: id, repeats: repeats, body: body)
|
|
let dispatch: @convention(c) (UnsafeMutableRawPointer?) -> Int32 = portico_source_dispatch
|
|
let notify: @convention(c) (UnsafeMutableRawPointer?) -> Void = portico_source_destroy_notify
|
|
let ptr = source.pointer
|
|
portico_g_source_set_priority(ptr, priority.rawValue)
|
|
portico_g_source_set_callback(ptr, dispatch, Unmanaged.passRetained(box).toOpaque(), notify)
|
|
id.rawValue = portico_g_source_attach(ptr, nil)
|
|
return id
|
|
}
|
|
|
|
// MARK: - Idle
|
|
|
|
/// Runs `body` when the GLib main loop has no higher-priority work pending.
|
|
///
|
|
/// Attaches a `g_idle_source_new` source to the default main context. The
|
|
/// closure receives the source's own ``SourceId`` so it can stop itself.
|
|
///
|
|
/// - Parameters:
|
|
/// - priority: Source priority. Defaults to ``SourcePriority/defaultIdle``.
|
|
/// - repeats: When `true`, `body` runs on every idle iteration until removed.
|
|
/// Defaults to `false` (run once). A repeating idle source keeps the main
|
|
/// loop from sleeping - prefer `Timeout` for periodic work.
|
|
/// - body: The work to perform on the main thread.
|
|
/// - Returns: A handle that removes the source; discardable.
|
|
@MainActor @discardableResult
|
|
public func Idle(
|
|
priority: SourcePriority = .defaultIdle,
|
|
repeats: Bool = false,
|
|
_ body: @escaping @MainActor (SourceId) -> Void
|
|
) -> SourceId {
|
|
attachSource(GLib.idleSourceNew(), priority: priority, repeats: repeats, body: body)
|
|
}
|
|
|
|
/// Runs `body` when the GLib main loop has no higher-priority work pending.
|
|
///
|
|
/// Convenience overload for closures that do not need the source handle.
|
|
///
|
|
/// - Parameters:
|
|
/// - priority: Source priority. Defaults to ``SourcePriority/defaultIdle``.
|
|
/// - repeats: When `true`, `body` runs on every idle iteration until removed.
|
|
/// Defaults to `false` (run once).
|
|
/// - body: The work to perform on the main thread.
|
|
/// - Returns: A handle that removes the source; discardable.
|
|
@MainActor @discardableResult
|
|
public func Idle(
|
|
priority: SourcePriority = .defaultIdle,
|
|
repeats: Bool = false,
|
|
_ body: @escaping @MainActor () -> Void
|
|
) -> SourceId {
|
|
Idle(priority: priority, repeats: repeats) { _ in body() }
|
|
}
|
|
|
|
// MARK: - Timeout
|
|
|
|
/// Runs `body` repeatedly at `interval` on the GLib main loop.
|
|
///
|
|
/// Attaches a `g_timeout_source_new` source to the default main context. The
|
|
/// interval is monotonic and millisecond-granular; GLib may delay a fire when
|
|
/// higher-priority sources are busy and does not try to catch up lost time, so
|
|
/// do not rely on this for precise timing. The first fire is one `interval`
|
|
/// after attach.
|
|
///
|
|
/// - Parameters:
|
|
/// - interval: Time between fires. Rounded up to whole milliseconds; negative
|
|
/// values clamp to `0`.
|
|
/// - priority: Source priority. Defaults to ``SourcePriority/default``.
|
|
/// - repeats: When `false`, the source is removed after the first fire.
|
|
/// Defaults to `true`.
|
|
/// - body: The work to perform on the main thread; receives the source's own
|
|
/// ``SourceId`` so it can stop itself.
|
|
/// - Returns: A handle that removes the source; discardable.
|
|
@MainActor @discardableResult
|
|
public func Timeout(
|
|
interval: Duration,
|
|
priority: SourcePriority = .default,
|
|
repeats: Bool = true,
|
|
_ body: @escaping @MainActor (SourceId) -> Void
|
|
) -> SourceId {
|
|
attachSource(
|
|
GLib.timeoutSourceNew(interval: _clampedMilliseconds(interval)),
|
|
priority: priority,
|
|
repeats: repeats,
|
|
body: body
|
|
)
|
|
}
|
|
|
|
/// Runs `body` repeatedly at `interval` on the GLib main loop.
|
|
///
|
|
/// Convenience overload for closures that do not need the source handle.
|
|
///
|
|
/// - Parameters:
|
|
/// - interval: Time between fires. See ``Timeout(interval:priority:repeats:_:)``
|
|
/// for granularity and drift behavior.
|
|
/// - priority: Source priority. Defaults to ``SourcePriority/default``.
|
|
/// - repeats: When `false`, the source is removed after the first fire.
|
|
/// Defaults to `true`.
|
|
/// - body: The work to perform on the main thread.
|
|
/// - Returns: A handle that removes the source; discardable.
|
|
@MainActor @discardableResult
|
|
public func Timeout(
|
|
interval: Duration,
|
|
priority: SourcePriority = .default,
|
|
repeats: Bool = true,
|
|
_ body: @escaping @MainActor () -> Void
|
|
) -> SourceId {
|
|
Timeout(interval: interval, priority: priority, repeats: repeats) { _ in body() }
|
|
}
|