Skip to content

Instantly share code, notes, and snippets.

@hbobenicio
Created August 5, 2021 21:04
Show Gist options
  • Save hbobenicio/240a64f6c7c4726fc28741e68aa56745 to your computer and use it in GitHub Desktop.
Save hbobenicio/240a64f6c7c4726fc28741e68aa56745 to your computer and use it in GitHub Desktop.
Dynamic dispatch (vtable) example with vtable value types
/**
* Dynamic Dispatch example in C.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>
typedef struct AllocatorInterface AllocatorInterface;
typedef struct Allocator Allocator;
/**
* 1. Create the Interface type. They need to receive a pointer to the Base type as a parameter to allow down/up casting.
*/
struct AllocatorInterface {
void* (*malloc)(Allocator* allocator, size_t size);
void (*free)(Allocator* allocator, void* ptr);
};
/**
* 2. Create the Base type that has just a pointer to the common interface.
*/
struct Allocator {
AllocatorInterface vtable;
};
/**
* 3. Implement the Base type dispatch.
*/
void* allocator_malloc(Allocator* allocator, size_t size)
{
return allocator->vtable.malloc(allocator, size);
}
/**
* 3. Implement the Base type dispatch.
*/
void allocator_free(Allocator* allocator, void* ptr)
{
allocator->vtable.free(allocator, ptr);
}
/**
* 4. Create the specialization type (any type that implements the common interface).
* NOTE: the base instance MUST be the first field of the struct. That will allow the up/down pointer cast to behave correctly.
*/
typedef struct {
// This need to be the first field
Allocator base;
} LibcAllocator;
/**
* 5. Implement the specialized behaviour.
*/
void* libc_allocator_malloc(Allocator* allocator, size_t size)
{
(void) allocator;
return malloc(size);
}
/**
* 5. Implement the specialized behaviour.
*/
void libc_allocator_free(Allocator* allocator, void* ptr)
{
(void) allocator;
free(ptr);
}
LibcAllocator libc_allocator()
{
return (LibcAllocator) {
.base = {
.vtable = {
.malloc = libc_allocator_malloc,
.free = libc_allocator_free,
},
},
};
}
/**
* 4. Create the specialization type (any type that implements the common interface).
* NOTE: the base instance MUST be the first field of the struct. That will allow the up/down pointer cast to behave correctly.
*/
typedef struct {
Allocator base;
Allocator* backing_allocator;
const char* tag;
} DebugAllocator;
/**
* 5. Implement the specialized behaviour.
*/
void* debug_allocator_malloc(Allocator* allocator, size_t size)
{
// "down cast"
DebugAllocator* debug_allocator = (void*) allocator;
void *data = allocator_malloc(debug_allocator->backing_allocator, size);
if (data == NULL) {
fprintf(stderr, "[DebugAllocator] [%s] failed to allocate %zu bytes: out of memory!\n", debug_allocator->tag, size);
return data;
}
fprintf(stderr, "[DebugAllocator] [%s] %zu bytes allocated at address %p\n", debug_allocator->tag, size, data);
return data;
}
/**
* 5. Implement the specialized behaviour.
*/
void debug_allocator_free(Allocator* allocator, void* ptr)
{
DebugAllocator* debug_allocator = (void*) allocator;
fprintf(stderr, "[DebugAllocator] [%s] freeing pointer at address %p\n", debug_allocator->tag, ptr);
allocator_free(debug_allocator->backing_allocator, ptr);
}
DebugAllocator debug_allocator(Allocator* backing_allocator, const char* tag)
{
return (DebugAllocator) {
.base = {
.vtable = {
.malloc = debug_allocator_malloc,
.free = debug_allocator_free,
},
},
.backing_allocator = backing_allocator,
.tag = tag,
};
}
void test1(Allocator* allocator)
{
int* num = allocator_malloc(allocator, sizeof(int));
*num = 42;
fprintf(stderr, "num = %d\n", *num);
allocator_free(allocator, num);
}
void test2()
{
/**
* 7. Create the specialized instance. It must not be local like these. It can be created whenever you like.
*/
LibcAllocator c_allocator = libc_allocator();
DebugAllocator foo = debug_allocator((Allocator*) &c_allocator, "TEST 2");
test1((Allocator*) &foo);
}
int main()
{
LibcAllocator c_allocator = libc_allocator();
test1((Allocator*) &c_allocator);
test2();
return 0;
}
@hbobenicio
Copy link
Author

Initialization ergonomics are better at the cost of some more bytes for the structs (that will be created and passed down up and down the stack frames). Not ideal on environments with stack size limitations. Not ideal if the Interface type is large enough. Not sure how well compilers optimize those copies (structs with only some function pointers).

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