Menu

Object

Phillip Kilgore

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.

Rationale

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.

Implementation

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.

Class Definition

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));

Interface Definition

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.

Extending Classes

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++;
    }
}

Implementing Interfaces

Abstract Classes

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>

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.