RK0

Embedded Real-Time Kernel '0'

This article is about Commonality, Generalisation and Clean-Code


While reusing for the sake of reusing is not always technically wise, when software evolution is a top concern, as on Software Product Lines, the first question to be answered is: what is common? It has been a long time since Parnas (on a balanced approach) coined the idea that eventually was classified as the Dependency Inversion Principle (the ‘D’ on SOLID): given two modules communicating, they should not depend on each other; they should depend on abstractions. Abstraction is one of the most overloaded words in software, and here it means generalisation, in the form of an interface.

But how to deal with the details of the concrete implementation? You write them so they will conform to abstractions. This rule is often phrased as:

« High-level modules should not depend on low-level modules and both should depend on abstractions. Plus, abstractions should not depend on details; details should depend on abstractions. »

It is entirely aligned with the definition of an Abstract Data Type: it is completely defined by its methods (what it does). The whole point is that you define a general interface for a module, which also happens to be general (for example, a carbon dioxide sensor client), and what goes inside is written so that it conforms to the generalisation.

Not the opposite. That is, our implementation its from the inside out. We expose what a module does, and how it does is not everyone’s concern. Most importantly, this same shape should be able to adapt by changing the inside, without altering the interface.

Typically, high-level developers (e.g., Java) would argue that deviating from this is bad code, and they have a point. Interestingly, some embedded developers (now pushing device trees on 64KB RAM MCUs) would say the same. In this case, it makes me wonder.


The implementations below aim to discuss generalisation as a panacea and the so-called clean code.

In this case we will be going for a cliché: a Polygon object with a method area(). First, we write it in C++, which turns out to be a first chapter example. Then, we go to C and discuss the so-called ‘best practices’.

In C++ the implementation is straightforward.

#include <iostream>
#include <string>

class Polygon 
{
public:
    virtual uint32_t area() = 0;
    virtual ~Polygon() = default;
};

/*
C++ does not have an Interface keyword. 
You have to define interfaces through inheritance 
mechanisms, as we do to inherit members. 
To declare an interface, declare a pure virtual class. 
To implement an interface, derive from it.
*/
class Triangle : public Polygon 
{
public:
    Triangle(uint32_t height, uint32_t base, const std::string& name)
        : height(height), base(base), name(name) 
    { }

private:
    uint32_t height;
    uint32_t base;
    std::string name;

    uint32_t area() override 
    {
        std::cout << "Identified " << name << " as a triangle." << std::endl;
        return (height * base) / 2;
    }
};

class Rectangle : public Polygon 
{
public:
    Rectangle(uint32_t height, uint32_t base, const std::string& name)
        : height(height), base(base), name(name) 
    { }

private:
    uint32_t height;
    uint32_t base;
    std::string name;

    uint32_t area() override 
    {
        std::cout << "Identified " << name << " as a rectangle." << std::endl;
        return height * base;
    }
};

int main(void) 
{
/* this is not modern idiomatic c++ */
    Triangle t1{5, 6, "polygon1"};
    Rectangle r1{9, 5, "polygon2"};

    Polygon* p1 = &t1;
    Polygon* p2 = &r1;

    std::cout << "area: " << p1->area() << std::endl;
    std::cout << "area: " << p2->area() << std::endl;

    return 0;
}

Output

Identified polygon1 as a triangle.
area: 15
Identified polygon2 as a rectangle.
area: 45

To implement it in C, besides using an object-based coding style we need a bit of crafting to create the late binding, as I will be explaining. An abstract class as “Polygon” in C , or the base class, is part of every concrete class that derives from it. It means that a given polygon instance, let it be a square or a hexagon will inherit this interface. Inheritance is fundamental for polymorphism.

Note that the derived class is inheriting an i-n-t-e-r-f-a-c-e. There is a lot in common between different polygons and in how we manipulate them, so we can easily think of such an interface.  We loosely say abstract classes can't be instantiated because they are meaningless without concrete classes. 
UML class diagram can be read as “Rectangle is a Polygon”.

The implementation:

/** 
@File: polygon.h
*/
#ifndef POLYGON_H 
#define POLYGON_H
#include <stdint.h>
struct VirtualTable; /* forward declaration */
typedef struct
{
	struct VirtualTable const* vptr;
} Polygon_t;
struct VirtualTable 
{
	uint32_t (*CalcArea)(Polygon_t const* const me);
};
void Polygon_Ctor(Polygon_t* const me);
/* inline to save a function call */
static inline uint32_t CalcArea(Polygon_t const* const me)
{
	return (*me->vptr->CalcArea)(me);
}
#endif
/** 
@File: rectangle.h
*/
#ifndef RECTANGLE_H
#define RECTANGLE_H
#include "polygon.h"
typedef struct
{
	Polygon_t Super; /* <-Base Class */
	uint32_t base;
	uint32_t height;
    const char* name;
} Rectangle_t;
void Rectangle_Ctor(Rectangle_t* this_rectangle, uint32_t base, uint32_t height, const char* name);
#endif
/**
@File: triangle.h
*/
#ifndef TRIANGLE_H
#define TRIANGLE_H
#include "polygon.h"
typedef struct
{
	Polygon_t Super;
	uint32_t base;
	uint32_t height;
    const char* name;
} Triangle_t;
void Triangle_Ctor(Triangle_t* this_triangle, uint32_t base, uint32_t height, const char* name);
#endif
/**
@File polygon.c
*/

#include "polygon.h"
#include <assert.h>
#include <stdlib.h>
static uint32_t CalcArea_(Polygon_t const* const me);
void Polygon_Ctor(Polygon_t* const this_polygon) 
{
	assert(this_polygon != NULL);

	static struct VirtualTable const vtbl =
	{
			&CalcArea_
	};
	this_polygon->vptr = &vtbl;
	
}
/* you can see this function as a pure virtual method
 it will always be overriden by a concrete implementation
*/
static uint32_t CalcArea_(Polygon_t const* const me)
{
	assert(0); /* if called, bang */
	return (0); 
}

