Advanced C Interfaces
Thu, 28 Jun 2018 21:41:01 -0000
In this article, we will provide advanced techinques to approach and master complexity when developing massive C applications.
Update: Added Link to code repository.
Update:
Update: Added footnotes.
Update: Fixed formatting.
Update: Fixed formatting.

Introduction

The source code accompanying this article can be found at https://github.com/vidarr/ringbuffer.

Modules are the key approach to mastering complexity [1] . Therefore, they should target maximum indenpendence. A module should be as independent as possible from other code. This will not only severly reduce complexity, but also allow you to create effective unit tests. The key in effectively moduralizing is designing good interfaces. Good interfaces should aim at The remainder of this article will show some techniques on how to design for proper modularity in C by developing a ringbuffer FIFO and improving upon it step by step. You will encounter certain practices that violate what you will have read in some introductory book / article about C. Just bear in mind that an introductory book is written for people totally unfamiliar with C. It totally makes sense to give some rule of thumbs for the beginner to give orientation. However, as in most scopes, rules of thumbs hold most of the time, but at times it makes sense to break them.

Basic design

As a small demonstration project, we want to implement a FIFO [2] as a ringbuffer. A ringbuffer is basically a FIFO, thus you can add items to it, and pop items back out of it. Key feature is that it will start overwriting the oldest items in the buffer. We will use it as a FIFO, i.e. when popping, the oldest element will be returned from the buffer (but you could use this structure as well as a LIFO 3 , popping out the newest element - however, I dont see an application for a stack overwriting its oldest elements now). What is the minimum a user requires to use this data structure? Now, wouldn't it be nice if we could provide different implementations of a ringbuffer in a way to prevent users from adapting their code? Maybe even allow for some kind of factory, that returnes different kinds of ringbuffers, just as you know from factories in C++ or Java? In other words, allow for some kind of inheritance? We will therefore tie the functions as function pointers to our Ringbuffer. Actually, the Ringbuffer itself needs to only be a collection of function pointers (from the users' point of view, and that whats matters for now):

typedef struct Ringbuffer {
size_t (*capacity) (struct Ringbuffer* self);
bool (*add) (struct Ringbuffer* self, void* item);
void* (*pop) (struct Ringbuffer* self);
struct Ringbuffer* (*free) (struct Ringbuffer* self);

} Ringbuffer;



(You might wonder why the label of the struct bears the same name as the type, but that's ok - C got different namespaces for typedefs and struct labels - and since both refer to the same item, don't bother coming up with a different name) Along with our constructor function to create such a Ringbuffer, this is all a user requires to use our ringbuffer. This is our interface. In C, a module consists of a source file and a header. Contrary to what you might have read in some beginners' books, putting all declarations into the header is not a good practice at all, because the header is the interface of the module, the header is what the users will see of your module. So in order to keep your interface simple and small, put as few things in the header as possible. This is the first beginners' rule of thumb we are going to break. So, as we got our minimal interface already, lets put this together in a header:

#ifndef __RINGBUFFER_H__
#define __RINGBUFFER_H__

typedef struct Ringbuffer {
size_t (*capacity) (struct Ringbuffer* self);
bool (*add) (struct Ringbuffer* self, void* item);
void* (*pop) (struct Ringbuffer* self);
struct Ringbuffer* (*free) (struct Ringbuffer* self);

} Ringbuffer;

Ringbuffer* ringbuffer_create(
size_t capacity,
void (*free_item)(void* item, void* additional_arg),
void* free_item_additional_arg);

#endif


That's it - that's our header. And surprisingly, in the course of this article, it won't change much, we will even provide 2 implementations in parallel, it will only grow with 2 additional functions. The trick is, to achieve this...

A first implementation

Ok, so we got our interface, now we need to implement the gory internals. Whatever might come, we need to implement our functions defined in the header - which is currently the constructor method only. Thus, start of copying its prototype over to an empty source file and include our glorious header:

