Trying to understand Racket's GUI windowing model

I am trying to understand Racket's GUI windowing model. I have tried reading the documentation and digging through bits of the code to understand how both windowing and event spaces are handled, but it is rather complex. There also seems to be a lot history, since it seems Racket originaly used wxWidgets, then bindings to the invidiual native platform window APIs, and now uses GTK for windowing and Cairo for 2D graphics, if I understand correctly from this blog post: Rebuilding Racket’s Graphics Layer (racket-lang.org).

I am wanting to better understand how Racket is doing everything under the hood, if even at just a high level. Additionally, I am writing bindings to GLFW in another language, and I want to recreate how cleanly Racket handles windowing and events on those windows. What I want is a system that:

  • Allows the creation of a window from any actual thread
  • Allows any window to have separate event handlers (potentially zero, one, or multiple) from other windows. In other words, each window can be basically its own standalone application within one program.

(It's my understanding this is how Racket's windowing and event spaces work. For what it's worth, this is also basically how LabVIEW's window and event handling works.)

However, one thing I am struggling with is how to manage the limitation that windows must be created by the main thread in a program. For example, from SDL's documentation:

You should not expect to be able to create a window, render, or receive events on any thread other than the main one.

From GLFW's glfwCreateWindow docs:

This function must only be called from the main thread.

Thus, a lot of windowing libraries, like Windows Forms, and WPF, GTK, etc. have something like this in the main thread:

create/define windows
create/define application
application.run(windows)

A GTK example of this.

I can create windows in Racket like this:

#lang racket/gui

(define frame1 (new frame%
                    [label "Example"]
                    [width 200]
                    [height 200]))
(define frame2 (new frame%
                    [label "Example"]
                    [width 200]
                    [height 200]))
(send frame1 show #t)
(send frame2 show #t)

or like this:

#lang racket/gui

(thread (lambda () (begin (define frame1 (new frame%
                                              [label "Example"]
                                              [width 200]
                                              [height 200]))
                          (send frame1 show #t))))

(thread (lambda () (begin (define frame2 (new frame%
                                              [label "Example"]
                                              [width 200]
                                              [height 200]))
                          (send frame2 show #t))))

using Racket threads (which aren't actual threads in the usual sense, i.e., not OS threads). I don't know if this works using futures or places though.

It seems Racket must be doing some bundling of this code somewhere such that these frames (i.e., windows) are getting created and managed only on the main thread and that these frame creation and show are "messages" to that main thread bundle. Is that true? How does this work? I would have to assume it's something like this. LabVIEW can create and manage windows from any thread and assign event handler loops (however many you want for a single window), but I do know that there, all the GUI stuff is still happening on the single main thread.

  • Can someone explain to me or sketch out how Racket handles managing several windows in a single program?
  • Is there somewhere I can read how event spaces work outside of this documentation?

Thank you!

1 Like

When you read a statement such as

You should not expect to be able to create a window, render, or receive events on any thread other than the main one.

or

This function must only be called from the main thread.

What this means is as follows:

The data structures read and modified by the function or method are not protected using locks and multithreaded access is not safe. Additionally, there is already one thread which is guaranteed to read and modify these data structures. Therefore we recommend calling this function on that same thread.

If you do call the function from another thread, the result is unpredictable -- things may work most of the time and only fail occasionally. Also, when, how, and how often such failures occur depends on the implementation details of the library.

In your example, when you created two frames in separate threads in Racket, things worked but Racked is not doing anything special -- it is simply multithreaded access on unprotected data structures.

Writing simple test programs does not prove things one way or another when it comes to multithreading. Racket GUI objects should be accessed from the event handler thread of the respective GUI object if you want to avoid multithreading problems. If you run code on another thread and want to modify a GUI object, you can use queue-callback

I am not familiar with LabVIEW, perhaps their GUI objects are all protected using locks, so multithreaded access is safe in their GUI library.

Hope this helps,
Alex.

There's some Racket code that polls the "OS" GUI layer (by which I mean Win32, GTK, etc) event queue and forwards them to the appropriate Racket eventspace. Look in gui/gui-lib/mred/private/wx/{gtk,win32,cocoa}/queue.rkt for the code that does that for each platform. For example, in the gtk version, see the event-dispatch function; if there is an associated Racket window with the event, it extracts the window's eventspace and calls queue-event in that eventspace with an event-handling callback. Polling is done by checking for events and then telling the OS to "wakeup" the Racket process if new events come in while it's sleeping (the wakeup might be conditioned on file descriptors or a Win32 event mask, for example); search for "wakeup" in the code.

The paper "Programming Languages as Operating Systems (or Revenge of the Son of the Lisp Machine)" describes the design of (some of) the pieces that fit together to make Racket's GUI system work, including eventspaces. It doesn't talk about their implementation, though.

2 Likes

This is not quite right: racket/gui uses the native Windows and Mac GUI APIs on those platforms, and GTK only on Unix-like systems. (It's possible to do a Gtk/X11 build for Mac, but this is not the default.)

The changes in the blog post you mentioned were that:

  1. racket/gui is implemented entirely in Racket using the FFI, instead of being written in C/C++/Objective C.
  2. racket/draw provides a 2D graphics API that can be used without a GUI. The implementation of racket/draw does use Cairo and Pango on all platforms

If by "actual thread" you mean an OS or hardware thread, unfortunately this is not supported by the underlying toolkits.

If you take a look at 12 Startup Actions in the GUI docs, is says:

The racket/gui/base module can be instantiated only once per operating-system process, because it sets hooks in the Racket run-time system to coordinate between Racket thread scheduling and GUI events. Attempting to instantiate it a second time results in an exception. Furthermore, on Mac OS, the sole instantiation of racket/gui/base must be in the process’s original place.

The thread where racket/gui/base is instantiated also becomes the handler thread for the initial eventspace. See also Eventspaces and Threads.

Furthermore,

This particular program works correctly, but in general great care is needed when using GUI objects from multiple Racket threads. Turning to § 1.6.2 1.6.2 Eventspaces and Threads in the docs, it says:

When the system dispatches an event for an eventspace, it always does so in the eventspace’s handler thread. A handler procedure can create new threads that run indefinitely, but as long as the handler thread is running a handler procedure, no new events can be dispatched for the corresponding eventspace.

Windowing functions and methods from racket/gui/base can be called in any thread, but beware of creating race conditions among the threads or with the handler thread. Graphical objects are thread-safe, but callbacks or other event handlers might see changing object states if graphical elements are manipulated in multiple threads. Editor classes have more significant thread constraints; see Editors and Threads.

A useful function for dealing with these constraints is queue-callback, which arranges to run an arbitrary function in the handler thread.

Just to be clear, all Racket-level threads in Racket's "main place" are run in the OS-level "main thread". Also, as you may have gathered from all of that, "event handler loops" aren't really part of the API of racket/draw the way they are for some GUI libraries.

1 Like