A Cocoa Bridge for OpenMCL

Randall D. Beer
beer@eecs.cwru.edu
http://vorlon.cwru.edu/~beer


INTRODUCTION

The purpose of CocoaBridge is to make Cocoa as easy as possible to use
from OpenMCL, in order to support GUI application and development
environment activities.  It builds on the capabilities provided in the
APPLE-OBJC example.  The eventual goal is complete integration of
Cocoa into CLOS.  The current release provides Lisp-like syntax and
naming conventions for ObjC object creation and message sending, with
automatic type processing and compile-time checking of message
sends. It also provides some convenience facilities for working with
Cocoa.

A small sample Cocoa program can be invoked by evaluating (REQUIRE
'TINY) and then (CCL::TINY-SETUP). This program provides a simple example
of using several of the bridge's capabilities


BASICS

The main things you need to know are:

1) You create and initialize ObjC objects using
MAKE-OBJC-INSTANCE. This should be replaced by MAKE-INSTANCE as CLOS
integration improves

Example: 
[[NSNumber alloc] initWithFloat: 2.7] in ObjC becomes
(MAKE-OBJC-INSTANCE 'NS-NUMBER :INIT-WITH-FLOAT 2.7) in Lisp

Note that class names and init keywords are translated from ObjC to Lisp in
pretty much the obvious way

2) You send messages to ObjC objects using SEND

Examples:
[w alphaValue] becomes (SEND W 'ALPHA-VALUE)
[w setAlphaValue: 0.5] becomes (SEND W :SET-ALPHA-VALUE 0.5)
[v mouse: p inRect: r] becomes (SEND V :MOUSE P :IN-RECT R)

Note that message keywords are translated to Lisp in pretty much the obvious
way.  From within a method, you can also use SEND-SUPER.


3) The @CLASS macro from APPLE-OBJC is currently used to refer to named ObjC
classes, which can also be sent messages via SEND. This should be replaced by
FIND-CLASS as CLOS integration improves.

Example: 
[NSColor whiteColor] becomes (SEND (@CLASS NS-COLOR) 'WHITE-COLOR)


4) New ObjC classes and methods are currently defined using DEF-OBJC-CLASS and
DEFINE-OBJC-METHOD from APPLE-OBJC.  This should be replaced by DEFCLASS and
DEFMETHOD as CLOS integration improves.


NAME TRANSLATION

There are a standard set of naming conventions for Cocoa classes,
 messages, etc.  As long as these are followed, the bridge is fairly
 good at automaticallly translating between ObjC and Lisp names.

Examples:
"NSURLHandleClient" <==> NS-URL-HANDLE-CLIENT
"NSOpenGLView" <==> NS-OPENGL-VIEW
"nextEventMatchingMask:untilDate:inMode:dequeue:" <==>
(:NEXT-EVENT-MATCHING-MASK :UNTIL-DATE :IN-MODE :DEQUEUE)

To see how a given ObjC or Lisp name will be translated by the bridge, you can
use the following functions:

OBJC-TO-LISP-CLASSNAME string
LISP-TO-OBJC-CLASSNAME symbol
OBJC-TO-LISP-MESSAGE string
LISP-TO-OBJC-MESSAGE keyword-list
OBJC-TO-LISP-INIT string
LISP-TO-OBJC-INIT keyword-list

Of course, there will always be exceptions to any naming convention.
Please let me know if you come across any name translation problems
that seem to be bugs.  Otherwise, the bridge provides two ways of
dealing with exceptions:

1) You can pass a string as the class name of MAKE-OBJC-INSTANCE and
as the message to SEND.  These strings will be directly interpreted as
ObjC names, with no translation. This is useful for a one-time
exception.

Examples:
(MAKE-OBJC-INSTANCE "WiErDclass")
(SEND o "WiErDmEsSaGe:WithARG:" x y)

2) You can define a special translation rule for your exception. This is useful
for an exceptional name that you need to use throughout your code.

