First of all, what is an interface? An interface can be thought of as a contract between the user of a software component and the provider of that component. It defines which parameters it needs in order to provide a certain functionality. The aim is to hide as much implementation details and thus maintain independence from a specific implementation. In Object Oriented programming, interfaces are well known and Object Orientated languages usually offer some kind of concept to enforce the usage of an interface.
C programmers often do not think in objects because the concept of objects is not embedded in the language itself. Additionally, many C developers come from low level platforms or even bare metal and also often work on smaller code bases where software architecture is sometimes not a top priority. But when it comes to keeping the software flexible and having clear cuts in the architecture, interfaces are an unavoidable necessity.
So, how is it done in C?
Since an interface is just a concept C, we can easily replicate it in C and benefit from it’s advantages. An interface in C is basically just a bunch of function pointer packed into a struct, That’s it.
In the .h
file you would typically have your interface definition:
The pointer itf
that is passed to the functions is the object the function is working on (basically this
in Java or self
in Python). The destroy function acts as the destructor and makes sure the object is properly deallocated.
An implementation of this interface includes that .h
file and provides a create function (the constructor in OO terms), which returns a pointer to a struct itf
.
The .c
file contains the implementation of the public functions, private functions and other private data.
A user of the interface would look as follows:
Lets look at a more concrete example
Imagine you want to create a data logger application that waits for a trigger signal, then reads a sensor and stores it in storage device.
Depending on the device type (A or B), the application has to support different options:
Type_A
consists of a timer trigger, reads a pressure sensor and writes to an eeprom.
Type_B
consists of a event trigger, reads a temperature sensor and writes to a file on the file system.
First, let’s define the interfaces for the trigger, storage and sensor:
And then we define an interface for the datalogger itself.
Next, let’s implement the datalogger:
The private data structure has the suffix _priv
which contains a struct datalogger_itf
. The datalogger_itf
is populated with the interface functions and returned when creating the object. container_of
is a macro to get the private structure from the public structure (see here how it works).
And finally the main:
This provides the advantage that the data logger doesn’t have any detailed knowledge about trigger, sensor and storage and just knows how to use these components. Each component can be changed without any changes required to the datalogger code.
Improve your tests
Furthermore, when testing, it is very convenient to replace a component with a mocked component. For example, if I want to simulate a failing sensor, I can implement a failing sensor as shown in the example below:
If I need to simulate a failing sensor in one test case but in the next test case a proper behaviour, I am able to separate this behaviour in two separate implementations. If no interfaces were used I have to implement a mock function for the e.g. temperature_sensor_read
function which mocks both behaviours in the same function. Whether it has to fail or not has to be communicated to the function via global variable.
It may not look terribly complicated in the example above but I am sure you can imagine that it can get quite hairy if several sensor behaviours have to be simulated in a single mock function.
Let’s wrap it up
We have learnt that it makes sense to use interfaces when implementing in C and learnt a way to define and implement such an interface. Furthermore, we have seen that interfaces increase the flexibility of the software and we could show advantages when writing unit tests.