Created
August 5, 2021 21:04
-
-
Save hbobenicio/240a64f6c7c4726fc28741e68aa56745 to your computer and use it in GitHub Desktop.
Dynamic dispatch (vtable) example with vtable value types
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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).