mini_objc: A minimal Objective-C like runtime in less than 50 lines of code
Here is the runtime:
struct _objc_class;
struct _objc_object
{
struct _objc_class *isa;
};
typedef struct _objc_object *id;
typedef struct _objc_class *Class;
#define SEL int // it's a define, coz clang reasons...
#define nil ((id) 0)
#define Nil ((Class) 0)
struct _objc_class
{
id (*messenger)( id self, SEL _cmd, void *param);
struct _objc_class *superclass;
};
id _objc_call( id self, SEL _cmd, void *param)
{
Class cls;
if( ! self)
return( nil);
cls = self->isa;
return( (*cls->messenger)( self, _cmd, param));
}
id _objc_super_call( id self, SEL _cmd, void *param)
{
Class cls;
if( ! self)
return( nil);
cls = self->isa->superclass;
if( ! cls)
return( nil);
return( (*cls->messenger)( self, _cmd, param));
}
And here is a little Foundation written for this runtime. It consists of a root class:
enum selectors
{
SEL_dealloc = 1,
SEL_index,
SEL_length
};
struct root
{
struct _objc_object object;
};
static void root_free( struct root *self)
{
free( self);
}
static id root_messenger( id self, SEL _cmd, void *param)
{
switch( _cmd)
{
case SEL_dealloc : root_free( (struct root *) self); return( nil);
default : return( _objc_super_call( self, _cmd, param));
}
}
struct _objc_class root_class = { root_messenger, NULL };
And a string class, that is a subclass of the root class:
struct string
{
struct root root; // inherit instance variables
size_t length;
char buf[ 1];
};
// safe index into buffer, avoiding overflows
static int string_index( struct string *self, unsigned int index)
{
if( index >= self->length)
abort();
return( self->buf[ index]);
}
static size_t string_length( struct string *self)
{
return( self->length);
}
static id string_messenger( id self, SEL _cmd, void *param)
{
switch( _cmd)
{
case SEL_index : return( string_index( (struct string *) self, param));
case SEL_length : return( string_length( (struct string *) self));
default : return( _objc_super_call( self, _cmd, param));
}
}
struct _objc_class string_class = { string_messenger, &root_class }; // inherit methods
//
// the "factory" function for creating string instances
//
id alloc_string( char *s)
{
struct string *self;
size_t len;
len = strlen( s);
self = calloc( 1, sizeof( struct string) + len);
self->root.object.isa = &string_class; // make it an instance
self->length = len;
memcpy( self->buf, s, len + 1);
return( (id) self);
}
And some test code, to show that it all works. It’s educational to use a debugger and step through the code, to see what happens:
int main( int argc, char *argv[])
{
id s;
int c;
size_t i, n;
s = alloc_string( "VfL Bochum 1848");
n = (int) _objc_call( s, SEL_length, NULL);
for( i = 0; i < n; i++)
{
c = (int) _objc_call( s, SEL_index, (void *) i);
putchar( c);
}
putchar( '\n');
_objc_call( s, SEL_dealloc, NULL);
return( 0);
}
Isn’t this just a slower virtual functions table ?
The mini_objc messenger is not like a virtual functions table vtab like in C++. Translated to C++ concepts, the selector would be similiar to an index into the vtab. But the index of a virtual function is only common to that class and participating super- and sub-classes. You can not reliably message objects of classes, that aren’t related:
#include <iostream>
class Foo
{
public:
virtual void a( void) { std::cout << "a"; }
virtual void b( void) { std::cout << "b"; }
};
class Bar
{
public:
virtual void b( void) { std::cout << "b"; }
virtual void a( void) { std::cout << "a"; }
};
int main(int argc, const char * argv[])
{
Foo *foo;
Bar *bar;
foo = new Foo;
bar = new Bar;
foo->a();
bar->a();
((Bar *) foo)->a();
((Foo *) bar)->a();
return 0;
}
produces aabb
.
But a corresponding mini_objc program would print aaaa
, because the
selectors are global.
Because the selectors are global, using a virtual functions table instead of a switch is not feasible though. That’s due to memory constraints, when the number of selectors grows large.
Other thoughts
-
Although admittedly this runtime is mostly for educational purposes, in terms of performance it’s comparatively very good. That is, if the selector is handled immediately by the class of the object. Due to the forwarding by
default: _objc_super_call
calls get progressively slower the longer the inheritance chain is. -
Also mini_objc is thread-safe, leak-free, wait-free.
-
A single parameter looks limiting at first until you recognize, that you can pass in a pointer to an arbitrary struct.
-
The messenger ensures, that calls to nil self pointers are returning nil and not crashing.
-
The inheritance can be observed by tracing
_objc_call( s, sel_dealloc, NULL);
First
_objc_call
will useself->object.isa->messenger
which is thestring_messenger
. Since no selectors match insidestring_messager
, the default calls_objc_super_call
, which will find the next messenger asroot_messenger
viaself->root.object.isa->superclass->messenger
.
Some steps to make it a more complete Objective-C Runtime
- Make
struct _objc_class
inherit fromstruct _objc_object
. This allows for example to put the factory function for objects into the class messenger:
struct _objc_class
{
struct _objc_object object;
id (*messenger)( id self, SEL _cmd, void *param);
struct _objc_class *superclass;
};
-
An obvious weakness of this runtime is, that all selectors have to be predeclared. Most runtimes solve this by registering selector names. The mulle_objc runtime uses hashes of the selector name.
-
Protocols are missing, but could be easily added like this:
#define PROTOCOL int // it's a define, coz clang reasons...
#define MAX_PROTOCOLS_PER_CLASS 8
struct _objc_class
{
id (*messenger)( id self, SEL _cmd, void *param);
struct _objc_class *superclass;
PROTOCOL protocols[ MAX_PROTOCOLS_PER_CLASS];
};
int _objc_class_conforms_to_protocol( Class self, PROTOCOL _proto)
{
unsigned int i;
for( i = 0; i < MAX_PROTOCOLS_PER_CLASS; i++)
if( self->protocols[ i] == _proto)
return( 1);
return( 0);
}
- Add introspection capabilities, like f.e. names to classes:
struct _objc_class
{
id (*messenger)( id self, SEL _cmd, void *param);
struct _objc_class *superclass;
char *name;
};
char *_objc_class_get_name( Class cls)
{
return( cls->name);
}
- Hypothetically categories could be added to the messenger function at link time. (Like templates are compiled by C++ at link time).
What’s the use ?
For a future article it’s good, if you, the cherished reader, understand on a C level how message sending works :)
Post a comment
All comments are held for moderation; basic HTML formatting accepted.