Interfaces are an extremely useful tool to develop loosely-coupled, testable software. In the embedded world it will even allow us to write firmware and run it on our development PC instead of the actual hardware, which may be scarce or not even produced yet.
Imagine we’re building an automated greenhouse and we’re writing a task to control the climate inside the greenhouse. To keep it simple, let’s say we need a temperature sensor and some kind of actuator to open and close a window. The hardware folks are still figuring out which sensor and actuator to use, and a PCB might not land on our desk for weeks or even months – and even then we might have to share it between several developers. But we don’t really care. We can start developing the application using interfaces to the hardware that abstract away the details, and then worry about implementing the actual drivers later. We can think of the interface as a contract between a module and the user of the module.
Let’s use the temperature sensor as an example. I will start by showing a solution in an object-oriented language, namely C++, and then how to do the same in C.
Interfaces in an object-oriented language (C++)
The interface to our temperature sensor is fairly simple. All we need is a way to initialize the sensor and to get the temperature in either Celsius or Fahrenheit. Our interface, or abstract class, might look like this:
class ITempSensor
{
public:
virtual void Init() = 0;
virtual float GetTempC() = 0;
virtual float GetTempF() = 0;
};
Notice that all the functions are declared pure virtual, meaning that the class is abstract. Therefore, it is not possible to create an instance of the ITempSensor class itself. When we get the actual sensor, we can write a concrete class that implements the interface. For now we’ll create a fake implementation which we can use for testing.
#include "ITempSensor.h"
class FakeTempSensor : public ITempSensor
{
public:
FakeTempSensor();
~FakeTempSensor();
/* ITempSensor */
void Init();
float GetTempC();
float GetTempF();
};
I have omitted the method definitions here for brevity, but we can simply let the getters return a fixed number or we can choose to add functions that will let us set the fake temperature.
In our application, we will have a task function (let’s call it ClimateTask()
) which takes a pointer to an ITempSensor
as a parameter. In the application code we inject the actual sensor implementation, but in our test code we can simply inject our fake implementation instead. The task function does not know, let alone care, which implementation we use. As long as it complies with the contract, i.e. the ITempSensor
interface!
#include "FakeTempSensor.h"
void ClimateTask(ITempSensor *sensor);
int main()
{
FakeTempSensor fakeSensor;
while (1)
{
ClimateTask(&fakeSensor);
}
return 0;
}
This is fairly straightforward in C++. Let’s take a look a how we might implement this in a structured programming language such as C.
Interfaces in a structured language (C)
Since there are no classes in C, we will use a struct to create multiple instances of our modules. For the interface, our struct will contain pointers to the three interface functions:
typedef struct itf_temp_sensor itf_temp_sensor_t;
struct itf_temp_sensor
{
void (*init)(itf_temp_sensor_t *this);
float (*get_temp_c)(itf_temp_sensor_t *this);
float (*get_temp_f)(itf_temp_sensor_t *this);
};
Notice that all functions take a pointer to the interface struct as the first parameter, so we know which instance to operate on. In C++ this is done for us automagically behind the scenes, but in C we have to do it ourselves. I’ve named the parameter this
in order to make it similar to C++, but you may also see it named me
, self
or handle
elsewhere.
Now, when creating a module that implements this interface, the struct of the module must have the interface struct as its first member. We’re free to add any variables or functions, that are specific to the concrete implementation, afterwards. Here I have added two floats, fake_temp_c
and fake_temp_f
, for illustration:
#include "itf_temp_sensor.h"
typedef struct fake_temp_sensor fake_temp_sensor_t;
struct fake_temp_sensor
{
itf_temp_sensor_t interface;
float fake_temp_c;
float fake_temp_f;
};
void fake_temp_sensor_create(fake_temp_sensor_t *this);
void fake_temp_sensor_init(itf_temp_sensor_t *this);
float fake_temp_sensor_get_temp_c(itf_temp_sensor_t *this);
float fake_temp_sensor_get_temp_f(itf_temp_sensor_t *this);
In addition to the three functions of the itf_temp_sensor
interface, I have also added a create()
function. The purpose of this function is to bind the function pointers of the interface
member to the concrete function implementations of the fake_temp_sensor
. Without this binding, the interface
member will contain NULL
pointers and bad things will happen. Since this function is not part of the interface, we can take a fake_temp_sensor_t
pointer instead of a itf_temp_sensor_t
pointer as the parameter.
#include "fake_temp_sensor.h"
void fake_temp_sensor_create(fake_temp_sensor_t *this)
{
this->interface.init = fake_temp_sensor_init;
this->interface.get_temp_c = fake_temp_sensor_get_temp_c;
this->interface.get_temp_f = fake_temp_sensor_get_temp_f;
}
Now, the interface functions take a pointer to itf_temp_sensor_t
, but what it we want to access the variables fake_temp_c
and fake_temp_f
that are specific to the fake_temp_sensor_t
? This is where it gets a little dirty: We must do a pointer typecast from itf_temp_sensor_t*
to fake_temp_sensor_t*
.
Let’s take fake_temp_sensor_get_temp_c()
as an example. We know that the itf_temp_sensor_t
pointer that is passed as an argument is actually a fake_temp_sensor_t
pointer. It’s just that we have to declare it as an itf_temp_sensor_t
pointer to comply with the interface. Since interface
is the first member, and thus has the same memory address as the struct it belongs to, we can simply perform a pointer typecast to be able to access the implementation-specific variables, like this:
float fake_temp_sensor_get_temp_c(itf_temp_sensor_t *this)
{
fake_temp_sensor_t *fake_this = (fake_temp_sensor_t*) this;
return fake_this->fake_temp_c;
}
Or we can use the container_of()
macro known from Linux like this:
float fake_temp_sensor_get_temp_c(itf_temp_sensor_t *this)
{
fake_temp_sensor_t *fake_this = container_of(this, fake_temp_sensor_t, interface);
return fake_this->fake_temp_c;
}
Some may like this solution more, because we’re explicitly stating that this
is actually the interface
member of the fake_temp_sensor_t
, instead of just performing some obscure typecast. Additionally, the interface
doesn’t strictly have to be the first member of the struct, and we also could create modules that implement more than just one interface.
Lastly, to use the fake temperature sensor in our application, we will inject it into the climate_task()
like we did in C++. Remember to call the create()
function first, in order to initialize the function pointers. Also, since the climate_task()
takes an itf_temp_sensor_t
pointer, an explicit typecast is good practice, although GCC will let you off with a warning if you forget.
#include "fake_temp_sensor.h"
void climate_task(itf_temp_sensor_t *sensor);
int main()
{
fake_temp_sensor_t fake_sensor;
fake_temp_sensor_create(&fake_sensor);
while (1)
{
climate_task((itf_temp_sensor_t*) &fake_sensor);
}
return 0;
}
Inside the climate_task()
we have no knowledge of the implementation details of the sensor. All we know is that we can initialize it and get the temperature – and that is all we care about in this context.
bool initialized;
float current_temp;
void climate_task(itf_temp_sensor_t *sensor)
{
if (!initialized)
{
sensor->init(sensor);
initialized = true;
}
current_temp = sensor->get_temp_c(sensor);
}
Conclusion
Although C is not an object-oriented language per se, it is possible to apply object-oriented design patterns. Here we have seen how to use interfaces and dependency injection to achieve low coupling and facilitate off-target development and testing.
Although creating interfaces in C this way may look a bit messy at first, it’s nice to know how to do it. For some projects you’re strictly required (e.g. by a client or manager) to use C, but if this is not the case, I would much prefer using C++ for this kind of design.