Using your own main loop on OSX
It seems that every dev environment/framework wants to be in charge of your program’s main loop.
Prompted by a question of how to add an optional non-blocking GUI to an OSX networking daemon, I thought I would investigate how you can wrestle back control of your main loop from Apple.
First some understanding of how Apple’s recommended main loop fits together, there are four relevant layers (in order of what uses what):
- NSRunLoop: the main loop class used by Cocoa etc.
- CFRunLoop: part of Core Foundation
- libdispatch: also known as “Grand Central Dispatch”
- mach ports: the kernel objects used for communication
In the dispatch_get_main_queue man page you can find:
Cocoa applications need not call dispatch_main(). Blocks submitted to the main queue will be executed as part of the “common modes” of the application’s main NSRunLoop or CFRunLoop. However, blocks submitted to the main queue in applications using dispatch_main() are not guaranteed to execute on the main thread.
From this you can infer that CFRunLoop is getting notified of events, and has a way to control processing them.
This brings us to my key discovery: the undocumented functions _dispatch_get_main_queue_port_4CF() and _dispatch_main_queue_callback_4CF(). These functions are used to retrieve and process messages from the mach port that receives events scheduled with libdispatch. These functions are part of an undistributed header dispatch/private.h, so you’ll need to declare them yourself.
Now with access to the underlying mach port we have a kernel primitive that can be used to drive your own main loop. You could build a main loop that simply uses mach_msg() to wait for messages from libdispatch. However on unix systems you usually want a file descriptor to use with poll() or select(). Fortunately, there is another primitive to help us out here: kqueue() using EVFILT_MACHPORT.
Annoyingly, OSX doesn’t support using a mach port directly with EVFILT_MACHPORT, only a mach port set (a fact only mentioned in the sys/event.h header!). So you need to wrap the mach port in a mach port set, and hand that to kevent.
I did some work on a demo/example over here: https://gist.github.com/daurnimator/8cc2ef09ad72a5577b66f34957559e47
The above research and inferences were made without access to a mac. Thanks to Peter van Dijk for testing the demo/example.