Examples:
(DEFINE-CLASSNAME-TRANSLATION "WiErDclass" WEIRD-CLASS)
(DEFINE-MESSAGE-TRANSLATION "WiErDmEsSaGe:WithARG:" (:WEIRD-MESSAGE :WITH-ARG))
(DEFINE-INIT-TRANSLATION "WiErDiNiT:WITHOPTION:" (:WEIRD-INIT :OPTION)

The normal rule in ObjC names is that each word begins with a capital letter
(except possibly the first).  Using this rule literally, "NSWindow" would be
translated as N-S-WINDOW, which seems wrong.  "NS" is a special word in ObjC
that should not be broken at each capital letter. Likewise "URL", "PDF",
"OpenGL", etc. Most common special words used in Cocoa are already defined in
the bridge, but you can define new ones as follows: (DEFINE-SPECIAL-OBJC-WORD
"QuickDraw")

Note that message keywords in a SEND such as (SEND V :MOUSE P :IN-RECT R) may
look like Lisp keyword args, but they really aren't. All keywords must be
present and the order is significant. Neither (:IN-RECT :MOUSE) nor (:MOUSE)
translate to "mouse:inRect:"

Note that an "init" prefix is optional in the initializer keywords, so
(MAKE-OBJC-INSTANCE 'NS-NUMBER :INIT-WITH-FLOAT 2.7) can also be expressed as
(MAKE-OBJC-INSTANCE 'NS-NUMBER :WITH-FLOAT 2.7)


STRETS

Some Cocoa methods return small structures (such as those used to represent
points, rects, sizes and ranges). Although this is normally hidden by the ObjC
compiler, such messages are sent in a special way, with the storage for the
STructure RETurn (STRET) passed as an extra argument. This STRET and special
SEND must normally be made explicit in Lisp.  Thus 

NSRect r = [v1 bounds];
[v2 setBounds r];

in ObjC becomes

(RLET ((R :<NSR>ect))
  (SEND/STRET R V1 'BOUNDS)
  (SEND V2 :SET-BOUNDS R))
  
In order to make STRETs easier to use, the bridge provides two conveniences:

1) The SLET and SLET* macros may be used to define local variables that are
initialized to STRETs using a normal SEND syntax. Thus, the following is 
equivalent to the above RLET:

(SLET ((R (SEND V 'BOUNDS)))
 (SEND V2 :SET-BOUNDS R))
 
2) The arguments to a SEND are evaluated inside an implicit SLET, so instead of
the above, one could in fact just write:

(SEND V1 :SET-BOUNDS (SEND V2 'BOUNDS))

There are also several psuedo-functions provided for convenience by the ObjC
compiler. The following are currently supported by the bridge: NS-MAKE-POINT,
NS-MAKE-RANGE, NS-MAKE-RECT, and NS-MAKE-SIZE. These can be used within a SLET
initform or within a message send:

(SLET ((P (NS-MAKE-POINT 100.0 200.0)))
  (SEND W :SET-FRAME-ORIGIN P))
  
or
  
(SEND W :SET-ORIGIN (NS-MAKE-POINT 100.0 200.0))

However, since these aren't real functions, a call like the following won't
work:

(SETQ P (NS-MAKE-POINT 100.0 200.0))

The following convenience macros are also provided: NS-MAX-RANGE, NS-MIN-X,
NS-MIN-Y, NS-MAX-X, NS-MAX-Y, NS-MID-X, NS-MID-Y, NS-HEIGHT, and NS-WIDTH.

Note that there is also a SEND-SUPER/STRET for use within methods.


OPTIMIZATION

The bridge works fairly hard to optimize message sends under two conditions. In
both of these cases, a message send should be nearly as efficient as in ObjC:

1) When both the message and the receiver's class are known at compile-time. In
general, the only way the receiver's class is known is if you declare it, which
you can do either via a DECLARE or THE form.  For example:

(SEND (THE NS-WINDOW W) 'CENTER)

Note that there is no way in ObjC to name the class of a class.  Thus
the bridge provides a @METACLASS declaration. The type of an instance
of "NSColor" is NS-COLOR.  The type of the *class* "NSColor" is
(@METACLASS NS-COLOR):

(LET ((C (@CLASS NS-COLOR)))
  (DECLARE ((@METACLASS NS-COLOR) C))
  (SEND C 'WHITE-COLOR))
  
2) When only the message is known at compile-time, but its type
signature is unique. Of the over 6000 messages currently provided by
Cocoa, only about 50 of them have nonunique type signatures.  An
example of a message whose type signature is not unique is SET.  It
returns VOID for NSColor, but ID for NSSet.  In order to optimize
sends of messages with nonunique type signatures, the class of the
receiver must be declared at compile-time.

If the type signature is nonunique or the message is unknown at compile-time,
then a slower runtime call must be used.

The ability of the bridge to optimize most constant message sends even
when the receiver's class is unknown crucially depends on a type
signature table that the bridge maintains.  When the bridge is first
loaded, it initializes this table by scanning all methods of all ObjC
classes defined in the environment.  If new methods are later defined,
this table must be updated. After a major change (such as loading a
new framework with many classes), you should evaluate
(UPDATE-TYPE-SIGNATURES) to rebuild the type signature table.

Because SEND, SEND-SUPER, SEND/STRET and SEND-SUPER/STRET are macros,
they cannot be FUNCALLed, APPLYed or passed as functional arguments.
The functions %SEND and %SEND/STRET are provided for this
purpose. There are also %SEND-SUPER and %SEND-SUPER/STRET functions
for use within methods. However, these functions should be used only
when necessary since they perform general (nonoptimized) message
sends.


VARIABLE ARITY MESSAGES

There are a few messages in Cocoa that take variable numbers of arguments.  
Perhaps the most common examples involve formatted strings:

[NSClass stringWithFormat: "%f %f" x y]

In the bridge, this would be written as follows:

(SEND (@CLASS NS-STRING) 
      :STRING-WITH-FORMAT #@"%f %f" 
      (:DOUBLE-FLOAT X :DOUBLE-FLOAT Y))

Note that the types of the variable arguments must be given, since the compiler
has no way of knowing these types in general.

Variable arity messages can also be sent with the %SEND function:

(%SEND (@CLASS NS-STRING) 
       :STRING-WITH-FORMAT #@"%f %f" 
       (LIST :DOUBLE-FLOAT X :DOUBLE-FLOAT Y))

Because the ObjC runtime system does not provide any information on
which messages are variable arity, they must be explicitly defined.
The standard variable arity messages in Cocoa are predefined.  If you
need to define a new variable arity message, use
(DEFINE-VARIABLE-ARITY-MESSAGE "myVariableArityMessage:")


TYPE COERCION

OpenMCL's FFI handles many common conversions between Lisp and foreign data,
such as unboxing floating-point args and boxing floating-point results.  The
bridge adds a few more automatic conversions:

1) NIL is equivalent to (%NULL-PTR) for any message argument that requires a
pointer

2) T/NIL are equivalent to #$YES/#$NO for any boolean argument

3) A #$YES/#$NO returned by any method that returns BOOL will be automatically
converted to T/NIL

To make this last conversion work, the bridge has to engage in a bit
of hackery.  The bridge uses ObjC run-time type info.  Unfortunately,
BOOL is typed as CHAR by ObjC.  Thus, a method that returns CHAR might
actually return only BOOL, or it might return any CHAR.  The bridge
currently assumes that any method that returns CHAR actually returns
BOOL.  But it provides a facility for defining exceptions to this
assumption: (DEFINE-RETURNS-BOOLEAN-EXCEPTION "charValue").
Eventually, the best way to handle issues like this is probably to get
our method type info directly from the header files rather than using
ObjC's runtime type system.

Note that no automatic conversion is currently performed between Lisp
strings and NSStrings.  However, APPLE-OBJ provides a convenient
syntax for creating constant NSStrings: (SEND W :SET-TITLE #@"My
Window"), as well as facilities for converting between Lisp strings
and NSStrings.  Note that #@"Hello" is a full ObjC object, so messages
can be sent to it: (SEND #@"Hello" 'LENGTH)