#include "../include/ringbuffer.h"

/******************************************************************************
PUBLIC FUNCTIONS
******************************************************************************/

Ringbuffer* ringbuffer_create(
size_t capacity,
void (*free_item)(void* item, void* additional_arg),
void* free_item_additional_arg) {

if(0 >= capacity) {
goto error;
}

error:

return 0;
}


And because we want to implement rock-solid code, write a neat unit test for checking that the function does what it is supposed to:

#include "../src/ringbuffer.c"
#include
#include

/*----------------------------------------------------------------------------*/

void test_ringbuffer_create() {

Ringbuffer* buffer = 0;

assert(0 == ringbuffer_create(0, 0, 0));

buffer = ringbuffer_create(1, 0, 0);
assert(buffer);
assert(capacity_func == buffer->capacity);
assert(add_func == buffer->add);
assert(pop_func == buffer->pop);
assert(free_func == buffer->free);

buffer = buffer->free(buffer);

fprintf(stdout, "ringbuffer_create OK\n");

}

/*----------------------------------------------------------------------------*/

int main(int argc, char** argv) {

test_ringbuffer_create();

}


And- you got your first program that compiles and will execute. But of course, the unit test will fail since our constructor does not construct anything. Lets make the test pass by extending your source code to:

static size_t capacity_func(Ringbuffer* self);
static bool add_func(Ringbuffer* self, void* item);
static void* pop_func(Ringbuffer* self);
static Ringbuffer* free_func(Ringbuffer* self);

Ringbuffer* ringbuffer_create(
size_t capacity,
void (*free_item)(void* item, void* additional_arg),
void* free_item_additional_arg) {

assert(0 < capacity);

if(0 >= capacity) {
goto error;
}

Ringbuffer* buffer = calloc(1, sizeof(Ringbuffer));

*buffer = (Ringbuffer) {
.capacity = capacity_func,
.add = add_func,
.pop = pop_func,
.free = free_func

};

return buffer;

error:

return 0;
}

static size_t capacity_func(Ringbuffer* self) {
return 0;
}

static bool add_func(Ringbuffer* self, void* item) {
return false;
}

static void* pop_func(Ringbuffer* self) {
return 0;
}

static Ringbuffer* free_func(Ringbuffer* self) {
return self;
}


Now, the test passes as you actually create a Ringbuffer struct. But, it's entirely usesless as it only provides dummy functions. So, carry on by first thinking about the add() and pop() methods, then implement unit tests to check for their proper functionality:

...

void test_add() {

int a = 1;
int b = 2;
int c = 3;

Ringbuffer* buffer = 0;

buffer = ringbuffer_create(1, 0, 0);
assert(buffer->add(buffer, &a));
assert(buffer->add(buffer, &b));
assert(buffer->add(buffer, &c));
buffer = buffer->free(buffer);

buffer = ringbuffer_create(2, 0, 0);
assert(buffer->add(buffer, &a));
assert(buffer->add(buffer, &b));
assert(buffer->add(buffer, &c));
buffer = buffer->free(buffer);

buffer = ringbuffer_create(20, 0, 0);
assert(buffer->add(buffer, &a));
assert(buffer->add(buffer, &b));
assert(buffer->add(buffer, &c));
buffer = buffer->free(buffer);

fprintf(stdout, "add() OK\n");

}

/*----------------------------------------------------------------------------*/

void test_pop() {

int a = 1;
int b = 2;
int c = 3;

Ringbuffer* buffer = 0;

buffer = ringbuffer_create(1, 0, 0);

assert(buffer->add(buffer, &a));
assert(&a == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));

assert(buffer->add(buffer, &a));
assert(buffer->add(buffer, &b));
assert(&b == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));

assert(buffer->add(buffer, &a));
assert(buffer->add(buffer, &b));
assert(buffer->add(buffer, &c));
assert(&c == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));

buffer = buffer->free(buffer);


buffer = ringbuffer_create(2, 0, 0);

