'(Carbon Programming in Lisp)

I've been working on a CLOS framework to help me write Lisp code for OS X using OpenMCL. Instead of using Cocoa as most people, I've favored the Carbon APIs. I already understand C, so it seems like a gentler learning curve for me than to try and learn Objective-C and Cocoa while I am also learning how to take advantage of Lisp. I don't think there are any compelling technical reasons to favor one framework over the other. It's just a matter of personal preference.

Carbon's strength, that it is a C API, is also its weakness. A C program using Carbon will have one or more rather lengthy switch blocks. The Carbon API mitigates this somewhat by using callbacks for event handling. A Carbon event handler will only see the event types you express interest in when you install it. But this is not a complete solution.

Fortunately, Lisp offers better ways. It's just a question of finding them. Also it is necessary to have a C callable function for Carbon events to work. OpenMCL provides DEFCALLBACK for the latter. The former can be handled with CLOS. What I ended up doing was using a single callback funtion to call CLOS methods. The current state (as of late April) is what I'll talk about here with some code examples.

Whoops! I've made some changes since I started this page. My code has evolved. Instead of using a simple callback, I've created a function that makes a callback closure. The callback closes over a variable containing the CLOS object so that the event handling code is greatly simplified.

Some Code

The first thing I needed was a class to define objects that receive Carbon events:

(defclass event-target ()
  ((event-handler-callback :initform (ccl::%null-ptr))
   (event-handler-ref :initform (ccl::%null-ptr)))
  (:documentation "An object that receives Carbon events"))

To go along with this class I have some generic functions and associated methods to do some basic house keeping such as installing the event handler itself and uninstalling it later. What I want to talk about are the generic functions that actually handle events and the callback itself:

(defgeneric handle-event (target class kind next-handler event user-data)
  (:documentation
   "Handle Carbon events.  Return T when the event is handled or NIL otherwise"))
(defgeneric menu-command (event-target command))

(defun make-event-target-callback (et)
  (let (fn-carbon-event-handler)
    (declare (special fn-carbon-event-handler))
    (ccl:defcallback fn-carbon-event-handler
        (:<e>vent<h>andler<c>all<r>ef next-handler :<e>vent<r>ef event (:* t) user-data :<oss>tatus)
      (let ((class (#_GetEventClass event))
            (kind  (#_GetEventKind  event)))
        (declare (dynamic-extent class kind))
        (debug-log "Callback CARBON-EVENT-HANDLER: event-handler-ref = ~S; Class: '~A' Kind: ~A~%"
                   (slot-value et 'event-handler-ref) (int32-to-string class) kind)
        (multiple-value-bind (r c)
            (ignore-errors
              (handle-event et class kind next-handler event user-data))
          (declare (dynamic-extent r c))
          (when c
            (debug-log "Condition signaled from CARBON-EVENT-HANDLER: < ~A >~%" c))
          (if r #$noErr #$eventNotHandledErr))))
    fn-carbon-event-handler))

Ignoring the punting on errors, CARBON-EVENT-HANDLER cracks open the event and uses the closed over variable et to reference the object that is supposed to receive the event. The real magic is done by CLOS dispatching the call to HANDLE-EVENT. The secret here is CLOS's EQL specialization. EV-CLASS and EV-KIND are of type (UNSIGNED-BYTE 32). The following method illustrates EQL specialization and further handles dispatching of menu commands in precisely the same way:

(defmethod handle-event ((et event-target)
                         (class (eql #$kEventClassCommand))
                         (kind  (eql #$kEventCommandProcess))
                         next-handler event user-data)
  (declare (ignore next-handler user-data))
  (rlet ((command :<hic>ommand))
    (#_GetEventParameter event #$kEventParamDirectObject #$typeHICommand
                         (ccl::%null-ptr) (ccl::record-length :<HIC>ommand)
                         (ccl::%null-ptr) command)
    (menu-command et (ccl::pref command :<hic>ommand.command<id>))))

Magic!

All I have to do to make my objects respond to various events is write the appropriate methods to specialize HANDLE-EVENT or MENU-COMMAND. Here are a couple of examples from code that I am working on:

(defmethod carbon:menu-command ((app opengl-application)
                                (command (eql #$kHICommandNew)))
  (carbon:show-window (make-opengl-demo-window app "Window" "Window"
                                               (make-instance 'opengl-pyramid)))
  t)

(defmethod carbon:handle-event ((w opengl-demo-window)
                                (class (eql #$kEventClassWindow))
                                (kind  (eql #$kEventWindowDrawContent))
                                next-handler event user-data)
  (declare (ignore next-handler event user-data))
  (carbon:debug-log "OpenGL Demo Window EQL method called: ~A ~A~%" class kind)
  (with-slots (agl-context carbon:window-ptr width height) w
    (destructuring-bind (w h) (window-size w)
      (setf width w height h)
      (cli:viewport agl-context w h))
    (cli:draw agl-context))
  t)

As you can see, all I need to do to respond to an event is specify the class and kind of the event in the lambda list for the method call. Menu commands are even simpler because of the dispatching done by the first event handler. What you see is pretty much all there is to event dispatching in the CLOS framework that I am developing.

I've released a toy example of a running application that shows my Carbon code in action. It's based on an older version of the Carbon code. The application was compiled under OS X 10.3.8. It runs on 10.3.9 and also Tiger 10.4.0. I have not heard if it also runs under 10.4.1. Also demonstrated is some OpenGL code. The application and code are in an OS X DMG file so you need OS X to unpack the archive and run the application:

Carbon-OpenGL.dmg

Carbon-OpenGL
        Screen shot

Please keep in mind that this is experimental code and not production quality.

Common-lisp.net Project

Thanks to the kind generosity of the folks running Common-lisp.net, I have founded the CL-Carbon project to develop a CLOS framework for using the Carbon APIs on OS X. There you can check out the latest CVS sources of CL-Carbon. If you are interested, you can also join the mailing list. This is a brand new project, so there isn't a lot going on at the moment. Where it goes from here is anyone's guess.