Next: Memory handling, Previous: Implementing Classes, Up: Top [Contents][Index]
Since version 1.3 ooc introduces a kind of multiple inheritance with the help of interfaces. The idea behind is a bit a mix of the Java and C++ interfaces, but differs in the way of inheritance and the use.
An interface is simply a collection of functions that describe the behavior of the Object in some aspect. The interface itself does not implement any functionality, it just defines what methods the Object must have, and behave according to it. In some design methods this is called a contract for the Object. The Object should implement its own implementation of the contract: this is called impementing the interface. In Java the interface is a pure abstract class without a data member, in C++ it is called a pure virtual class without constructor, destructor and data members.
The easiest way to define a group of functions is to collect some function pointers in a C struct. For example in pure C we would write:
struct DrawableInterafce { void (* draw)( Object ); void (* rotate)( Object, double ); int (* get_heigth)( Object ); int (* get_width)( Object ); };
This describes the behavior of a drawable Object
. Any Object type that implements a DrawableInterafce
can be asked for its dimensions and can be drawn and rotated.
The use of interfaces provides a kind of multiple inheritance for ooc. While the classes can be inherited in a single inheritance chain (each class can have only one superclass), every class can implement as many interfaces as nedded. Since interfaces can be implemented by unrelated classes, it is a kind of multiple inheritance, like in Java.
In ooc interfaces are simply added to the virtual table, so you can reference and use them as any other virtual function of the class! This is very similar to the C++ implementation of an interface.
As a consequence, in ooc the interface implementation is inherited by the subclasses and can be overriden (like in C++, and unlike Java).
The good news is that by the help of interfaces ooc introduces multiple inheritance. The bad news is, that multiple inheritance calls for the dread problem of diamond inheritance (http://en.wikipedia.org/wiki/Diamond_problem).
Since an ANSI-C compiler has nothing to handle such a situation, we must avoid any possibility of a diamond inheritance, thus the interfaces themselves can not be inherited (other words: can not be extended) in ooc.
The following table summarizes the behaviour of interfaces in different languages:
ooc | C++ | Java | |
---|---|---|---|
The implementation of the interface is inherited by subclassing a class | yes | yes | no |
Interface methods can be overriden in subclasses | yes | yes | must be |
The interface itself can be inherited (a.k.a. extended in Java) | no | yes | yes |
Creating an interface is very easy in ooc: you just simply deaclare its members in a publicly available header file. For example we create an interface for a drawable Object:
In drawable.h
:
DeclareInterface( Drawable ) void (* draw )( Object ); void (* rotate )( Object, double ); int (* get_heigth )( Object ); int (* get_width )( Object ); EndOfInterface;
Please note, that the first parameter for each method in the interface is Object
! This is, because any kind of a Class
can implement the interface, so we can not limit the type of the Object
for just a given class.
Each interafce has a unique identifier, an interface descriptor that must be statically allocated (in ROM on microcomputers). This can be done in drawable.c
, or if we have many interfaces, collected them together in e.g. interfaces.c
:
#include "drawable.h" AllocateInterface( Drawable );
That’s it! We have done: the interface is declared and has a unique id: Drawable
.
To minimize your work you can use the ooc tool to create the skeleton:
~$ ooc --new Drawable --template interface
To implement an interface for a class, you must do the followings:
So let’s say we implement the Drawable interface for the Cat
class! In the followings we
show only the steps necessary to implement the interface, other issues covered earlier are not repeated here!
Implelementing multiple interfaces for the class is the same, just repeat the necessary macros and lines!
As an example, along with the Drawable
interface we add the Pet
interafce as well.
In the public header (cat.h) locate the Virtuals
, and add the interface with the Interface
macro:
Virtuals( Cat, Animal ) void ( * miau )( Cat ); Interface( Drawable ); Interface( Pet ); EndOfVirtuals;
The interface method impementations are static
functions in the class implementation file.
This is exactly the same aproach to the implementation of any virtual method. (Do not forget: an interface is just a
collection of virtual functions in ooc.)
So in cat.c write:
static void cat_rotate( Cat self, double arcus ) { assert( ooc_isInstanceOf( self, Cat ) ); // Rotate your cat here :-) }
In the class initialization code you must assign the the implementation methods with the appropriate function pointers in the virtual table. Again, see the section about virtual functions.
static void Cat_initialize( Class this ) { CatVtable virtuals = (CatVtable) this->vtable; virtuals->Drawable.rotate = (void (*)(Object, double)) cat_rotate; // add the other methods as well ... }
There is a way to retrieve the implemented interfaces for any class, that is the ooc_get_interface()
function. To let it work, we must
register all implemented interfaces for the class. This is done in the class implementation file, preferably just right before the class allocation.
So, in cat.c we register the Drawable and Pet interfaces for the Cat class:
InterfaceRegister( Cat ) { AddInterface( Cat, Drawable ), AddInterface( Cat, Pet ) };
Listen to the different syntax! Internally this is a table of structs, so you must end it with a semicolon, and use comma as the internal list separator.
You must not put a comma after the last item in the list!
ooc_get_interface()
scans this table, so you can get some increase in speed of average execution, if you place your most frequently used interfaces
in the first positions.
The only thing remained is to let the ClassTable know about the interface register. So instead of AllocateClass
use AllocateClassWithInterface
:
AllocateClassWithInterface( Cat, Animal );
If you forget this step, ooc_get_interface()
will always return NULL
, since it will not know about the registered interfaces for the class.
With this step we have finished the implementation of an interface for a class.
When we need to invoke an interface method, simply call the virtual function via the function pointer in the virtual table. There can be two situation in practice: we may know the type of the object we have, or may not.
If we know the type of the object we have, and this type has implemented the interface that we need, then we can simply call the virtual function of the interface.
Cat mycat = cat_new(); // rotating mycat: CatVirtual(mycat)->Drawable.rotate( (Object) mycat );
This is the fastest way to invoke an interface method, since this is a dereferencing of a function pointer only.
The main use of an interface is when we do not know the exact type of the object we have, but we want it to behave according its interface method. The ooc_get_interface()
method can retrieve a given interface from any kind of Object
. The ooc_get_interface()
function returns a pointer to the interface
methods of the class, and the desired method can be called via this function pointer.
void rotate_an_animal( Animal self ) { Drawable drawable = ooc_get_interface( (Object) self, Drawable ); if( drawable ) drawable->rotate( (Object) self ); }
ooc_get_interface()
returns NULL
if the desired interface is not implemented for the object. If you use this function, always check for a NULL
pointer, otherwise you will generate a segmentation fault, if the interface is not implemented.
In case you know that your object has implemented the desired interface, but you do not know the exact type of your object, you can use the ooc_get_interface_must_have()
function instead. This will never return a NULL
pointer, but throws an Exception
if the object does not have the desired interface.
ooc_get_interface_must_have( (Object) self, Drawable )->rotate( (Object) self );
Note: the ooc_get_interface()
function traveses the InterfaceRegister for the class and its superclasses, so it may be relative slow. If you use the interface for an object multiple times within a context, it is worth to retrieve the interface pointer only once for the object, and keep it around until needed.
The mixins are interfaces that have default implementations and have their own data (http://en.wikipedia.org/wiki/Mixin).
A mixin does not know anything about the enclosing Object / Class!
From the user point of view a mixin looks identical to an interface! See Using an interface.
A mixin has its own data, but it can not be instantiated directly. It is always held by an Object (a carrier object).
A mixin has the following features:
The mixin is nothing else than an interface impementation plus a data structure. But this data structure is handled like a class, with very similar methods: there are initialization, finalization, construction, destruction and copy methods like for a normal Class, and an additional one: the ..._populate() method.
In this section we go through the steps creating a mixin.
To create the mixin skeleton for Flavour
, just type:
~$ ooc --new Flavour --template mixin
Three files has been created:
The flavour.h is familiar, it looks like a normal interface declaration except it is declared with DeclareMixinInterface()
.
Put your interafce methods (function pointers, of course) into the declaration as needed.
There is a implement/flavour.h file, that contains the data member declaration for the mixin. Add your data members as needed.
These data fields will be mixed into the enclosing class’s object structure, and can be referenced via a FlavourData
type pointer (interface name + Data
suffix).
In the flavour.c file you will find the necessary AllocateMixin
macro, and you must provide the method implementations for the mixin. The following methods must be implemented:
Flavour_initialize()
This is called only once, and is called automatically before the initialization of the first enclosing class.
Shared or global resources used by the mixin may be initialized here.
Flavour_finalize()
Called only once when ooc_finalize_all()
is called.
Release the shared or global resources that were initialized by the mixin.
Flavour_constructor( Flavour flavour, FlavourData self )
Initializes the data fields of the mixin. All data fields are garanteed to be zero at entrance.
This method is called automatically before the enclosing class’s constructor is called.
If your data fields do not require any construction, you can leave this method empty.
The interface methods are set (and are overriden by the enclosing class), so they can be used here. (The first parameter.)
Note, that there are no constructor parameters! There is only "default constructor" for mixins. If you need some values or construction depending on the enclosing object then implement a setter or constructor method in the interface itself and call it from the enclosing class’s constructor!
Flavour_destructor( Flavour flavour, FlavourData self )
Destroys the data fields of the mixin.
This method is called automatically after the enclosing class’s destructor, and the normal rules within a desctructor must be applied here as well!
If your data fields do not require any destruction, you can leave this method empty.
The interface methods are set (and are overriden by the enclosing class), so they can be used here. (The first parameter.)
Flavour_copy( Flavour flavour, FlavourData self, const FlavourData from )
Copy constructor. This is very similar to the copy constructor used for Object
s! Must return: OOC_COPY_DEFAULT
, OOC_COPY_DONE
or OOC_NO_COPY
, and the same rules apply!
This method is called automatically before calling the enclosing class’s copy constructor.
The interface methods are set (and are overriden by the enclosing class), so they can be used here. (The first parameter.)
Flavour_populate( Flavour flavour )
Populates the interface method table with function pointers to the default mixin method implementations.
This method is called automatically before calling the enclosing class’s initialization method.
The mixin interface methods’ implementation must follow an important rule! Since the mixin does not know where its data is located, the mixin’s data must be retrieved first!
The only thing that a mixin method knows is the carrier Object
(always the first parameter for an interface method!). Retrieving the mixin’s data fields can be done with the ooc_get_mixin_data()
function.
Therefore a typical mixin interface method starts with the followings:
static void flavour_set( Object carrier, const char * flavour_name ) { FlavourData self = ooc_get_mixin_data( carrier, Flavour ); self->name = flavour_name; }
The implementation of a mixin for a class is almost the same as the implementation of an interface, with few additional tasks.
The steps to implement some Flavour
for the IceCream
class are:
IceCream
class with ~$ ooc --new IceCream.
Interface( Flavour );
macro to the IceCream
virtuals in icecream.h.
MixinData( Flavour );
data member to the end of the ClassMembers( IceCream, Base )
structure in implement/icecream.h.MixinData()
macros in the ClassMembers()
list, otherwise the copy constructor will not work correctly!
IceCream
in icecream.c.
InterfaceRegister( IceCream ) { AddMixin( IceCream, Flavour ) };
AllocateClass( IceCream, Base );
macro with AllocateClassWithInterface( IceCream, Base );
in icecream.c.
Flavour
interface methods as needed in IceCream_initialize()
.
Flavour
mixin while constructig an IceCream
object, if the default constructor is not sufficient for you.
static void IceCream_constructor( IceCream self, const void * params ) { assert( ooc_isInitialized( IceCream ) ); chain_constructor( IceCream, self, NULL ); IceCreamVirtuals( self )->Flavour.set( (Object) self, "vanilla" ); ... }
Mixins work like a superclass for the enclosing class: you do not have to care with it, ooc does everything automatically for you:
..._populate()
method is called.)
ooc_finalize_all()
is called.
This is the same behavior of a superclass, you can treat the implemented mixin as a superclass of the enclosing class as well, except you can not cast the types (Mixins do not have type information at all.), and you must access it as an interface via its interface methods.
However, mixins have some drawbacks!
First: it exposes all mixin data members to the enclosing (carrier)
class: the enclosing class can acces the mixin’s data directly, and it is up to the programmer’s intelligence
not to do bad thigs. :-(
Second: using interafces and mixins frequently is a very elegant design, but be aware that in ooc it is a bit expensive! Since a C compiler gives no support for such implementations, every lookup, check and conversion is done in run-time, thus much slower than in C++ for example.
Next: Memory handling, Previous: Implementing Classes, Up: Top [Contents][Index]