AparokshaUI / adwaita-swift

A framework for creating user interfaces for GNOME with an API similar to SwiftUI
https://aparokshaui.github.io/adwaita-swift/
MIT License
749 stars 16 forks source link

`DispatchQueue.main` not working (on Windows) #38

Open s-k opened 1 week ago

s-k commented 1 week ago

Describe the bug

Closures passed to DispatchQueue.main.async will never be called. Similarly, Swift Concurrency Tasks scheduled to run on @MainActor are never called.

To Reproduce

These print statements are never executed when using Adwaita for Swift on Windows:

DispatchQueue.main.async {
  print("Hi!") // Never called
}
Task { @MainActor in
  print("Hi, too!") // Never called
}

Expected behavior

I expect that the Main Dispatch Queue and the Main Actor can be used normally. Alternatively, the library should surface a similar API to run code on the Main Thread.

Additional context

I assume, to make this work, the GTK run loop must be integrated with libdispatch. Maybe this can be helpful: https://stackoverflow.com/questions/10291972/integrating-a-custom-run-loop-with-libdispatch

david-swift commented 1 week ago

Thanks for opening the issue! I will look into this at some point. Currently, you can use Idle which calls GLib.idle_add to add functions to the main context (e.g. for updating the UI).

s-k commented 1 week ago

@david-swift Thanks for your response! Interestingly, I have been working on a workaround and stumbled on g_idle_add myself.

Here is my workaround for anyone interested. It works but is obviously not optimal:

import CAdw
import Foundation

@globalActor
public struct MainActor {
    public actor Actor {
        public nonisolated var unownedExecutor: UnownedSerialExecutor {
            Executor.sharedUnownedExecutor
        }
    }

    final class Executor: SerialExecutor {
        static var sharedUnownedExecutor: UnownedSerialExecutor = UnownedSerialExecutor(ordinary: Executor())

        private class JobWrapper {
            let job: UnownedJob

            init(_ job: consuming ExecutorJob) {
                self.job = UnownedJob(job)
            }
        }

        private init() {}

        func enqueue(_ job: consuming ExecutorJob) {
            let wrappedJob = JobWrapper(job)
            g_idle_add({ jobPointer in
                let job = Unmanaged<JobWrapper>.fromOpaque(jobPointer!).takeRetainedValue().job
                job.runSynchronously(on: MainActor.Executor.sharedUnownedExecutor)
                return 0
            }, Unmanaged.passRetained(wrappedJob).toOpaque())
        }

        func asUnownedSerialExecutor() -> UnownedSerialExecutor {
            UnownedSerialExecutor(ordinary: self)
        }
    }

    public static let shared = Actor()

    @available(*, noasync)
    public static func assumeIsolated<T>(
        _ operation: @MainActor () throws -> T,
        file: StaticString = #fileID,
        line: UInt = #line
    ) rethrows -> T {
        guard Thread.isMainThread else {
            fatalError("`MainActor.assumeIsolated` was called from a non-main thread", file: file, line: line)
        }

        typealias UnconstrainedOperation = () throws -> T

        return try withoutActuallyEscaping(operation) { (operation) -> T in
            let unconstrainedOperation = unsafeBitCast(operation, to: UnconstrainedOperation.self)
            return try unconstrainedOperation()
        }
    }
}

This creates a new MainActor which can be used as a replacement for the built-in MainActor to make Swift Concurrency work with Adwaita for Swift on Windows.