Nat! bio photo

Nat!

Senior Mull

Twitter Github Twitch

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 use self->object.isa->messenger which is the string_messenger. Since no selectors match inside string_messager, the default calls _objc_super_call, which will find the next messenger as root_messenger via self->root.object.isa->superclass->messenger.

Some steps to make it a more complete Objective-C Runtime

  • Make struct _objc_class inherit from struct _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.

Name:
E-mail: (not published)
Website: