My library for Swift-Node.js interop, node-swift, has an issue where Node's libuv runloop starves the main thread, preventing MainActor
tasks from running. I have a WIP solution that uses GCD SPI but the pitched API would provide a more stable solution. Big fan.
I do have a couple questions regarding gaps in the current feature set. I imagine much of this will be out of scope of this pitch but would be good to know whether it's something that might be addressed in the language in the future.
Polling
An important function of application run loops is to provide polling functionality — after all, run loop implementations (including CoreFoundation, GLib, and libuv) spend most of their time blocking on a select
/poll
call or similar.
The proposed SchedulingExecutor
is a good solution for time-based events, but it would be great to see a similar API that allows executors to watch a file descriptor/Win32 handle/Mach port. Perhaps something like this?
protocol PollingExecutor: Executor {
#if os(Windows)
typealias PollTarget = HANDLE
#elseif canImport(Darwin)
typealias PollTarget = mach_port_t
#else
typealias PollTarget = CInt // file descriptor
#endif
func enqueue(
_ job: consuming ExecutorJob,
whenReadable target: PollTarget
)
}
(aside: it would be nicer if we could use System.FileDescriptor
and System.Mach.Port
here for the unix cases but I imagine that's not possible due to dependency inversion. Also, if we aren't comfortable using Mach ports in public APIs it's possible to wrap the port in a file descriptor on the runtime side with EVFILT_MACHPORT
.)
Looking ahead, this would be very useful for implementing APIs for async IO without a direct GCD/CF dependency. It could also be very useful if you do want to support GCD/CF, which brings me to…
Handling GCD and CF sources
I imagine a custom main executor would result in lower level CFRunLoop
/DispatchQueue
ports/sources/blocks not being serviced. It would be great to see this problem addressed in the current pitch at least for common cases like DispatchQueue.main.async
, which shows up in tons of Swift code today. Perhaps that particular case could be hard-coded to forward to the MainExecutor
?
This is also especially relevant on Darwin where a lot of system libraries (all the way from Foundation to SwiftUI) count on the existence of a main CFRunLoop
/DispatchQueue
. Further complicating matters, many of those instances will probably not be covered by special-casing DispatchQueue.main.async
, whether that's because they use the IO features or more esoteric things like RunLoop modes. This is another instance where PollingExecutor
could provide a solution. Specifically, if CF could wrap the RunLoop's __CFPortSet
in a file descriptor/[HANDLE]
and pass it to the PollingExecutor
, we'd be able to have our cake and eat it too: a custom executor that's also able to service CoreFoundation and libdispatch!
Side note: my suggestion is motivated by the fact that this is exactly how GCD plugs into CF, i.e. GCD gives CF 1) a handle that it can poll and 2) a function it can call to wake GCD back up. This is the GCD SPI I mentioned at the top of my post.