Beside the bindings to Objective-C runtime API adding Cocoa flavor to Scheme 48 means dealing with GUI events. This has been the hardest part of this project. Basically, the Scheme 48 VM executes byte code and, if it has to wait for events (file, network, or terminal I/O) it calls select. GUI events are further source of events select needs to take into account. However, only few GUI libraries offer notifcations on events via a file descriptor. X11 does this, Qt doesn’t. I’m not sure about GTK. However, this is a problem. In the case of Qt this has been solved by re-implementing the select mechanismn in terms of Qt classes and their Notification mechanismn. Well, that’s a different story.
Well, for Cocoa this is also a problem. Cocoa GUI application usually fire up a NSRunLoop which takes over the control. In a first attempt Gregor tried to re-implement the run loop to enable running it only for a defined period of time. This is possible, however, doesn’t work very well in practice. The run loop is completly mysterious. It seems that this run loop does some strange initializations we were not able to follow. With Gregor’s run loop some things happened that didn’t happen when using the original run loop. For example, in certain situations there was more than one window holding the focus or some black hole swallowed events unpredictablely. This behavior and the apprehension that Apple might change the mysterious initialization any time made us give up this attempt.
So, here is the real solution, which is more complex. If the run loop wants to run, let it run, but in an extra operating system thread. At startup the Cocoa edition of Scheme 48 spawns an operating system thread. This thread runs the VM. The main thread executes the run loop (for some esoteric reason it’s a good idea to run the run loop in main thread), like a normal Cocoa would do. To react to GUI events a Cocoa programmer attaches callbacks to a control/widget (i.e., using the setTarget or setAction methods). Thing become a bit complicated if the callback is written in Scheme. Also it’s possible to install a Scheme function as a callback, it’s dangerous: The callback may occur at any time and that is not the way the Scheme 48 VM excepts the world to be. The VM decides when to run code and when to wait. The FFI knows to handle calls from C to Scheme, however, that’s a different situation: Some piece of Scheme code calls some piece of C code and that code calls some Scheme function. This works fine, however, just calling Scheme code from C at an arbitrary point in time is not advisable.
So, the solution is to enqueue the callback events and notify the VM somehow instead of directly executing the callbacks. Scheme 48 Cocoa uses a proxy object (S48EventHandler) to callbacks. If the Scheme programmer registers for a callback, a method of S48EventHandler is set as the callback. This callback is executed in main thread which is also the thread running the run loop (it seems to be of some importance to Cocoa to handle events in thread they occur). The callback (of the proxy object) does two things. It enqueues a tuple consisting of an object id (which identifies the affected object) and a event type id into a global queue so the VM can later read from that queue. And the method also writes to a pipe that connects the main thread with Scheme 48 VM. Besides the file descriptors the VM also selects on this pipe. This way the VM notices GUI events when selecting. In this case the VM may check the queue (filled by the proxy object) and schedule a so-called GUI interrupt and then things become even more complicated. It seems that using a pipe for ending the select is the easiest method. After Gregor’s talk there was a discussion whether a using a signal would be a better solution. I think that this makes things even more complicated, because GUI events may also happen when select is not running, i.e. it’s not the time to execute the callback. It’s unclear to me what the signal handler should do in this case.
If the GUI interrupt handler catches the GUI interrupt things get a bit complicated, but that’s a different story. However, with the information from the queue it’s easy to find the callback to execute. A table maps object id, event type tuples to the Scheme function that serves as the callback. For the Scheme programmer this looks like this:
(define nsbutton
(objc-make-class "NSButton"))
(define my-button
(objc-alloc nsbutton))
(register-cocoa-callback
my-button
s48-action-notification
(lambda ()
(display "button pressed")
(newline)))
(start-action-notifications my-button)
The function register-cocoa-callback installs the callback for the NSButton represented by my-button. The second argument for register-cocoa-callback specifies to event type, in this case it’s s48-action-notification. There are several different types of events like low level events like mouse events, and “action” is one of the high level events. Such an event is generated if the user clicks a NSButton. Register-cocoa-callback stores the information about the callback (object id, type id and callback) in the global callback table and start-action-notifications actually connects the callbacks of my-button to the proxy object.
Hm, I probably misused some of the Objective-C and Cocoa slang in this story—sorry. That’s simply because I haven’t done much Cocoa programming yet. But I hope to use Cocoa more often in the future—in Scheme programs of course.
Technorati Tags: Scheme, Objectiv-C, Cocoa, Scheme 48