Follow our illustrated blog on Embedded Software Architecture

How to build a hybrid solar/wind energy harvester?

Object-orientation in C – Part 4

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)

We continue to build on the example in Part 3, but we go another step further. Even more abstraction and information hiding can be achieved by partially mimicking (part of) the COM manner of programming: strictly talking with interfaces. This style of coding is not for everybody, nor is it a fit for every project. The amount of work that is involved in setting up the system structure is high. But let us see how it works out as a further evolution of previous episodes. Therefor, the metamorphose is not yet complete. It is an intermediate step! In the context of this example, it might be difficult to evaluate the advantages of this Part 4 when e.g. compared with Part 2. This Part 4 setup is more for bigger systems and interchangeable modules. But it is very clean and maintainable, and achieves encapsulation and information hiding very well. In fact, it does it better than how people usually do C++.

First we define a base vtable class. The mother (or father) of all vtables.


typedef struct _vtableItf
{
  const void* (*QueryItf)(const void* object, const void* vtable);
}
vtableItf;

Query

The idea is to query for interfaces (i.o.w. other vtables) and to hide all object implementation details from the object user. Only interfaces are exposed, which brings us even more information hiding. With real COM, a UUID (a very large unique number) is used to “query” the availability of an interface. Here, we will use the pointer to the vtable as our UUID within our program context.
The shape vtable is then defined as:


typedef struct _shape_vtable
{
  vtableItf BaseItf;
  shapePtr (*alloc)(void);
  void (*free)(shapePtr);
  void (*set_label)(shapePtr, const char*);
  const char* (*get_label)(const shapePtr);
  int (*get_area)(const shapePtr);
}
shape_vtable;

and the circle vtable:


typedef struct _circle_vtable
{
  shape_vtable base_vtable;
  void (*set_radius)(shapePtr s, int radius);
  int (*get_radius)(const shapePtr s);
}
circle_vtable;

So, shape_vtable inherits from vtableItf. And circle_vtable inherits from shape_vtable in this case (which is not required but was handy for the implementation details in this case and because it build on Part 3). We are not going into detail about the implementation code for shape or circle, but instead we look at the usage in the main.c file first:


#include <stdio.h>
#include "shape.h"
#include "circle.h"

int main(int argc, char ** argv)
{
  shapePtr s = circle_alloc(); // (1)
  const vtableItf* Itf = vtable(s); // (2)
  const shape_vtable* ItfShape =
    Itf->QueryItf(s, &shape_vtable_imp); // (3)
  const circle_vtable* ItfCircle =
    Itf->QueryItf(s, &circle_vtable_imp); // (4)
  ItfShape->set_label(s, "my_circle");  // (5)
  ItfCircle->set_radius(s, 10); // (6)
  printf("label by vtable: %s\n",
    ItfShape->get_label(s)); // (7)
  printf("area by vtable: %d\n",
    ItfShape->get_area(s)); // (8)
  ItfShape->free(s); // (9)
  return 0;
}

Interfaces

All object interaction is performed through interfaces.
In step (1), the object is allocated. A circle is allocated, but the function returns a shape pointer. Note, that this even could be an opaque pointer, since the inner object details are never revealed or exposed to the user. As a second note, we could also allocate the object via the already exposed circle_vtable_imp. In that case, we would call circle_vtable_imp.base_vtable.alloc().
In step (2), the base vtable (vtableItf) pointer is retrieved from the object. From then on, we can query for functionality or interfaces which might be supported (or not, in that case NULL is returned).
To set the label we need a basic shape interface functionality. To set the radius of a circle, we need the circle interface functionality. We query both interfaces in step (3) and (4), which will be successful since it – obviously – is a circle object.
We do our business in step (5), (6), (7) and (8).
At step (9), we can use the basic shape interface, to get rid of the object. And we are done.

The ‘magic’ is performed by the QueryItf function. The implementation for shape (in shape.c):


static const void* QueryItf(const void* object, const void* vtable)
{
  if (vtable == &shape_vtable_imp) {
    return ((shapePtr) object)->vtable;
  }
  return NULL;
}

and the implementation for circle (in circle.c):


static const void* QueryItf(const void* object, const void* vtable)
{
  if (vtable == &circle_vtable_imp) {
    return ((circlePtr) object)->vtable;
  }
  else if (vtable == &shape_vtable_imp) {
    return &((circlePtr) object)->vtable->base_vtable;
  }
  return NULL;
}

The source code of this example setup, is available for download here. In Part 5, we will cleanup the example and work with opaque handles.
 

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
Share
About rtos.be

rtos.be is a joined initiative from Gert Boddaert and Pieter Beyens.
More info: about us, our mission, contact us.

Speak Your Mind

*