Skip to content

Instantly share code, notes, and snippets.

@ohazi
Created July 11, 2021 07:02
Show Gist options
  • Save ohazi/b364ff43f8f258330e4663f607cd8d1e to your computer and use it in GitHub Desktop.
Save ohazi/b364ff43f8f258330e4663f607cd8d1e to your computer and use it in GitHub Desktop.
"Object Oriented Programming" in C
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* This is an example of an object system for C that supports single
* inheritance.
*
* To define a class, we define two structs:
* ClassName_Data
* ClassName_vtable
*
* `ClassName_Data` contains the fields that a `ClassName` instance (i.e. an
* object of type `ClassName`) is expected to have:
* typedef struct {
* uint32_t field1;
* char * field2;
* bool field3;
* } ClassName_Data;
*
* `ClassName_vtable` is a "virtual function dispatch table," and contains a
* bunch of function pointers. The first parameter of each of these functions
* should be a pointer to a `ClassName` struct named `self`. These functions
* are the class's virtual methods. A virtual method is a method that a
* subclass can override.
* typedef struct {
* void (*virtual_method1)(ClassName *self);
* void (*virtual_method2)(ClassName *self, int16_t param2);
* uint8_t (*virtual_method3)(ClassName *self, bool param2);
* } ClassName_vtable;
*
* Not all methods need to be virtual methods. To define a non-virtual method,
* create a free function with the same `self` parameter, and then just leave
* it out of the vtable. We can then call this method like so:
* // define a non-virtual method
* void non_virtual_method(ClassName *self, ...) { ... }
*
* // Create a `ClassName` object and call the non-virtual method
* ClassName *obj = new_ClassName(...);
* non_virtual_method(obj, ...);
*
* The two `ClassName_*` structs are glued together like so to create the
* in-memory representation of a `ClassName` object:
* typedef struct ClassName_s {
* const ClassName_vtable *vptr;
* ClassName_Data data;
* } ClassName;
*
* We can access the object's fields like so:
* ClassName *obj = new_ClassName(...);
* obj->data.field1 = ...;
* obj->data.field2 = ...;
* obj->data.field3 = ...;
*
* We can call an object's virtual methods like so:
* obj->vptr->virtual_method1(obj);
* obj->vptr->virtual_method2(obj, -5);
* uint8_t result = obj->vptr->virtual_method3(obj, false);
*
* The `ClassName` fields are stored directly inside the object as part of the
* `data` struct member. In contrast, the vtable is *not* stored in the object.
* An object only contains a pointer to its vtable (vptr).
*
* This layer of indirection allows us to define a `Subclass` that extends
* `ClassName` by adding new fields, defining new methods, and replacing old
* methods, while still remaining compatible with the expected memory layout of
* a `ClassName` object. We'll see how this works later, but first we need to
* define a `Subclass`.
*
*
* To define `Subclass` as a child of `ClassName`, we do the following:
*
* Define the two `Subclass_*` structs and glue them together, as before:
* typedef struct {
* ClassName_Data super; // _Data struct of superclass must go first
* int16_t field4;
* double field5;
* } Subclass_Data;
*
* typedef struct {
* // virtual methods of superclass must go first.
* // These should be copied directly from `ClassName_vtable`. The
* // names and parameters should be identical, except for virtual
* // methods that we intend to override in `Subclass`.
* // To override a method, change the type of the `self` parameter
* // from `ClassName *` to `Subclass *`. In this example, `Subclass`
* // only overrides `virtual_method2`, and leaves methods 1 and 3
* // alone.
* void (*virtual_method1)(ClassName *self);
* void (*virtual_method2)(Subclass *self, int16_t param2); // override
* uint8_t (*virtual_method3)(ClassName *self, bool param2);
*
* // New virtual methods that will be defined in `Subclass` go next
* void (*virtual_method4)(Subclass *self);
* void (*virtual_method5)(Subclass *self, double param2);
* } Subclass_vtable;
*
* typedef struct Subclass_s {
* const Subclass_vtable *vptr;
* Subclass_Data data;
* } Subclass;
*
* We can access `Subclass`'s fields just as before:
* Subclass *sub = new_Subclass(...);
* sub->data.field4 = ...;
* sub->data.field5 = ...;
*
* The parent class's fields can be accessed through the `super` field:
* sub->data.super.field1 = ...;
* sub->data.super.field2 = ...;
* sub->data.super.field3 = ...;
*
* Virtual methods can be called just as before:
* sub->vptr->virtual_method1(sub);
* sub->vptr->virtual_method2(sub, -5);
* uint8_t result = sub->vptr->virtual_method3(sub, false);
*
* sub->vptr->virtual_method4(sub);
* sub->vptr->virtual_method5(sub, 3.14159);
*
* We can define a free function to act as a non-virtual method that only
* applies to `Subclass`, just as before:
* // define a non-virtual method
* void non_virtual_subclass_method(Subclass *self, ...) { ... }
*
* // call the non-virtual method
* non_virtual_subclass_method(sub, ...);
*
* Finally, we can cast an object of type `Subclass` into an object of type
* `ClassName` so that we can interact with a variety of specific subclasses
* using the generic interface defined in the parent class.
* Subclass *s = new_Subclass(...);
* SubclassA *sa = new_SubclassA(...);
* SubclassB *sb = new_SubclassB(...);
* ClassName *cn = new_ClassName(...);
*
* ClassName * everything[4];
* everything[0] = (ClassName *)s;
* everything[1] = (ClassName *)sa;
* everything[2] = (ClassName *)sb;
* everything[3] = (ClassName *)cn;
*
* for (size_t i = 0; i < 4; i++) {
* everything[i]->vptr->virtual_method1(everything[i]);
* everything[i]->data.field3 = true;
* }
*
* Why does this work?
*
* This is the memory layout of a `ClassName` object and its vtable:
* [ * vptr ] ----> [ * virtual_method1 ] ----> void ClassName_virtual_method1(ClassName *self);
* [ data.field1 ] [ * virtual_method2 ] ----> void ClassName_virtual_method2(ClassName *self, int16_t param2);
* [ data.field2 ] [ * virtual_method3 ] ----> uint8_t ClassName_virtual_method3(ClassName *self, bool param2);
* [ data.field3 ]
*
* This is the memory layout of a `Subclass` object and its vtable:
* [ * vptr ] ----> [ * virtual_method1 ] ----> void ClassName_virtual_method1(ClassName *self);
* [ data.super.field1 ] [ * virtual_method2 ] ----> void Subclass_virtual_method2(Subclass *self, int16_t param2);
* [ data.super.field2 ] [ * virtual_method3 ] ----> uint8_t ClassName_virtual_method3(ClassName *self, bool param2);
* [ data.super.field3 ] [ * virtual_method4 ] ----> void Subclass_virtual_method4(Subclass *self);
* [ data.field4 ] [ * virtual_method5 ] ----> void Subclass_virtual_method5(Subclass *self, double param2);
* [ data.field5 ]
*
* When we cast a `Subclass` object to a `ClassName`, all of the values in
* memory remain the same, they are simply reinterpreted as follows:
* [ * vptr ] ----> [ * virtual_method1 ] ----> void ClassName_virtual_method1(ClassName *self);
* [ data.field1 ] [ * virtual_method2 ] ----> void Subclass_virtual_method2(ClassName *self, int16_t param2);
* [ data.field2 ] [ * virtual_method3 ] ----> uint8_t ClassName_virtual_method3(ClassName *self, bool param2);
* [ data.field3 ] [ ????????????????? ] ----> ?????????????????
* [ ????????????????? ] [ ????????????????? ] ----> ?????????????????
* [ ????????????????? ]
*
* Notice that vptr and the three `ClassName` fields are located at the correct
* offsets in memory. The three virtual method pointers in the vtable are also
* at the correct offsets.
*
* The only subtle difference between this layout and the previous one is that
* we now believe that the `self` parameter of `Subclass_virtual_method2` is a
* pointer to a `ClassName` rather than a pointer to a `Subclass`.
*
* This is fine though, because all pointers are the same size. So when we call
* `virtual_method2`, the `Subclass_virtual_method2` function will still be
* invoked correctly. This function will then *re*-reinterpret the `self`
* argument as a pointer to a `Subclass`, since that's how the function was
* defined. This essentially unmasks the parts of the `Subclass` object that
* were hidden by question marks when we were treating it as a `ClassName`.
*
* Magic!
*
*/
/* forward declarations */
struct BaseAnimal_s;
typedef struct BaseAnimal_s BaseAnimal;
/* This struct holds the fields of the `BaseAnimal` class. */
typedef struct {
uint8_t num_legs;
uint8_t num_eyes;
bool has_tail;
bool eyes_closed;
} BaseAnimal_Data;
/* This is the "virtual function dispatch table" (vtable) definition for the
* `BaseAnimal` class. A vtable is a list of function pointers to to a class's
* virtual methods. A method is "virtual" if a subclass can override it.
*/
typedef struct {
void (* const delete)(BaseAnimal *self);
void (* const describe_animal)(BaseAnimal *self);
void (* const go_to_sleep)(BaseAnimal *self);
void (* const wake_up)(BaseAnimal *self);
} BaseAnimal_vtable;
/* This is the in-memory representation of a `BaseAnimal` object. */
typedef struct BaseAnimal_s {
const BaseAnimal_vtable *vptr;
BaseAnimal_Data data;
} BaseAnimal;
/* Here we define a few example functions for `BaseAnimal` that will become
* virtual methods once we add them to the vtable.
*/
void BaseAnimal_describe_animal(BaseAnimal *self) {
char *tail = self->data.has_tail ? "a" : "no";
char *eyes = self->data.eyes_closed ? "closed" : "open";
printf("BaseAnimal with %u legs, %u eyes (%s), and %s tail.\n",
self->data.num_legs, self->data.num_eyes, eyes, tail);
}
void BaseAnimal_go_to_sleep(BaseAnimal *self) {
self->data.eyes_closed = true;
printf("BaseAnimal went to sleep.\n");
}
void BaseAnimal_wake_up(BaseAnimal *self) {
self->data.eyes_closed = false;
printf("BaseAnimal woke up.\n");
}
void BaseAnimal_delete(BaseAnimal *self) {
/* BaseAnimal doesn't allocate any memory, so there's nothing to free here,
* but we still want `delete` to be a virtual method so the correct
* destructor will be called if a subclass that *does* allocate happens to
* be destroyed through a BaseAnimal pointer.
*/
printf("BaseAnimal deleted!\n");
}
/* This is the actual vtable for every BaseAnimal object.
*
* You only need one global vtable for each class definition. A vtable is
* immutable, so the fact that it's global isn't really cause for concern.
*/
const BaseAnimal_vtable _BaseAnimal_vtable = {
.delete = BaseAnimal_delete,
.describe_animal = BaseAnimal_describe_animal,
.go_to_sleep = BaseAnimal_go_to_sleep,
.wake_up = BaseAnimal_wake_up,
};
/* BaseAnimal constructor */
BaseAnimal * new_BaseAnimal(uint8_t num_legs, uint8_t num_eyes, bool has_tail) {
BaseAnimal *p_new = malloc(sizeof(BaseAnimal));
if (!p_new) {
return NULL;
}
*p_new = (BaseAnimal) {
.vptr = &_BaseAnimal_vtable,
.data = {
.num_legs = num_legs,
.num_eyes = num_eyes,
.has_tail = has_tail,
.eyes_closed = false,
},
};
printf("BaseAnimal created!\n");
return p_new;
}
/* This object system only supports single inheritance.
*
* If you want to extend a base class, the `BaseClass_Data` struct needs to be
* the first field of the `Subclass_Data` struct. This allows you to cast a
* pointer to a `Subclass` object into a pointer to a `BaseClass` object, and
* have everything work as expected, because the memory layout of the truncated
* Subclass will be identical to the memory layout of the BaseClass.
*
* The advantage of embedding the parent `*_Data` struct directly like this is
* that we don't have to copy all of the individual fields from the parent, and
* if we change them, the changes will automatically apply. The disadvantage is
* that when using a subclass, you need to know which fields are fields defined
* in the subclass, and which are defined in the parent:
*
* Dog *d = ...;
* d->data.how_hungry = ...;
* d->data.super.has_tail;
*
* There might be a way to get around this with union trickery, but whatever.
*
* A similar requirement applies to `*_vtable` structs, but instead of
* embedding the parent's vtable, here we'll take the opposite approach of
* copying the method list directly, changing the type of the first argument
* depending on whether we intend to override that method.
*/
/* forward declarations */
struct Dog_s;
typedef struct Dog_s Dog;
typedef struct {
BaseAnimal_Data super;
char *coat_color;
int32_t how_hungry;
} Dog_Data;
typedef struct {
/* BaseAnimal's virtual methods */
void (* const delete)(Dog *self); /* override */
void (* const describe_animal)(Dog *self); /* override */
void (* const go_to_sleep)(BaseAnimal *self);
void (* const wake_up)(BaseAnimal *self);
/* Dog's virtual methods */
void (* const feed)(Dog *self, uint32_t how_much_food);
void (* const boop)(Dog *self);
} Dog_vtable;
typedef struct Dog_s {
const Dog_vtable *vptr;
Dog_Data data;
} Dog;
void Dog_delete(Dog *self) {
free(self->data.coat_color);
printf("Dog deleted!\n");
/* This is how we call `super().delete()`
* In this case, we happen to know that the `BaseAnimal` destructor doesn't
* actually do anything, but in reality we might not know that.
*/
_BaseAnimal_vtable.delete((BaseAnimal *)self);
}
void Dog_describe_animal(Dog *self) {
char *tail = self->data.super.has_tail ? "a" : "no";
char *eyes = self->data.super.eyes_closed ? "closed" : "open";
printf("A %s Dog with %u legs, %u eyes (%s), and %s tail.\n",
self->data.coat_color,
self->data.super.num_legs, self->data.super.num_eyes, eyes, tail);
}
void Dog_feed(Dog *self, uint32_t how_much_food) {
if (self->data.how_hungry > 0) {
printf("The %s Dog is hungry, and accepts your %u food. ",
self->data.coat_color, how_much_food);
self->data.how_hungry -= how_much_food;
if (self->data.how_hungry > 0) {
printf("Still hungry!\n");
} else {
printf("So full!\n");
}
} else {
printf("The %s Dog is not hungry, and ignores your %u food.\n",
self->data.coat_color, how_much_food);
}
}
void Dog_boop(Dog *self) {
char *tail = self->data.super.has_tail ? "tail" : "butt";
printf("You booped the %s Dog. Dog wags its %s.\n",
self->data.coat_color, tail);
}
const Dog_vtable _Dog_vtable = {
/* BaseAnimal's virtual methods */
.delete = Dog_delete, /* override */
.describe_animal = Dog_describe_animal, /* override */
.go_to_sleep = BaseAnimal_go_to_sleep,
.wake_up = BaseAnimal_wake_up,
/* Dog's virtual methods */
.feed = Dog_feed,
.boop = Dog_boop,
};
Dog * new_Dog(uint8_t num_legs, uint8_t num_eyes, bool has_tail, char *color) {
char *coat_color = malloc(strlen(color) + 1);
if (!coat_color) {
return NULL;
}
strcpy(coat_color, color);
Dog *p_new = malloc(sizeof(Dog));
if (!p_new) {
free(coat_color);
return NULL;
}
*p_new = (Dog) {
.vptr = &_Dog_vtable,
.data = {
{
.num_legs = num_legs,
.num_eyes = num_eyes,
.has_tail = has_tail,
.eyes_closed = false,
},
.coat_color = coat_color,
.how_hungry = 100,
},
};
printf("Dog created!\n");
return p_new;
}
int main(int argc, char *argv[]) {
/* Create a generic animal with 4 legs, 2 eyes, and a tail */
BaseAnimal *ba = new_BaseAnimal(4, 2, true);
ba->vptr->describe_animal(ba);
ba->vptr->go_to_sleep(ba);
ba->vptr->describe_animal(ba);
ba->vptr->wake_up(ba);
/* Call destructor and release memory */
ba->vptr->delete(ba);
free(ba);
/* Create a corgi */
Dog *corgi = new_Dog(4, 2, false, "tricolor");
corgi->vptr->describe_animal(corgi);
corgi->vptr->feed(corgi, 80);
corgi->vptr->feed(corgi, 30);
corgi->vptr->feed(corgi, 10);
corgi->vptr->boop(corgi);
corgi->vptr->go_to_sleep((BaseAnimal *)corgi);
corgi->vptr->wake_up((BaseAnimal *)corgi);
/* pretend the corgi is a generic animal */
BaseAnimal *incorgnito = (BaseAnimal *)corgi;
/* We didn't override these methods in `Dog`, so the implementations in
* `BaseAnimal` will be called. */
incorgnito->vptr->go_to_sleep(incorgnito);
incorgnito->vptr->wake_up(incorgnito);
/* This fails to compile.
* We don't know whether this particular `BaseAnimal` is boopable! */
/* incorgnito->vptr->boop(incorgnito); */
/* Unmask the impostor.
* We *did* override this method in `Dog`, so the `Dog` implementation will
* be called. */
incorgnito->vptr->describe_animal(incorgnito);
/* Note that the Dog destructor is called as expected, even though we're
* calling it through the type cast object pointer.
*
* The address passed to free() is the same regardless of how the object
* has been type cast, so this works correctly too.
*/
incorgnito->vptr->delete(incorgnito);
free(incorgnito);
}
@ohazi
Copy link
Author

ohazi commented Jul 14, 2021

ohazi@woodstock:~/source/test$ make oop
cc     oop.c   -o oop
ohazi@woodstock:~/source/test$ ./oop 
BaseAnimal created!
BaseAnimal with 4 legs, 2 eyes (open), and a tail.
BaseAnimal went to sleep.
BaseAnimal with 4 legs, 2 eyes (closed), and a tail.
BaseAnimal woke up.
BaseAnimal deleted!
Dog created!
A tricolor Dog with 4 legs, 2 eyes (open), and no tail.
The tricolor Dog is hungry, and accepts your 80 food. Still hungry!
The tricolor Dog is hungry, and accepts your 30 food. So full!
The tricolor Dog is not hungry, and ignores your 10 food.
You booped the tricolor Dog. Dog wags its butt.
BaseAnimal went to sleep.
BaseAnimal woke up.
BaseAnimal went to sleep.
BaseAnimal woke up.
A tricolor Dog with 4 legs, 2 eyes (open), and no tail.
Dog deleted!
BaseAnimal deleted!
ohazi@woodstock:~/source/test$ 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment