[Openmcl-devel] Trouble defining ObjC classes with struct slots
Gary Byers
gb at clozure.com
Fri May 25 18:26:34 PDT 2007
On Fri, 25 May 2007, Daniel Dickison wrote:
>>>
>>> 2. If I define the slot to be a CLOS slot (i.e. no :foreign-type
>>> option), then I can store NSPoints in the slot and get back NSPoints
>>> as expected, BUT, any initialization I do during the ObjC -init
>>> method gets overridden by the :initform. It seems that CLOS slots
>>> get re-initialized after the ObjC -init method runs, so the ObjC
>>> initializations get wiped out.
>>
>> I think that that's a bug, but there are some chicken-and-egg issues
>> here. (In order to be able to refer to the object's lisp slots from an
>> ObjC init method, it's necessary to run INITIALIZE-INSTANCE - with no
>> initargs - so that the slots' initforms are executed. The ObjC init
>> method is often called (as part of ALLOCATE-INSTANCE) by
>> MAKE-INSTANCE, which then turns around and does INITIALIZE-INSTANCE
>> with initargs. That shouldn't cause any slots to be "initialized
>> twice", but it's certainly unintuitive that INITIZLIZE-INSTANCE
>> should run twice per instance (and it probably offers ways to lose.))
>>
>> The bug is that ALLOCATE-INSTANCE creates a "slots vector" for
>> the instance even if one was created by virtue of the fact that
>> a lisp slot was referenced in an ObjC #/init method.)
>>
>
> I admit this is CLOS territory I haven't seen before, so bear with me... I'm
> still trying get my head wrapped around how this all works. So here's my
> understanding:
> - INITIALIZE-INSTANCE is called with no arguments if an object's
> slots are accessed for the first time.
> - ALLOCATE-INSTANCE calls #/init or it's equivalent by using
> non-initarg keyword arguments.
> - If the #/init method accesses a Lisp slot, then INITIALIZE-INSTANCE
> is called with no arguments.
> - MAKE-INSTANCE calls INITIALIZE-INSTANCE with all the args after
> ALLOCATE-INSTANCE (and #/alloc #/init, etc).
> - If you call (#/init (#/alloc foo)) "by hand" (or by any external
> non-Lisp code), then INITIALIZE-INSTANCE is only called when slots are first
> accessed, which could be much later on.
>
> So (if I'm on the right track), if you have Lisp slots in an ObjC class, and
> you need to initialize them in some way, you can either do it in the #/init
> method or the INITIALIZE-INSTANCE :after method. If you do it in the #/init
> method, then you shouldn't use MAKE-INSTANCE to create instances because the
> slot initializations you did in #/init will be obliterated by the default
> primary INITIALIZE-INSTANCE. On the other hand, if you do the slot
> initialization in an INITIALIZE-INSTANCE :after method, then you should
> always use MAKE-INSTANCE to create instances -- but this doesn't work if you
> want foreign code to be able to create instances and you need to use
> arguments in the initializer.
>
> It seems unintuitive that there are 2 distinct and incompatible ways to
> initialize ObjC objects, and it feels like it could be made more consistent.
> For example, instead of calling #/init or the like during ALLOCATE-INSTANCE,
> it could just call #/alloc and INITIALIZE-INSTANCE could call the #/init
> method -- I presume this doesn't work because #/init can return a different
> object than self and ALLOCATE-INSTANCE needs the final object... But now I'm
> confused how doing (#/init (#/alloc foo)) manually can work properly if
> ALLOCATE-INSTANCE has to do important work on the instance to make it a
> proper CLOS object.
>
> Sorry about the long email. My immediate problems are solved so now I'm just
> writing out of curiosity.
>
> Daniel
>
To try to be clear: the bug is one that I introduced a month or so
ago.
To back up (and maybe ramble a bit):
An ObjC doesn't necessarily return its argument. One common idiom
involves classes that only have one global instance (NSApplication,
NSDocumentController); other cases involve "abstract" things like
NSString, where the particuler concrete subclass you get depends on
how the object is initialized. (This is true of NSArray, NSString,
and a lot of other fairly common things.) So, it's the combination
of #/alloc and #/init[WithWhatever] that do something close to
what ALLOCATE-INSTANCE wants (allocate an instance of something
and initialize its foreign slots and state).
That division of labor works fine (the #/alloc and #/init methods
handle allocation and foreign initialization, INITIALIZE-INSTANCE
handles initializing the lisp side of things) for things that (a)
have lisp slots and (b) are only created from lisp.
Lisp slots are (not surprisingly) allocated differently than foreign
slots are. Obviously, we only really need to create lisp slots for
an instance if the class has lisp slots ... So, conceptually,
we have ALLOCATE-INSTANCE on an ObjC class doing:
(let* ((instance (#/init... (#/alloc class))))
(create-lisp-slots-and-associate-them-with instance)
instance)
MAKE-OBJC-INSTANCE existed before the bridge tried to integrate
the ObjC object system into CLOS. It was (and is) just a syntactically
convenient way of invoking #/alloc and an #/init method; it didn't
do anything about allocating or initializing lisp slots. There
should be no difference between:
(make-objc-instance (find-class 'ns:ns-number) :with-double pi)
and
(make-instance (find-class 'ns:ns-number) :with-double pi)
so it's harder to explain why MAKE-OBJC-INSTANCE still need(ed)
to exist. The practical answer was that the other path - through
ALLOCATE-INSTANCE - was always creating a little vector of lisp
slots and associating them with the instance, even if the instance
didn't have any slots (there'd be a few bytes of stuff consed up
in that case.) I finally got around to changing ALLOCATE-INSTANCE
to do:
(let* ((instance (#/init... (#/alloc class))))
(if (class-has-some-lisp-slots class)
(create-lisp-slots-and-associate-them-with instance))
instance)
to avoid that, and changed all of the example code that was using
MAKE-OBJC-INSTANCE to just use MAKE-INSTANCE instead.
Once we've done this (allocation and foreign initialization),
INITIALIZE-INSTANCE can initialize the instance's lisp slots.
Any initargs that weren't used to select an #/init method variant
get treated as :initarg slot options, and initforms run for
slots that don't have :initargs supplied/defaulted.
People have found that it's sometimes necessary to initialize lisp
slots in #/init methods that need to be called from ObjC code; your
example method did that in order to create a typed rectangle, but
there are probably other cases where it's necessary to do this
sort of thing. About all that you can really hope to do in
the #/init method is to do a few (SETF (SLOT-VALUE ...) ..)s,
and expect that the lisp slots that can be initialized via
:INITFORMs to be initialized that way. This is done lazily;
if we try to touch the lisp slots of an instance that should
have them but doesn't yet have them, the slots are created
and initialized by calling (INITIALIZE-INSTANCE instance) with
no initargs.
That's what happened in your typed-rectangle example, but since
ALLOCATE-INSTANCE was doing:
(if (class-has-some-lisp-slots class)
(create-lisp-slots-and-associate-them-with instance))
it basically clobbered the slot(s) that had been initialized
in the #/init method, created a fresh set of (unbound) slots,
and then INITIALIZE-INSTANCE comes along and (re-)initializes
the unbound rectange (that had already been initialized in
the #/init method.
To handle this, ALLOCATE-INSTANCE needs to do something like:
(if (class-has-some-lisp-slots class)
(unless (instance-already-has-slots instance)
(create-lisp-slots-and-associate-them-with instance)))
and the bug that I was referring to is that it didn't handle
this properly. (I haven't checked that change into the trunk
yet.)
Some of this confusion can be avoided if the division of labor (#/init
methods only iniitalize the foreign side of things) can be maintained.
It's often possible to maintain that division, but it may not always
be possible.
More information about the Openmcl-devel
mailing list