A couple of weeks back I gave myself the somewhat impossible task of writing a game/rendering engine in C. The "somewhat impossible" part relates to writing the engine in C. I actually think that writing rendering engines is awesome. There is so much computer science in those things, and it is overall much fun.

So, while writing the damn thing in C, I encountered a serious problem.
Rendering engines heavily rely on object oriented paradigm in general.
Since the entire codebase was in pure C, it turns out to be quite complicated to mimic object oriented behaviour in depth. It turns out to be even harder if you try to be as efficient as possible.

However, it is possible. I pushed the project for a couple of weeks, writing everything in C. It got pretty sophisticated, the engine abstracted away GUI windows, all sorts of OpenGL related buffers, shaders, renderers and renderables. You could initialise those things as custom types (typedef abuse) and do all sorts of stuff with them.

I was actually surprised how well it all worked together. But I was getting tired of C limitations. So I quit. Eventually I've started rewriting the thing in Objective-C (I don't use C++ due to some religious reasons). However, this was me being lazy. It is completely possible to write the entire rendering engine in C.

I refactored the (then relatively tiny) codebase a couple of times, each time having a different approach on how to mimic the OO paradigm. I settled down with a relatively simple and efficient approach. I'll just copypaste my notes down below, enjoy!


File naming

Each file set (.h and .c) that mimics a certain Class is starting in uppercase letters. For example, Vec2.h and Vec2.c.

Function/Method naming

To mimic Object Oriented paradigm, each class must have (at least) 2 functions.

This was a very conscious decision. There were many (uncommited) refactorings that I've done - changing the number of required functions per class, their names and their purpose. But at the end I followed the good-ole KISS (Keep It Simple, Stupid) principle and settled down with this:

1) ClassNameInit

Initializes the struct at a given adress (pass by reference). Doesn't return anything.

void Vec2Init(Vec2* vec, float x, float y)
{
    vec->x = x;
    vec->y = y;
}

This should (usually) be the only way to initialize an class member.

Note how you have to allocate memory for the class instance in the main program. This was also a very conscious decision. By pushing memory management one layer "above", we eradicate complex memory leaks.

This also significantly increases speed as it reduces the number of malloc() and free() calls (and therefore reduces heap fragmentation as well) as you can initialize an entire array of structs in the main program with a single malloc().

On the other hand, why don't we return by value? TLDR;performance. If we were to return the struct by value instead of modifying the existing struct, it would be approx. 4 times slower than modifying the reference. Also it would be dangerous for large structs. Usually it really doesn't matter. But this is a rendering engine. Theres a lot of batch processing here.

The only downside of not returning by value is that we can't do this:
Vec2 vec = Vec2Init(0.0f, 0.0f); but we instead have to split it into this:
Vec2 vec; Vec2Init(&vec, 0.0f, 0.0f);. If inline initializing is really necessary, we can still use a compound literal when we declare/init the variable:
Vec2 vec = {0.0f, 0.0f};, or even when we are passing a pointer to an inline initialized variable: some_function( &(Vec2){0.0f, 0.0f} );.

2) ClassNameDestruct

Do stuff and prepare the struct to be destroyed. Usually called in the main program before free().

void IndexBufferDestruct(IndexBuffer* buf)
{
    SomeNestedClassDelete(&buf->some_nested_object);
    glDeleteBuffers(1, &buf->buffer_id);
}

If the object has nested other objects, their Destruct methods should be called here.

As you can see, IndexBufferDestruct takes care of the OpenGL buffer that
IndexBuffer is indirectly storing.


There you go, thats it. Although it seems very simple and concise, it helped me a bunch. Hope it helps someone else, too!