This is something I've discovered like 18 months ago or so, but I had local workaround that was ok at the time.
Some context: I have a pet project, where I'm writing UI toolkit for Linux in swift. It was fairly obvious for me to use RunLoop as, you know, run loop :) In order to integrate with X11 window system, both xlib and xcb expose the file descriptor which is used by X11 to send events to client. CFRunLoop on Linux is using epoll file descriptors to wait on events from eventfd or timerfd to provide same functionality as it does on Darwin. For example, _wakeUpPort is eventfd. When run loop needs to be awaken, code writes 8 integer bytes with value of 1 to this fd. Then code reads those exact same 8 bytes in order to "reset" this file descriptor.
The issue I see is that when I'd like to listen to that file descriptor from xlib or xcb in CFRunLoop via creating CFRunLoopSource from CFRunLoopSourceContext1, the code that "reads 8 bytes" from file descriptor is also being executed on this file descriptor. Which, you know, makes data provided from X11 malformed. It actually detects that and aborts application execution.
All of that was ok, because I used FileHandle from Foundation to watch on this file descriptor via waitForDataInBackgroundAndNotify. But now I am starting to work on integration with libinput which has a very critical limitation: event received from libinput has to be handled as soon as it arrived. With this limitation I can not proceed with the project and looking for help how to mitigate the issue. I've filed a task SR-14920 and attached a sample project to reproduce the issue. I've also started to poke around possible fixes, but this is wrong and will disable timers handling by runloop. Basically, i wanted to ask for opinions from community before i proceed in trying to change anything.
I don't think it makes sense to put bare fds as ports for the runloop. This is because, as you've already observed that these are only eventfds and timerfds in the runloop internals; it's not really a bug per se. Furthermore, the semantics described in the documentation suggest that the runloop expects plain signalling rather than the select style readable/writable/exceptional set. You would need to create a runloop source that adapts filehandle events to runloop signals -- as you note, that's what FileHandle (presumably? I have not looked at that closely) does.
If you're needing to handle the libinput events at a higher priority than other events, it appears CFRunLoop doesn't offer you that functionality and maybe that API is a bad fit for you. Maybe trying to hook into Dispatch directly (as others mention) might be more useful, but it may not be any easier than writing a hot loop with XNextEvent for now and trying to figure this all out when you have the basics of your library in place.
Hey Cory, thanks for reply
Assuming you are talking about makeFileSystemObjectSource method. It would be viable for my current use case, but it does not suit me with libinput. DispatchSource is asynchronous by it's nature because it places the handlers into provided dispatch_queue. This, unfortunately, would not work long term. Also CFRunLoop provides all sorts of interesting things, like nesting run loops and custom run loop modes which simplifies prioritizing of events by a lot
Hey 3405691582 (sorry, don't know your name), thanks for reply
This is because, as you've already observed that these are only eventfd s and timerfd s in the runloop internals; it's not really a bug per se .
Yea, now I see this point. Linux has... too many file descriptors :) I guess my expectations are incorrect since fd exposed by xlib or xcb is not eventfd, but rather an io socket.
You would need to create a runloop source that adapts filehandle events to runloop signals -- as you note, that's what FileHandle (presumably? I have not looked at that closely) does.
That's exactly what I did when I found that CFRunLoop does not work as i expected. I literally started a background thread and watched X fd in it via epoll and signaled another eventfd (i.e. replicated what CFRunLoop does internally) which I placed in CFRunLoop via CFRunLoopSourceContext1. And then I discovered that Foundation team is very nice and ported FileHandle to Linux which does exactly the same thing (i.e. starts background thread and watches fd).
but it may not be any easier than writing a hot loop with XNextEvent for now and trying to figure this all out when you have the basics of your library in place.
I'm exactly at this stage of the project when it's time to figure this out. It's been a year and a half since I started this thing as a pet project and I feel like it's matured enough to publish it (if my employer will grant me that permission, because you know, contracts)
To be honest, deep inside I want to have Linux equivalent of CFMachPort with a difference being that the provided callback is responsible for reading from file descriptor rather than CFRunLoop doing that.
I do understand that there's a fundamental difference between mach_port_t, which operates with messages, and Linux file descriptors, which just do raw bytes. And mach_port_t unified behavior exactly allows CFRunLoop behavior on Darwin platforms, i.e. CFRunLoop does all the "heavy lifting" for it's clients
I just realized there's something could be done. Watching a file descriptor in Linux is a very common task, basically it's the standard for "run loop" entities in any library that exposes run lops (gtk, qt, weston, libinput, xlib, xcb, you name it). It is possible to create CoreFoundation types representing eventfd, timerfd, socket, any other fd and have those integrated in CFRunLoop. Foundation would have the wrapping types that would be exposed to work with RunLoop. Do you think that's a viable option? I could create a pitch and implement it
Given that Swift is working towards formalizing a async/await-concurrency based model and subsequently privileging Dispatch suggests radical changes to CF would not be much of a priority. There might be scope to facilitate improvements to Foundation at the Swift level, though, but as we saw, we do have a FileHandle mechanism to integrate into the runloop.
That being said, privileging eventfds/timerfds/whatever to some sort of exposed formal API seems like a bad idea for portability in any case.
I do understand the goals to move to async/await coroutine model, but on linux there are a lot of APIs that will not alight well with it. libinput is only one example
Here's the quote from it's documentation:
libinput exposes a single file descriptor to the caller. This file descriptor should be monitored by the caller, whenever data is available the caller must immediately call libinput_dispatch(). Failure to do so will result in erroneous behavior.
libinput_dispatch() may result in one or more events being available to the caller. After libinput_dispatch() a caller should call libinput_get_event() to retrieve and process this event. Whenever libinput_get_event() returns NULL, no further events are available.
Any async model is just something that can not be used here, including the FileHandle fd watch watcher
Lots of things align well with async/await, they just need to be bridged into it. SwiftNIO is a useful example of this: we have already provided a bunch of wrapping APIs that expose async/await surface area for SwiftNIO, and there will be more to come.
Yup, I've got a lot of inspiration for a lot of things from SwitNIO code. You are right in the fact that lots of things alight well with cooperating multitasking. But not all of them. My example is just only single one, but I am pretty sure there are much more.