The base-class (or the Super class) has a pointer vptr to a VirtualTable structure. In this case this virtual table has a single function pointer. When called it relies on the caller type to tell which function will be dispatched. When constructing the derived object you bind an interface address to the object’s own base class’s virtual table. Before you have to construct the interface within the object, by injecting the address of the virtual table that points to a concrete implementation.

/*
@File: triangle.c
*/
#include <stdlib.h>
#include "polygon.h"
#include "triangle.h"
//forward declaration of the concrete implementation
static uint32_t CalcAreaTriang_(Polygon_t const* const this_polygon);
void Triangle_Ctor(Triangle_t* this_triangle, uint32_t base, uint32_t height, const char* name)
{
    assert(this_triangle != NULL);
	static struct VirtualTable const vtbl =
	{
		&CalcAreaTriang_ 
	};
	
     /* constructing the base-class */
 		Polygon_Ctor(&this_triangle->Super); 
     /* binding this vtable to base-class table */
		this_triangle->Super.vptr = &vtbl; 
    /* defining class members */
		this_triangle->base = base;
		this_triangle->height = height;
		this_triangle->name = name;
	
}
static uint32_t CalcAreaTriang_(Polygon_t const* const me)
{
	Triangle_t* this_triangle = (Triangle_t*)me; //downcast
	printf("Identified %s as a triangle.\n\r", this_triangle->name);
	return ((this_triangle->height) * (this_triangle->base))/2;
}
/*
@File: rectangle.c
*/
#include "polygon.h"
#include "rectangle.h"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
static uint32_t CalcAreaRect_(Polygon_t const* const this_polygon);
void Rectangle_Ctor(Rectangle_t* this_rectangle, uint32_t base, uint32_t  height, const char* name)
{
	assert(this_rectangle != NULL);
    static struct VirtualTable const vtbl = 
	{
		&CalcAreaRect_
	};
	Polygon_Ctor(&this_rectangle->Super);
	this_rectangle->Super.vptr = &vtbl;
	this_rectangle->base = base;
	this_rectangle->height = height;
	this_rectangle->name = name;
	
}
static uint32_t CalcAreaRect_(Polygon_t const* const me)
{
	Rectangle_t* this_rectangle = (Rectangle_t*)me;
	printf("Identified %s as a rectangle.\n\r", this_rectangle->name);
	return (this_rectangle->base) * (this_rectangle->height);
}

Since the private function which calculates the area receives a pointer to Polygon_t, we perform a downcast (a cast from base class to a derived class, Polygon_t to Triangle_t, e.g.) to access the data we need.

To use our fancy polymorphic CalcArea function we either perform an upcast on the object (e.g., from Triangle_t to Polygon_t) or access its Super class’ member.

#include <stdio.h>
#include <stdlib.h>
#include "polygon.h"
#include "rectangle.h"
#include "triangle.h"
int main(void)
{
    /* declaring objects */
    Rectangle_t r1;
    Triangle_t t1;
    Rectangle_t r2 ;

    /* constructing objects */
    Rectangle_Ctor(&r1, 10, 20, "Polygon1");
    Triangle_Ctor(&t1, 5, 10, "Polygon2");
    Rectangle_Ctor(&r2, 300, 2, "Polygon3");

    /* computing areas polymorphically */
    printf("area: %d\n\r", CalcArea(&r1.Super));
    printf("area: %d\n\r", CalcArea((Polygon_t*)&t1));
    printf("area: %d\n\r", CalcArea(&r2.Super));

    return (0);
}
Identified Polygon1 as a rectangle.
area: 200
Identified Polygon2 as a triangle.
area: 25
Identified Polygon3 as a rectangle.
area: 600

The key take away here is as follows:

  • When we call CalcArea(&r1.Super), we are effectively telling the compiler that we are passing a Polygon_t object, which happens to be part of a Rectangle_t.
  • The compiler is clueless of the derived class at this point; it only recognises that it’s dealing with a Polygon_t.
  • Only at runtime, the method is routed to the one belonging to a Rectangle_t implementation, because we wrote it. This dynamic redirection is a handcrafted late-binding.

We agree (I guess?) that complex code does not mean good code. In fact, we could achieve the very same if Polygon_t had a member whose value indicated its concrete form (triangle, rectangle, etc.) and CalcArea(Polygon_t*) implemented a switch-case to call the appropriate area computation. With this approach, every new concrete class would touch the interface implementation.

From a purist perspective, the virtual table is more extensible and will evolve better as we never touch the interface itself (Open-Closed Principle). That might be true in C++.

In C, I would have a hard time advocating for virtual tables over static ones. The unique argument would be that it is ‘breaking the Open-Closed Principle‘. So, sue this code and take him to court?


Motivational example: Madonna and the Sparrow
An abstract data type (ADT) emphasises form over matter—thus, in software, an ADT is an entity defined by its behaviour. Suppose something can walk, talk, and sing. It might be Madonna or a brown sparrow.

Both share these capabilities, though each has unique behaviours—like playing back recordings (Madonna) or fly'n'poop®  (the songbird).

Suddenly, “Madonna is a sparrow that can’t quite fly-poop” and "A sparrow is a Madonna that can't quite autotune" are valid statements in our well-defined universe. That's how inheritance works in OOP languages: functionality is composable.

This brings us to debugging. If Madonna is a songbird, does that mean we debug her "like a 'birgin'? If the interface is shared, do we diagnose them with the same tools? Code is fine, though: it does not repeat itself and leverages commonality wherever it exists.