In Part 3 of this mini serie I`ll discuss how interfaces / (multi)inheritance like behaviour can be achieved in a language like C as in C++.
In the posts before (part 1, part 2) I discussed the possibility to inherit behaviour of an ‘object’ in C by adding a type pointer which handles initialization and destruction of the object; achieving the possibility to extend an ‘object’ by adding fields at the end and updating the type pointer to a new type.
Any object contains a pointer at the start to its type. The type of an object is passed to the CreateInstance
function when an instance needs to be created of the object. The type itself just contains a few fields:
- Length, the required length of the object, this allows an inheritance to extend the amount of data.
- Constructor Function Pointer, the Constructor Function Pointer is called after the object is allocated to construct its contents.
- Destructor Function Pointer, the Destructor Function Pointer is called before the object is freed to destruct its contents.
These fields, along with using function pointers in the instance itself instead of functions, allow an inherited type to expand the existing type. The problem is that an object can’t inherit from more than one object, for both object’s expect to start just after the pointer to the type. To solve this an additional field needs to be added:
- QueryInterface Function Pointer, the QueryInterface Function Pointer can be used to get an instance of a supported type from the instance called.
- Top Object Pointer, the Top pointer points to the top object. (An object returned from QueryInterface its Top pointer would point to the object where QueryInterface was called)
nb. The latter one will be added after the type pointer in the instance instead of in the type
A good example would be a type called ICloneable
which itself only contains one field: Clone, a functionpointer to a function to clone the object. When you would want to clone an object given you would use:
ICloneable* interface = toClone->top->type->QueryInterface(toClone->top->type, toClone->top, ICloneableType);
Object* clone = interface->Clone(interface);
Lets take a look to the first line:
ICloneable* interface = toClone->top->type->QueryInterface(toClone->top->type, toClone->top, ICloneableType);
This line queries the interface that the object that will be cloned wants you to use to clone it. The top object is used for that is the only way to be certain that you are dealing with the top object and the latest code instead of some base. Using this code an object can be cloned by just providing its interface!
(the QueryInterface takes 3 parameters: first one is the type itself where the function pointer relies, second is the object in question, third is the requested type where ICloneableType is an instance of the ICloneable type).
Object* clone = interface->Clone(interface);
The second line just calls the functionpointer in the interface queried.
To ensure that every bit of behaviour can be overriden, the object queried using ->top->type->QueryInterface
on the provided object must be used instead of the provided object itself for the provided object can be a whole different object merely supporting that interface secondarily.
The nice thing about this architecture is that a top object merely wraps its implemented objects and returns them (possibly addapted) when queried for it. This allows way more access to the inherited types than in any other object orientated model.
This model is quite easy to implement in C and (I hope) will be easy to intergrate with for instance objects in a Virtual Machine.