[Openmcl-devel] Using Cocoa with MCL

Gary Byers gb at clozure.com
Fri May 16 10:03:25 PDT 2003

On Thu, 15 May 2003, Randall Beer wrote:
> Yes, I have looked at OBJC-OBJECT-P and I've also been giving some
> thought to essentially making ObjC MACPTRs act like a CLOS class.  As
> you point out, there is much to recommend this idea.  I guess that my
> biggest hesitations with this approach are mostly the things you've
> mentioned: (1) I want to be able to redefine my *own* classes and
> methods freely; (2) I want to be able to store any Lisp object in my
> own classes; (3)  I don't want to have to keep manually converting back
> and forth between Lisp and ObjC data; (4) I don't want to program ObjC
> in Lisp. So, let me push the parallel approach just a bit more below,
> if you don't mind.
> No matter how Cocoa is represented in Lisp, it will certainly have some
> restrictions relative to CLOS.
> To be as clear as possible, let me spell out the parallel approach I
> had in mind, with some sample CLOS code for how it might work.
> Although this code was obviously written manually, the idea would be
> for much of it to be generated automatically.
> Suppose we define the following OBJC-OBJECT class to serve as the
> superclass of any CLOS object which is associated with a corresponding
> ObjC object:
> (defclass objc-object ()
>    ((objc-object :accessor objc-object)))
> In order to ensure that the ObjC memory allocation works, we need
> something like:
> (defmethod initialize-instance :after ((o objc-object))
>    (progn
>      [(objc-object w) "retain"]
>      (terminate-when-unreachable o)))
> (defmethod terminate ((o objc-object))
>    (prog
>      [(objc-object o) "release"]
>      (setf (objc-object w ) (%null-ptr))))

One thing to bear in mind is that a lot of ObjC objects (especially
things that're commonly used in the event loop) are autoreleased.
Cocoa's event loop is something like:

  (let ((pool (create-autorelase-pool))) ; make an NSAutoreleasePool and
                                         ; establish it as the "current"
                                         ; pool for the calling thread.
    (let ((event (next-event-matching-mask ...)))
      (handle-event event))
   [pool release]))              ; free the pool and send release to
                                 ; everything in it.

Most of the things allocated during the process of waiting for and
handling an event (including the NSEvent itself) are added to the
current autorelease pool (many of these objects have constructors
that do the autorelease automatically, as well as/instead of those
that create long-lived objects.)

I don't know how to tell if an NSObject has been autoreleased or how
to tell what objects have been registered in an autorelease pool.  The
retain/release-on-finalize trick seems like a good way to deal with
ObjC objects that're known or assumed to be long-lived (like an NSWindow.)
I'm not sure what the solution is, but it might have a severe and negative
effect on performance if all ObjC objects that lisp "saw" became long-lived
as a side-effect of lisp "seeing" them.

> Now, for each class built-in to Cocoa, we have to do something like:
> (defclass ns-window (objc-object) ())
> Below is a simple-minded INITIALIZE-INSTANCE. In order to do this
> properly, we probably need some code that requires *sets* of  initargs
> that match Cocoa initializers (e.g., NS-WINDOW would require
> (:CONTENT-RECT :STYLE-MASK :BACKING :DEFER)  as initargs). It would
> certainly be possible and very convenient to have reasonable defaults
> for these, but I don't see how that can be done automatically.
> (defmethod initialize-instance :before ((w ns-window) &rest initargs)
>    (declare (ignore initargs))
>    (setf (objc-object w) [[(@class "NSWindow") "alloc"] "init"]))

One of the other things that we might do here is to establish a mapping
between the ObjC object and the CLOS object that encapulates it, enabling
us to find the latter from the former via GETHASH or ASSOC or something.

> Now, for each built-in Cocoa method for each built-in Cocoa class, we
> need to define a CLOS wrapper method:
> (defmethod make-key-and-order-front ((w ns-window))
>    [(objc-object w) "makeKeyAndOrderFront:" :address (%null-ptr)])
> (defmethod set-title ((w ns-window) (title string))
>    (progn
>      [(objc-object w) "setTitle:" :address (%make-nsstring title)]
>      title))
> The above code should be sufficient for the following to work:
> (setq w (make-instance 'ns-window))
> (make-key-and-order-front w)
> (set-title w "My Window")

That certainly seems reasonable.

> Note that I can also easily define my own new methods:
> (defmethod window-fu ((w ns-window)) ...)
> A key point here is that this is a *Lisp* extension and OBJC DOESN'T
> NEED TO KNOW ANYTHING ABOUT IT because it can only be invoked from Lisp.
> Likewise, we can easily define a subclass of NS-WINDOW whose slots can
> contain any Lisp objects we want.
> (defclass my-window (ns-window)
>    (slot1 slot2))
> Note that we need to enforce the restriction that any subclass of
> OBJC-OBJECT can only inherit OBJC-OBJECT along one path.  Thus,
> multiple inheritance is still allowed, just not involving multiple
> Note also that if I make an instance of MY-WINDOW, the actual OBJC
> object created will be an instance of "NSWindow", so once again, OBJC
> instance of MY-WINDOW gets passed down to ObjC, it will treat it as an
> instance of "NSWindow" because it is.
> Finally, suppose that I want to provide my own "drawRect:" method:
> (defmethod draw-rect :before ((w my-window) (r ns-rect)) ...)

This is a little confusing: drawRect: is typically an NSView method.
On the one hand, I wouldn't want to be prohibited from defining something
called DRAW-RECT on a CLOS NSWindow subclass.  OTOH, I might want to
be told that the ObjC NSWindow class doesn't typically respond to the
"drawRect:" selector.  On yet another hand, I might plausibly want
this to be shorthand: [my-window drawRect: r] might just be convenient
shorthand for [[mywindow contentView] drawRect: r], in which case
I -would- want the ObjC class to respond to the selector.

> I think that this is the trickiest case.  Whenever we define a method
> with a specializer matching a subclass of OBJC-CLASS and with the
> method selector matching a built-in Cocoa method, we need to do
> something like:
> 1) Define a primary method for that generic function that invokes the
> "original" ObjC method
> 2) Replace that ObjC method with one that invokes the Lisp generic
> function (once only)
> Thus, whenever ObjC asks this object to "drawRect:", our Lisp method
> will get invoked and then eventually the "default" ObjC method (which
> in this case doesn't do anything). A major issue here is how the
> "patched" ObjC method can invoke the corresponding generic function on
> the corresponding *Lisp* object.

drawRect: is actually an interesting case.  The most important caller
(the one that's most likely to actually be able to draw something if/when
native threads are involved) is something deep in the guts of the
"next-event-matching-mask" method in the event loop.

Suppose that we had something like:

(defclass ns-view (objc-object) ())  ; analogous to ns-window above
(defclass my-view (ns-view)
  ((interesting-and-complicated-display-structures ...)))

e.g., the data structures that describe how to draw our view are
only really accessible from the Lisp side of things.  I'd expect:

(defmethod draw-rect ((v my-view) rect)
    (slot-value v 'interesting-and-complicated-display-structures)

to have a side-effect something like:

(define-objc-method ("drawRect:" my-view) (r :<NSR>ect)
  (draw-rect (lookup-clos-object-for self) r))

If we want to muck around inside the guts of DEFCALLBACK (and hey,
who doesn't ?) we could almost imagine something like:

(defcallback clos-trampoline (:id self :id selector (&rest others)
  (let* ((gfn (extract-gfn-name-from selector))
         (clos-object (lookup-clos-object-for self)))
    (%foreign-coerce (apply gfn clos-object (insert-keywords-using-selector
                                      selector (lispify-foreign-args others)))

That might not be practical, but the ObjC method implementations would
be boilerplate in many cases.

> Whew!  Anyway, that's the idea.  It's probably full of holes, so please
> let me know what I've missed.

I think that the notion of parallel (standard-class) CLOS objects that
encapsulate underlying ObjC objects is often useful.  I think that it's
also fair to say that there are many cases where the encapsulation is
impractical and undesirable.  In the cases that we just looked at, it
seems to work well.  But ...

Consider things like:

(defmethod key-down ((v my-view) event) ...)

(defmethod mouse-moved ((v my-view) event) ...)

(defmethod send-event ((w my-window) event) ...)

It's fine with me if the view or the window is a standard-class CLOS
instance that's mapped to and retaining an ObjC object that I don't
really want to know about and rarely want to deal with.

It's less fine with me if I have to (or some macro has to ...) map
the NSEvent object to some corresponding CLOS object and retain
the low-level NSEvent (which is about to get released by the
autorelease mechanism) until the GC got around to finalizing the
encapsulating objc-object.  It's not too hard to imagine both the
ObjC heap and the lisp heap filling up with garbage, and this seems
like a heavy price to pay for wanting to intercept an event.

I think that many applications would want to intercept some events
directed toward their views, and it would be unfortunate if the
only choices involved creating a CLOS object for every event
or using the FFI to access the ObjC event object.  I think that that's
a lot of the reason that I've been advocating a "middle layer".  If that
middle layer is also CLOS-based (and the low-level ObjC objects are
CLOS objects), that would be attractive.  (If it's possible and practical.)

a)  If it's possible and practical, there's a thin CLOS layer around
    ObjC objects; if not, there's some sort of Lisp-y functional
    interface to them.

    This would probably be -all- that makes sense for certain types
    of "transient" ObjC objects (NSEvent, NSDate, NSString in most
    cases ...).

b)  For many kinds of "persistent" ObjC objects, greater functionality
    can be obtained by encapsulating the ObjC object in a higher-level
    standard-class CLOS object.  The low-level object is still exposed
    and accessible, but can (sometimes ? often ?) be ignored.

I don't know that we'd necessarily want to make it impossible to
create a "high level NSEvent class" so much as we'd want to discourage
people from doing so.

Suppose that the encapsulation/wrapping was exposed:

(defSOMETHING wrap-objc-class (objc-class clos-class) ...)

at some layer of the fictional AHMOP.  That might plausibly be done
for some classes (NSWindow, NSView) so that the typical user didn't
have to do so themselves.

A two-layer scheme ("not all Cocoa classes are created equal") is a
little harder to present and may be harder to understand at first,
but I don't think that all Cocoa classes are created equal ...

> Randy

Does this make sense ?

Openmcl-devel mailing list
Openmcl-devel at clozure.com

More information about the Openmcl-devel mailing list