Review Board

1.0

Prototypes in Objective-C

Updated 1 year, 8 months ago

David Chisnall Reviewers
EtoileCore
None Etoile trunk (etoile/trunk/Etoile)
Implemented Lieberman prototypes using hidden class transforms, removing the need for a modified runtime.  Hidden classes are reference counted, and are free'd when all of the objects that have them in their class hierarchy are dealloc'd.  

I propose removing ETPrototype (which no one is using, because it depends on a libobjc that only I have installed) in favour of this, which allows any arbitrary object to be used as a prototype.

The test program shows two methods being added to a simple NSObject subclass (NSObject could be used, but the subclass implements copyWithZone: allowing cloning to work).  These methods simply set and return a specific key on self, showing that both slots and methods can work.

I intend to also add a trampoline allowing blocks to be used as methods, so Smalltalk users can enjoy fun with prototypes too.
Running the following program gives this output:

$ ./obj/test 
2008-12-16 16:51:27.762 test[18935] Test value: A string
2008-12-16 16:51:27.778 test[18935] Extra ivars destroyed


Test program:



#import <Foundation/Foundation.h>
#import "ETPrototype.h"

// Two free-standing methods which will be added to an object for testing.
id setValueIMP(id self, SEL _cmd, id aValue)
{
	[self setValue:aValue forKey:@"TestKey"];
	return self;
}
id getValueIMP(id self, SEL _cmd)
{
	return [self valueForKey:@"TestKey"];
}
// Protocol added to remove warnings when invoking the methods, and to set type
// info for them.
@interface MyPrototype
- (id) testValue;
- (id) setTestValue:(id)aValue;
@end

@interface MyObject : NSObject {}
@end
@implementation MyObject
// Implement copying, to ensure make sure that the class is not freed
// prematurely
- (id) copyWithZone:(NSZone*)z
{
	return [isa allocWithZone:z];
}
@end

@interface MyObject2 : NSObject {}
@end
@implementation MyObject2
- (void) dealloc
{
	NSLog(@"Extra ivars destroyed");
	[super dealloc];
}
@end
int main(void)
{
	id pool = [NSAutoreleasePool new];
	id proto = [[MyObject new] autorelease];
	[proto setMethod:(IMP)setValueIMP forSelector:@selector(setTestValue:)];
	[proto setMethod:(IMP)getValueIMP forSelector:@selector(testValue)];
	[proto setTestValue:@"A string"];
	proto = [proto copy];
	[pool release];
	pool = [NSAutoreleasePool new];
	NSLog(@"Test value: %@", [proto testValue]);
	[proto setTestValue:[[MyObject2 new] autorelease]];
	[proto release];
	[pool release];
	return 0;
}

Posted 1 year, 8 months ago (December 16th, 2008, 12:48 p.m.)

   

  
  1. Nice work David :-)
    
    The main thing that I would suggest is to try to get closer to a classic prototype model:
    - both property and method slots can be added and removed explicitly 
    - access to a prototype inheritance chain by adding a method -prototype or -proto.
Should be #import "NSObject+Prototypes.h"
  1. Ooops - forgot this change when I imported it back to the tree.
Redefining the structure could be avoided by making it a pointer and allocate it from load(). I don't know if you think that's worth it (probably not since you chose to redefine the struct).
  1. It's defined here so it can be a static allocation, to avoid a malloc() on load.
It looks an awful lot like this is going to free when refCount hits 1 and not 0. What am I missing here?
  1. __sync_fetch_and_sub() is an atomic op.  It returns the value of the pointed-to first argument before the operation.  This is subtracting 1 from the refcount, and freeing if the old value was 1 (and since 1-1=0, the new value is 0)
Frameworks/EtoileFoundation/Source/NSObject+Prototypes.m (Diff revision 1)
 
 
 
 
 
 
 
 
 
Should there be a way to remove the NULL_OBJECT_PLACEHOLDER? Or is it a good idea to make it a one way street like this? Once you store nil in a slot, you cannot go back to inherit the value.
  1. I am not sure if this is desirable behaviour, but it might be I suppose...
  2. Well, the problem of the current implementation is that it misses methods such -addSlot:, -removeSlot: which are common in prototype-based languages.
    For example, in Io they would respectively declare the slot in the receiver with a null placeholder and remove it (whether it has a null placeholder or a real value).
  3. David's implementation implicitly adds the slot when you assign a value, much like JavaScript.
    I think -removeValueForKey: would do fine.
Could also be written as;

	for (Class cls = class ; cls != Nil ; cls = cls->super_class)
	{
		if (CLS_ISHIDDEN(cls))
  1. Yup.  An older version of this code modified cls in the loop.  Will fix.
Any reason not to use HiddenClass as return type here?
  1. Not any good ones.
Frameworks/EtoileFoundation/Source/NSObject+Prototypes.m (Diff revision 1)
 
 
 
 
 
 
 
 
 
 
I think you can avoid the need for a cloneWithZone: method.

In the objc_hidden_class struct you could add;

  id owner;

Then in hiddenClassTransform() extend the criteria and let the function that makes hidden classes know the owner;

  if (CLS_ISHIDDEN(obj->class_pointer) && ((HiddenClass)obj->class_pointer)->owner == obj)
  {
           return;
  }
  obj->class_pointer = (Class)hiddenClassForObject(obj);

This way, setMethod:forSelector: will subclass whenever you add methods to an object that does not already have it's own hidden class. No need to distinguish copy and clone.

Also, you could inline either hiddenClassForObject() in hiddenClassTransform() or hiddenClassTransform() in setMethod:forSelector:.
  1. cloneWithZone: is done intentionally, so that you can, if you want, have two objects which add methods to the same, shared, hidden class (not sure if this is useful, but a few prototype-based languages allow it).  On the other hand, having the owner in the class would let you get the prototype for the current object, so maybe that would be better.
    
    Inlining functions is what the compiler is for.
  2. -cloneWithZone: makes sense I think. For example, Io supports both clone() and shallowCopy() with the behavior described by David. The main benefit is that it helps code readability by making clear when you want a copy and when you want a clone, and how you deal with state mutability and inheritance. If only clone is available, you have be very careful with the inherited state. If copy is also available, it makes safer to use a prototype in a class role.
    
    A good example could be an UI builder such as Garnet or Newton ones.
    All your UI objects can share a single ObjC class behind the scene, this makes possible to have an uniform and abstract tree representation for the UI. Each specialized type of UI object rather than to be implemented as a subclass is then created as a clone/prototype. In the UI builder, the object palette contains these prototypes. To add a button to a window, you drag a button prototype into a window. At this point, rather than calling -copy as Gorm would do, -clone is called. If you use the 'copy' menu entry on the button that is now in the window, the button is duplicated by calling -copy on it rather than -clone. By doing so, changes made on the button prototype of the object palette will be replicated to all buttons but changes to a particular button won't be propagated to every buttons created by invoking -copy on it.
    
    Having an owner/proto ivar in the hidden class would be very welcome though. Unless I misunderstand something, this should do the trick to introduce a -prototype method that exposes the parent we inherit from.
  3. "If copy is also available, it makes safer to use a prototype in a class role."
    
    I'm afraid I don't understand this argument because copy is less safe than clone. When you change methods or slot values on a copy, you also change its prototype. To avoid that, you need to use clone. This is with the current semantics.
    
    My proposal was to have any method/slot change make sure the object has its own class. This means that copying is cheap unless you change the copy. I forgot to mention that setValue:forUndefinedKey: would have to call hiddenClassTransform()
    
    I do see one problem with this: If you copy the copy before changing it, the second copy will inherit the original, not the one it was copied from.