assert(buffer->add(buffer, &a));
assert(&a == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));

assert(buffer->add(buffer, &a));
assert(buffer->add(buffer, &b));
assert(&a == buffer->pop(buffer));
assert(&b == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));

assert(buffer->add(buffer, &a));
assert(buffer->add(buffer, &b));
assert(buffer->add(buffer, &c));
assert(&a == buffer->pop(buffer));
assert(&b == buffer->pop(buffer));
assert(&c == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));

assert(buffer->add(buffer, &a));
assert(buffer->add(buffer, &b));
assert(&a == buffer->pop(buffer));
assert(buffer->add(buffer, &c));
assert(&b == buffer->pop(buffer));
assert(buffer->add(buffer, &a));
assert(&c == buffer->pop(buffer));
assert(&a == buffer->pop(buffer));
assert(0 == buffer->pop(buffer));

buffer = buffer->free(buffer);

fprintf(stdout, "pop() OK\n");

}

/*----------------------------------------------------------------------------*/

int main(int argc, char** argv) {

test_ringbuffer_create();
test_add();
test_pop();

}


Again, the very first test will fail. That's actually good, because it means that our tests are probably dumb, but actually can fail and therefore check at least a little bit ;) However, now we need to implement the add() and pop() functions. The easiest implementation of a ringbuffer is a linked list, where the last entry links back to the beginning (creating a ring - hence "ring buffer' ). Thus we require some helper data type to store 1 item and a pointer to the following entry:

typedef struct Entry {
void* datum;
struct Entry* next;
}


Does the user require this struct? No, its an implementation detail that better be hidden from the user - we could as well use an indexed array or whatever... Thus lets store it in our source file. Now, we need to store this linked list somewhere. We do not want to make one global linked list for obvious reasons, Naturally, we should attach it to our Ringbuffer. So let's enhance it's definition - but: Does the user require direct access to the internal data representation of a ringbuffer? Of course, this would be violating all of our principles. Thus, better not alter the definition of Ringbuffer. But - how to attach data to a struct without altering it's definition? Gracefully, the C standard provides a neat assertion:
A pointer to a struct is the same as a pointer to it's first element [4] .
This means, that

Ringbuffer* buffer = ringbuffer_create(1, 0, 0);

assert(buffer == (Ringbuffer*) & buffer->capacity);


For example. We can exploit that by extending a structure without having to alter it's definition:

typedef struct InternalRingbuffer {

Ringbuffer public;

Entry* next_entry_to_read;
Entry* next_entry_to_write;
size_t max_num_items;

void (*free_item)(void* item, void* additional_arg);
void* free_item_additional_arg;

} InternalRingbuffer;


We can safely cast a pointer to an InternalRingbufer to Ringbuffer since a pointer to InternalRingbuffer equals a pointer to InternalRingbuffer->public which incidentally is a Ringbuffer:

InternalRingbuffer internal = calloc(1, sizeof(InternalRingbuffer));
/* initialize internal */
...
Ringbuffer* ringbuffer = (Ringbuffer*) internal;
ringbuffer->add(ringbuffer, item); /* Works perfectly */


This seems to be some kind of ugly kludge, but is totally portable and standard conforming techinque. The C standard guarantees this to work! Since this is private stuff, this definition goes into the source file.

Footnotes and References

1 This approach is far more universal than just for software engineering - for analyzing complex systems, the general approach is to separate the system as simple and independent parts, and minimize the interactions between them.

2 First-in-First-Out. typical applications are queues, e.g. for communicating between threads. Actually, this very ringbuffer stems a audio streaming software project where it is used to pass PCM data between threads.

3 Last-In-First-Out. Typically called 'stack' and used for parsing all kinds of languages, in interpreters and compilers, but also within the processor to store local variables and handling function calls and returns.

4 Draft C11 Standard, ISO/IEC 9899:201x, 6.7.2.1, Semantics, paragraph 15: "A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning."