I lay out a model for implementing a two-threaded Glk library -- one thread for the user interface, one for the virtual machine (Glulx). I used this when implementing Glk for iOS.
This document does not cover the notion of running two Glk applications in the same process. The Glk API does not currently allow this, and yes, it's a nuisance. I will deal with it someday. Not today.
This document also does not cover the notion of a threaded IF virtual machine which can perform work while the user is typing input. This is possible, but the Glk API does not manage it. The VM would have to manage its own threading model. (The Glk API must always be called from the thread that glk_main() was invoked in.)
The early Glk implementations were single-threaded. That is, the game interpreter and the Glk library all ran in the same thread. The game would call glk_select(), which would block and wait for input via some native API. (CheapGlk calls fgets() to accept input; GlkTerm calls the curses getch() function.)
This is a simple model, and it works tolerably well if the game's command processing is fast. However, a slow command will make the player's interface freeze for a moment, and if the game gets stuck in an infinite loop the interface does too. This is not ideal.
Furthermore, modern GUI systems don't want their event loops to be called from inside game code. (MacOS Classic could be made to work this way, but MacOS X is not suited to it, for example.)
The best plan for these systems is for the Glk library to operate two threads. The UI thread talks to the native UI and handles user interactions; the VM thread runs the game code.
(The familiar example of a Glk game is Glulx code executing in a Glulx interpreter. For the purposes of this document, I am lumping them together; I will refer to "the game code" and "the VM" interchangeably. Of course the Glk library doesn't care whether it is linked to an interpreter, a non-VM-model game, or any other program.)
Threading is always a minefield; we must be careful to keep a clean separation between the threads. This document describes the model that I used in the iOSGlk library. (It took me a few tries to work this out, so I figure I should document it for posterity.)
The plan is to have two separate sets of data describing the interface (the Glk windows and input state). One set is managed by the UI thread, the other by the VM thread. We synchronize these at specific times: when entering and leaving glk_select(), for example.
Again, the application will spend nearly all of its time waiting for input (inside glk_select). The VM thread normally computes only for short intervals after an input event.
The sync procedure (entering glk_select) goes roughly like this:
It is important to have unique window identifiers for the sync process. (If the VM side closes one window and immediately opens another, the UI side will have to recognize that this is a different window.) A simple incrementing counter suffices. The game code will never see these identifiers.
It is also important to have unique identifiers for line input events. (A window may keep a line input event active across several glk_select() cycles, or it may cancel and restart line input within a single cycle. The UI side must be able to distinguish these cases. So it can't just look at a boolean flag "is line input active?" at sync time.)
A VM-side text buffer window only needs to store the text printed in the current cycle. It can be emptied out at sync time, because the text gets appended to the UI-side buffer window and then lives there forever. (Or until the UI-side window is cleared, or trims its history.)
A VM-side text grid window should keep the entire current state of the grid, as printed by the VM. (This is not strictly necessary, but it's easiest to sync the entire grid every cycle.)
If the UI application window is resized, this percolates into the VM as an "arrange" event. (The VM might respond to this by redrawing the status line.) Usually this happens when the app is in input mode (VM is blocked on glk_select). This is the easy case; you hand off the arrange event to glk_select, just like any other user input, and glk_select() ends.
However, you should consider that a window resize might occur while the VM is running. (This is unlikely, because the VM only runs in short bursts, but it is possible!) In this case you will have to stash the arrange event and deliver it the next time glk_select() occurs. It is sufficient to use a boolean flag here; if several window resizes occur in a row, you only need to deliver one arrange event.
The glk_fileref_create_by_prompt() call blocks and syncs, just like glk_select(). The UI should display a modal file dialog and wait for the user to select a file. Other UI events (line input, char input, arrange) can not be accepted.
The experimental gidebug_pause() call also blocks and syncs. The UI should display a debug console and accept debug commands.
Life gets more complicated in the mobile world. A mobile application has to be able to launch back to a previous state, seamlessly. Glk was not designed for this; it requires an extended Glk library that can serialize its entire state, including window contents. That's a topic for another time, however.
The difficulty for threading is that when the game ends, the mobile convention is to display a "Restart" button. (It is never appropriate for a mobile application to shut itself down.) Therefore, the VM thread must be prepared to restart glk_main() after glk_exit(). Glulxe has been updated to permit this, but other interpreters might not.
Glk home page
Zarfhome (map) (down)