News: Stay up to date

The Étoilé community is an active group of developers, designers, testers and users. New work is being done every day. Visit often to find out what we've been up to.

News

Prototypes and LangaugeKit

Posted on 20 December 2008 by David Chisnall

Over the last few days, I've been playing around with rewriting the prototypes code not to require a modified runtime. The old code used a new (backwards-compatible) version of the GNU Objective-C runtime that added a 'third chance' dispatch mechanism, allowing objects to implement their own method lookup function.

The new version works by doing a hidden class transform. One of the unused flags in the class structure has been set to define whether a class is a hidden class or a real one. Hidden classes are just like normal classes, but they are generated at runtime and inserted at the end of the inheritance chain. When you send an object a -becomePrototype message, it inserts a new class/metaclass pair as its class, with the old class becoming its new superclass.

Since I'm inserting new classes, this gave the opportunity to add some other information, most usefully an NSMutableDictionary for storing extra values. In Smalltalk, there is a mechanism a lot like this. Instance variables in the Objective-C sense are called 'indexed instance variables,' but others stored in a linked list are also possible (but slightly slower to access).

The hidden class implements a few methods of its own. The most important of these are the two related to Key-Value Coding, meaning that you can use the standard KVC methods to access this hidden dictionary. In Objective-C, you simply do something like this:

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"];
}

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"];
    NSLog(@"Test value: %@", [proto testValue]);
    return 0;
}

This will output something like this, when run:

2008-12-20 16:16:29.054 test[6813] Test value: A string

Of course, this is not possible in Smalltalk, since Smalltalk doesn't have the ability to write functions outside of classes. It does, however, have the ability to create blocks.

This week, I tweaked the code slightly so that if you insert a block into this dictionary as an object, it implicitly registers a trampoline method for the key as a selector. When you call the method, the trampoline uses the _cmd argument (the hidden argument in all Objective-C methods giving the selector used to look up the current method) to get the block from the dictionary and then sends it a -value: message, or a value:value: message (and so on) depending on the number of arguments that the block expects.

This lets you write code in Smalltalk like this:

NSObject subclass: SmalltalkTool
[
    run 
    [
    | a |
        a := NSObject new.
        a becomePrototype.
        a setValue:[ :object :aValue | ETTranscript show:aValue; cr ] 
        forKey:'logValue:'.
        a logValue:'A string'.
    ]
]

When you run this with edlc, you get:

A string

Obviously this is slower than a direct method call, since you first enter the trampoline, then look up the block in a dictionary, and then finally call the block, but it should be a lot faster than using forwarding. On GNUstep, sending a message via forwardInvocation: takes around 300 times as long as a direct message send. A message send to a block costs around 3 times as much as a message send to an Objective-C object. A message send to a prototype costs around ten times as much. Note that a lot of the extra overhead from the block (which has a similar overhead to calling a Smalltalk method) is due to the need to clean up the object context at the end of the call.