With respect to WDMF, an object is a data structure which may carry a private state and which may exhibit polymorphism facilitated through a common interface (often through) through dynamic dispatch.
Object-oriented programming languages have the advantage of decoupling knowledge of the implementation. Client code that utilizes object-oriented programming is more easily able to treat objects as a single entity rather than a composite data structure.
The C Programming language has no intrinsic concept of an object in the form discussed here, which means that implementing support for objects is often more explicit than in languages which natively understand this concept. WDMF Implements this using a series of preprocessor macros which are used to produce the structure for the object. Objects are laid out as follows:
struct object {
const struct vtable* WDMF_OBJ_VTABLE;
const struct data WDMF_OBJ_DATA;
};
... where WDMF_OBJ_VTABLE
refers to a table of virtual methods (the virtual table or vtable) and WDMF_OBJ_DATA
refers to the object's state (or data). All objects logically derive from thewdmf_obj_t
interface, which provides a minimal interface applicable to all objects. Thus, to access the variable bar
in an object foo
, the following should be done:
foo->WDMF_OBJ_DATA->bar = 1;
An object's vtable and data are specified according to their class. A class describes a group of entities with a common interface and set of attributes. Some classes may describe a subset of another class; in this case, this class is called a subclass of the original class, and the original class is called the subclass's superclass. When a class is a subclass of a superclass, it is said that the subclass extends the superclass.
To illustrate this point, consider taxonomy: mammals are a subclass of animals, and cats are a subclass of mammals. Mammals are a superclass of cats, but animals are also transitively a superclass because all mammals are animals. We refer to the mammal class as a direct superclass or base class of the cat class.
Because of how structs are defined in C, the fields corresponding to data section of the object are not directly copied to the new class's data segment, but are instead indirectly included by creating a field within the data section referencing the superclass's data. Such a data segment might look like the following:
struct Animal_data {
unsigned int age;
};
struct Mammal_Data {
struct Animal_data WDMF_OBJ_SUPER;
unsigned int legs;
};
struct Cat_data {
struct Mammal_data WDMF_OBJ_SUPER;
unsigned int whiskers;
};
The WDMF_OBJ_SUPER
macro provides the name of the data provided by the superclass. Thus, to access the legs
variable of a Cat instance, one should reference cat->WDMF_OBJ_DATA.WDMF_OBJ_SUPER.legs
. This can be chained to access data provided in higher superclasses (e.g., WDMF_OBJ_SUPER.WDMF_OBJ_SUPER.age
).
Not all classes need a data segment. According to C99, the size of a structure with no members is undefined, and pedantic compilers may refuse to emit code for an empty struct. Therefore, a special refinement of the class, the interface, is used to describe a class with no data. A class extends an interface by implementing it, which causes the subclass to omit its data section. Likewise, an interface does not create the structure for a data section (as it has no data). It is an error to extend an interface and undefined behavior will occur (mostly bad) whenever an class is implemented.
A new class or interface can be defined using the WDMF_OBJ_CLASS
, WDMF_OBJ_METHODS
and WDMF_OBJ_END_CLASS
macros. The WDMF_OBJ_CLASS
macro begins the definition of the object's data segment, whereas the WDMF_OBJ_VTABLE
begins the definition of the class's virtual table. The WDMF_OBJ_END_CLASS
macro finalizes the class definition. Below is the definition of a class named foo
that contains an instance variable named bar
and two methods, inc
and set
:
#include <wdmf/object.h>
WDMF_OBJ_CLASS(Foo)
int bar;
WDMF_OBJ_METHODS(foo)
WDMF_OBJ_DECLARE_METHOD(inc, int);
WDMF_OBJ_DECLARE_METHOD(set, void, int value);
WDMF_OBJ_DECLARE_METHOD(countdown, void, int newValue);
WDMF_OBJ_END_CLASS(foo)
Next, callbacks for each method should be defined separately. The WDMF_OBJ_METHOD
macro creates the prototype for the method,. To help prevent name collisions, the name of the method is mangled . The mangled name of the method can be obtained using the WDMF_OBJ_MANGLE_METHOD
macro. In the scope of a method, the WDMF_OBJ_THIS
can be used to refer to the object on which the method call targets. Below is a possible implementation of this class's inc
method:
WDMF_OBJ_METHOD(Foo, inc, int) {
return ++WDMF_OBJ_THIS.WDMF_OBJ_DATA.bar;
}
The above formulation can be verbose, so the WDMF_OBJ_THIS_DATA
and WDMF_OBJ_THIS_VTABLE
are defined to address this. These are equivalent to WDMF_OBJ_THIS->WDMF_OBJ_DATA
and WDMF_OBJ_THIS->WDMF_OBJ_VTABLE
respectively. Below is an example using the WDMF_OBJ_THIS_DATA
macro to implement foo
's set
method:
WDMF_OBJ_METHOD(Foo, set, void, int bar) {
WDMF_OBJ_THIS_DATA.bar = bar;
}
Within the context of a method, it is possible to write code invoking methods directly with
#include <stdio.h>
WDMF_OBJ_METHOD(Foo, countdown, void, int newValue) {
if (WDMF_OBJ_THIS_DATA->bar == 0) {
WDMF_OBJ_THIS_DATA.bar = newValue;
return;
}
printf("Value of foo->bar: %s", WDMF_OBJ_THIS_DATA.bar);
WDMF_OBJ_THIS_DATA.bar--;
WDMF_OBJ_THIS_VTABLE.countdown(WDMF_OBJ_THIS, newValue);
}
This can again be seen as verbose, so the WDMF_OBJ_THIS_CALL
macro is defined to replace it. This accepts the name of the method to call and its corresponding arguments. An example refactoring countdown
to use WDMF_OBJ_THIS_CALL
is provided below:
#include <stdio.h>
WDMF_OBJ_METHOD(Foo, countdown, void, int newValue) {
if (WDMF_OBJ_THIS_DATA->bar == 0) {
WDMF_OBJ_THIS_DATA.bar = newValue;
return;
}
printf("Value of foo->bar: %s", WDMF_OBJ_THIS_DATA.bar);
WDMF_OBJ_THIS_DATA.bar--;
WDMF_OBJ_THIS_CALL(countdown, newValue);
}
Every object has associated with it a special function called its destructor. The destructor is responsible for finalizing the object so that it may be safely deallocated.
WDMF_OBJECT_DEFINE_DESTRUCTOR(foo) {
/* Do Nothing */
}
This class's destructor is a no-op; therefore, it may be convenient to use the WDMF_OBJ_DEFAULT_DESTURCTOR
macro to emit the same code. This function sets the corresponding vtable entry to NULL
so that no additional code is emitted. Additionally, simply calling the destructor may fail to superclass destructors (see below). It is therefore an error to directly invoke the constructor. Instead, the function wdmf_obj_destroy
should be used to start the destruction process for an object.
The virtual table for the object must be defined separately using the WDMF_OBJ_DEFINE_VTABLE
and WDMF_OBJ_ATTACH_METHOD
macro. The destructor is implicitly included and should not be placed into the vtable.
WDMF_OBJ_DEFINE_VTABLE(foo)
WDMF_OBJ_ATTACH_METHOD(foo,inc)
WDMF_OBJ_ATTACH_METHOD(foo,set)
WDMF_OBJ_ATTACH_METHOD(foo,countdown)
WDMF_OBJ_END_VTABLE(foo)
So far, interacting with the object in the context of an object's methods has been discussed; however, no method of invoking the method outside that context has yet been provided. The WDMF_OBJ_CALL
macro allows this to happen. Additionally, the WDMF_OBJ_IDATA
macro may be used to directly access an object's fields. Suppose that we would like to invoke the methods inc
and countdown
on an instance of foo
:
#include <stdio.h>
foo instance*;
/* Construction happesn in between */
int value = WDMF_OBJ_CALL(instance, inc);
WDMF_OBJ_CALL(instance, countdown, value);
printf("Bar is now %u:\n", WDMF_OBJ_IDATA(instance, bar));
The WDMF_OBJ_INTERFACE
and WDMF_OBJ_END_INTERFACE
macros can be used to define a new interface. Consider the following interface:
WDMF_OBJ_INTERFACE(baz)
WDMF_OBJ_DECLARE_METHOD(callMe, void)
WDMF_OBJ_END_INTERFACE(baz)
This creates a structure like the following:
struct baz_vtable {
/* ... */
};
struct baz {
const struct baz_vtable* vtable;
};
Which is roughtly equivalent to the follwing class creation code (except it does not emit the class's data section):
WDMF_OBJ_CLASS(baz)
WDMF_OBJ_METHODS(baz)
WDMF_OBJ_DECLARE_METHOD(callMe, void)
WDMF_OBJ_END_CLASS(baz)
WDMF_OBJ_DEFAULT_DESTRUCTOR(baz)
Because the data section is not emitted, interface instances are more compact. Unlike in other programming, interfaces are permitted to be instantiated. This allows for interfaces to be used as function or strategy objects.
Many times, it is possible for a class to fit within a subclass
WDMF_OBJ_CLASS(animal)
int age;
WDMF_OBJ_METHODS(animal)
WDMF_OBJ_DECLARE_METHOD(eat, void, animal* other);
WDMF_OBJ_END_CLASS(animal)
WDMF_OBJ_CLASS_EXTENDS(mammal, animal)
unsigned int legs;
WDMF_OBJ_METHODS(mammal)
/* Inherited from animal */
WDMF_OBJ_DECLARE_METHOD(eat, void, animal* other);
/* Peculiar to class mammal */
WDMF_OBJ_DECLARE_METHOD(speak, void, const char* sentence);
WDMF_OBJ_END_CLASS(mammal)
WDMF_OBJ_CLASS_EXTENDS(cat, mammal)
unsigned int lives;
WDMF_OBJ_METHODS(mammal)
/* Inherited from animal */
WDMF_OBJ_DECLARE_METHOD(eat, void, animal* other);
/* Inherited from mammal */
WDMF_OBJ_DECLARE_METHOD(speak, void, const char* sentence);
WDMF_OBJ_END_CLASS(mammal)
This creates a new class mammal
which is a subclass of animal
. The class mammal
is said to inherit the state of class animal
when this is done. Internally, a mammal
's data section looks look the following:
struct mammal_data {
/* Data associated with animal, our superclass */
struct animal_data WDMF_OBJ_SUPER;
/* Data peculiar to class mammal */
unsigned int legs;
};
Note that we must explicitly repeat the class interface of animal
; currently, there is no good workaround for this (but it may be addressed in the future with an external class compiler). This is to ensure that all methods are included in the vtable when it is populated. However, it is still possible to inherit methods using the WDMF_OBJ_ATTACH_METHOD
macro:
/* Only necessary if the method is outside of the object's scope. */
extern WDMF_OBJ_METHOD(animal, eat, void, animal* other);
WDMF_OBJ_DEFINE_VTABLE(mammal)
/* Inherited from animal */
WDMF_OBJ_ATTACH_METHOD(animal, eat);
/* New method definition for mammal */
WDMF_OBJ_ATTACH_METHOD(mammal, speak);
WDMF_OBJ_END_VTABLE(mammal)
However, it is also possible to override the method eat
in class mammal
so that it behaves differently, even when the class is treated as an animal
. Consider the following: perhaps eat()
'ing causes the mammal
to grow additional legs, then to decrease other
's age. One such implementation of this might be possible:
WDMF_OBJ_METHOD(mammal, eat, void, animal* other) {
if (WDMF_OBJ_IDATA(other).age > 0) {
WDMF_OBJ_IDATA(other).age--;
}
WDMF_OBJ_THIS_DATA.legs++;
}
/* No need to forward-declare animal::eat() */
WDMF_OBJ_DEFINE_VTABLE(mammal)
/* Inherited from animal */
WDMF_OBJ_ATTACH_METHOD(mammal, eat);
/* New method definition for mammal */
WDMF_OBJ_ATTACH_METHOD(mammal, speak);
WDMF_OBJ_END_VTABLE(mammal)
The behavior can be completely replaced, or it may incorporate the behavior of its superclass into it by invoking the superclass's version of the method. References to the superclass's state and interface may be made with WDMF_OBJ_SUPER
, WDMF_OBJ_SUPER_DATA
, and WDMF_OBJ_SUPER_CALL
respectively. An object's immediately inherited data should be referenced through the WDMF_OBJ_SUPER_DATA
macro. Likewise, superclass methods should be invoked with WDMF_OBJ_SUPER_CALL
.
Consider now that a cat
's lives
are incremented every time it grows four more legs
. We wish to retain the behavior of mammal::eat()
, and we also need to access mammal
's legs
variable in order to do so. Such an implementation might look like this:
WDMF_OBJ_METHOD(cat, eat, void, animal* other) {
/* Invoke the superclass's implementation */
WDMF_OBJ_SUPER_CALL(eat, other);
/* Conditionally the number of lives */
if (WDMF_OBJ_SUPER_DATA.legs % 4 == 0) {
WDMF_OBJ_THIS_DATA.lives++;
}
}
Now suppose that when a mammal eat()
's, its age increases, and that when a cat's age
has increased and is a multiple of 4, its lives decrease. A method of obtaining animal
's age
field is needed; in order to do this, we access the superclass data of mammal
's superclass. to do this, we must append WDMF_OBJ_SUPER
to account for each superclass until we reach animal
's data.:
WDMF_OBJ_METHOD(cat, eat, void, animal* other) {
/* Invoke the superclass's implementation */
WDMF_OBJ_SUPER_CALL(eat, other);
/* Conditionally the number of lives */
if (WDMF_OBJ_SUPER_DATA.WDMF_OBJ_SUPER.age % 4 == 0) {
WDMF_OBJ_THIS_DATA.lives++;
}
}
The same can be done for method invoking a grandparent method (that is, a method's implementation that belongs to the superclass of a superclass); because this is not a common operation, no macro is provided for this functionality. However, by directly referencing the object's vtable, this is possible:
WDMF_OBJ_METHOD(cat, eat, void, animal* other) {
/* Invoke the grandparent's implementation (we need an explicit cast to
* animal here. */
WDMF_OBJ_THIS->WDMF_OBJ_VTABLE.WDMF_OBJ_SUPER.eat(
(animal*) WDMF_OBJ_THIS, other);
/* Conditionally the number of lives */
if (WDMF_OBJ_SUPER_DATA.WDMF_OBJ_SUPER.age % 4 == 0) {
WDMF_OBJ_THIS_DATA.lives++;
}
}
Note that it is possible for some methods to remain undefined or abstract. An abstract class or abstract interface contains at least one abstract method and is considered incomplete. This is useful when no reasonable implementation of the abstract method can be determined for the class. The method can be declared abstract in the class's interface by using the WDMF_OBJ_DECLARE_ABSTRACT
macro. The WDMF_OBJ_ABSTRACT_METHOD
macro is used to make it abstract in the vtable. The WDMF_OBJ_DECLARE_ABSTRACT
method is synonymous with WDMF_OBJ_DECLARE_METHOD
macro and is simply used for annotation. Below is an example which implements the above animal heirarchy.
thisdef.h
and thisndef.h
The macros provided above may still be considered too verbose to be useful; therefore, in certain situations, the wdmf/thisdef.h
header may be included to provide even shorter names. This is not done by default to prevent symbol collisions. The following macros should not be defined when including this header:
THIS
- equivalent to WDMF_OBJ_THIS
THIS_DATA
- equivalent to WDMF_OBJ_THIS_DATA
THIS_VTABLE
- equivalent to WDMF_OBJ_THIS_VTABLE
THIS_CALL(method, ...)
- equivalent to WDMF_OBJ_THIS_CALL(method, ...)
SUPER
- equivalent to WDMF_OBJ_SUPER
SUPER_DATA
- equivalent to WDMF_OBJ_SUPER_DATA
SUPER_VTABLE
- equivalent to WDMF_OBJ_SUPER_VTABLE
SUPER_CALL(method, ...)
- equivalent to WDMF_OBJ_SUPER_CALL(method, ...)
CLASS(name)
- equivalent to WDMF_OBJ_CLASS(name)
METHODS(name)
- equivalent to WDMF_OBJ_METHODS(name)
END_CLASS(name)
- equivalent to WDMF_OBJ_END_CLASS(name)
INTERFACE(name)
- equivalent to WDMF_OBJ_INTERFACE(name)
END_INTERFACE(name)
- equivalent to WDMF_OBJ_END_INTERFACE(name)
METHOD(class, method, rettype, ...)
- equivalent to WDMF_OBJ_METHOD(class, method, rettype, ...)
OBJ_CALL(instance, method, ...)
- equivalent to WDMF_OBJ_CALL(instance, method, ...)
IDATA(instance, var)
- equivalent to WDMF_OBJ_IDATA(instance, var)
DEFINE_VTABLE(class)
- equivalent to WDMF_OBJ_DEFINE_VTABLE(class)
END_VTABLE(class)
equivalent to WDMF_OBJ_END_VTABLE(class)
When none of these macros are defined, you may include wdmf/thisdef.h
for convenience. When you are done, you may later include wdmf/thisndef.h
to undefine these macros so that they no longer pose a risk for collision. This allows wdmf/thisdef.h
to be included in the future and permits for sections where these macros are locally interpreted as shorthand for the above. It is possible to refactor the foo
class specified above as follows:
#include <wdmf/object.h>
/* Use the compact synonyms to make the code more readable */
#include <wdmf/thisdef.h>
/* For printf() */
#include <stdio.h>
/* Declare class */
CLASS(foo)
int bar;
METHODS(FOO)
DECLARE_METHOD(inc, void);
DECLARE_METHOD(set, void, int value);
DECLARE_METHOD(countdown, void, int value);
END_CLASS(FOO)
/* Define methods */
METHOD(foo, inc, int) {
return ++THIS_DATA.bar;
}
METHOD(foo, set, void, int bar) {
THIS_DATA.bar = bar;
}
METHOD(foo, countdown, void, int newValue) {
if (THIS_DATA.bar == 0) {
THIS_DATA.bar = newValue;
return;
}
printf("Value of foo->bar: %s", THIS_DATA.bar);
THIS_DATA.bar--;
THIS_CALL(countdown, newValue);
}
/* Now attach the methods to the class */
DEFINE_VTABLE(foo)
ATTACH_METHOD(foo, inc);
ATTACH_METHOD(foo, set);
ATTACH_METHOD(foo, countdown);
END_VTABLE(foo)
/* We're done with our synonyms, release them */
#include <wdmf/thisndef.h>