In this blog post, we will have a look at the “main loop”, which is the engine of most Graphical User Interface (GUI) libraries. Different GUI libraries implement their own main loop. When porting a GUI library to another platform, such as porting GTK+ to OS X, the main loops of GTK+ and the native toolkit of the platform need to be integrated. During this summer, Lanedo has looked into a number of issues with this integration of the GTK+ and OS X main loops. As part of this, we had to really delve into the internals of the GTK+ main loop. This blog post describes what we have learned.
Modern GUI libraries have in common that they all embody the Event-based Programming paradigm. These libraries implement GUI elements that draw output to a computer screen and change state in response to incoming events. Events are generated from different sources. The majority of events are typically generated directly from user input, such as mouse movements and keyboard input. Other events are generated by the windowing system, for instance requests to redraw a certain area of a GUI, indications that a window has changed size or notifications of changes to the session’s clipboard. Note that some of these events are generated indirectly by user input.
In general, libraries that employ the Event-based Programming paradigm rely on main loops to make sure input is received and events are dispatched. To receive input, main loops poll input sources. Examples of input sources are input drivers for mouse and keyboard or sources to receive specific window manager events. For instance, the DirectFB library supports obtaining input from multiple different input drivers, such as tslib, lirc and the Linux Input subsystem. As another example, GTK+ when using the X11 backend receives all input in the form of XEvents from the X11 libraries. This can be seen as a single input source. The X11 system already abstracts different event sources behind a single event interface. XEvents are defined for mouse and keyboard input and for window management events such as notifications for changes to window size.
A GUI library uses events internally. Controls that are implemented in the GUI library thus set up “event handlers” to process incoming events. The GUI library generates these events from input it gets. So, a translation takes place from received input to an event that is dispatched.
Anatomy of a main loop
Main loops are in charge of receiving input and dispatching events. As an example, we will briefly describe the anatomy of the GLib main loop. The GLib main loop manages a collection of “event sources”. It is easy to add your own event sources, but this is outside the scope of this blog post. An event source consists out of a number of callback functions: prepare, check and dispatch. Next to these callback functions, a GSource contains information on which file descriptors to poll for this source and a poll timeout. All the GLib main loop does is monitoring these event sources and calling dispatch when an event has occurred. This is done by going through five phases in a loop:
- prepare: prepares the sources to be polled, using the prepare callback on the event sources. An event source has to prepare to be polled but may also indicate it does not have to be polled (for instance, when it knows already that there’s an event waiting).
- query: determines all information that is necessary to perform the poll operation for a main loop. For example, an array of file descriptors to be polled is set up.
- poll: actually performs the poll operation, which waits for an event to happen on a file descriptor until a given timeout expires. As a timeout the smallest timeout specified by the monitored event sources is used.
- check: the results of the poll operation are written into the poll records provided by the sources and the event source is requested to check whether an event has happened by calling its check callback.
- dispatch: the dispatch callback is called for event sources that indicated an event has occurred.
As an example event source, consider the event source installed by GDK to listen for X11 events. The file descriptor by which communication with the X server is carried out is associated with this event source. The event source implements the different callbacks as follows:
- prepare: checks whether any XEvents already received from the X server are still pending in the event queue. If this is the case, then there is no need to include this event source in the poll.
- check: if an input event has occurred on the registered file descriptor, it checks whether there now is an event present in the event queue.
- dispatch: get new XEvents, unqueue an event and dispatch this event by calling gdk_event_func.
Main loops can commonly also be invoked recursively. A common example is when showing a modal dialog window from within a callback function or when updating a progress bar while a large computation is being processed (e.g. GIMP applying a filter to an image) within a callback function. In these cases, the main loop is run periodically so that the flow of events does not get stuck. The GLib main loop supports this kind of recursion from within its dispatch stage.
Integration of main loops
We now have a good idea of what the responsibilities of a main loop look like and how a main loop functions. Different toolkits, or libraries, implement their own main loops. For instance, GTK+, Qt and EFL all implement their own main loop. What happens if we want to use two libraries with each other, that implement their own main loop, without too much pain?
As a first example, consider that we are writing an application using EFL but want to use a library that uses the GLib main loop. Within EFL, the main loop is implemented within Ecore. Ecore contains explicit support to integrate the GLib main loop in the EFL main loop. This can be achieved by calling the function ecore_main_loop_glib_integrate. According to the documentation of this function, this support is often used to use libraries such as GUPnP and LibSoup within EFL applications.
The Ecore library uses the select system call by default in its implementation of the main loop. This select function can be replaced with a custom select function when desired. This is how the GLib main loop is integrated. A custom select function is installed, which calls the relevant phases of the GLib main loop (prepare, query, check and dispatch as described above) and performs the polling phase by calling select for the file descriptors monitored by Ecore as well as the file descriptors that need to be monitored for the event sources installed in the GLib main loop. So, in this case, the GLib main loop is a secondary main loop and is integrated with the Ecore main loop.
A more complicated example of main loop integration can be found in GTK+ itself. In the OS X backend the GLib main loop and the Cocoa main loop are integrated with one another. The main problem that needed to be solved here is that one cannot determine whether new Cocoa events have arrived by checking for events on a file descriptor. As a consequence, we cannot write an event source that can be used together with the GLib main loop which relies on using poll. To get around this, the code uses a feature of the GLib main loop API which allows one to install a custom poll function. In this custom poll function, we can determine whether new Cocoa events are pending using an Objective-C call ([NSApp nextEventMatchingMask]).
This call does not solve all of our problems. It is not possible to wait for both file descriptors and Cocoa events at the same time. When it is necessary to wait for activity on either of these, another trick is used. A helper thread is used to perform the call to poll on a given set of file descriptors and the main thread calls the Objective-C call to wait for new events. Using these two tricks, the Cocoa main loop has been integrated into the GLib main loop and the GLib main loop is the primary main loop processing events.
It may seem we have covered everything now, but there’s even more. Remember that a GLib main loop may recurse within the dispatch phase to implement modal operations? The Cocoa main loop does this as well. In the dispatch phase, certain events are sent to Cocoa to be handled. An example are window resizing events. So, within the dispatch phase, the Cocoa main loop may be run. While the Cocoa main loop is processing, the GLib main loop is essentially blocked. How does one get around this? The solution that has been implemented is to iterate the GLib main loop while the Cocoa main loop is running. In fact, this is an integration of the GLib main loop within the Cocoa main loop. Within the OS X backend, main loop integrations in both directions takes place!
Integration of the GLib main loop within the Cocoa main loop is implemented how one would expect: call the different GLib main loop phases at the right moment, as can be seen in the figure. This is done by “observing” the Cocoa run loop. Through an observer callback, the application process is informed in what state the Cocoa run loop currently is. Based on this state, the corresponding GLib main loop phase is called. Note that the Cocoa main loop is also run from the modified poll func, while waiting for a new Cocoa event to arrive. In this case, the GLib main loop is not integrated within the Cocoa main loop, to avoid recursing the GLib main loop from within its poll phase.
The integration of the GLib and Cocoa main loops is one of the hardest to understand parts of the GTK+ OS X code base. This integration code was implemented by Owen Taylor. The implementation is thoroughly commented, see GNOME Bug 550942 and the relevant source file in the GTK+ git repository: gtk+/gdk/quartz/gdkeventloop-quartz.c. Together with the general description of the GLib main loop provided in this article, it should be possible to understand most of the code involved.
In this article, we have explored the engine of GUI libraries: the main loop. We have seen how the GLib main loop is implemented and how the GLib main loop can be used with different toolkits. At Lanedo, we have a lot of experience with different GUI toolkits on different software platforms. If this is something your company needs help with, do not hesitate contact us!