Initial garbage colletor implementation

This commit is contained in:
2025-08-30 21:21:02 -07:00
parent 80e3f1a916
commit d78cf29765
11 changed files with 709 additions and 214 deletions

View File

@ -9,14 +9,46 @@
/**
* Initial value of #refcount_global_allocator.
*/
static const RefcountAllocator default_allocator = {
static const RefcountAllocator DEFAULT_ALLOCATOR = {
.malloc = malloc,
.realloc = realloc,
.free = free,
.pass_data = false,
.user_data = NULL,
};
/**
* The global #RefcountAllocator used by other parts of RefCount. The default
* value just calls the C standard functions malloc, realloc, and free.
* value just calls the C standard functions malloc and free.
*/
const RefcountAllocator *refcount_global_allocator = &default_allocator;
const RefcountAllocator *refcount_global_allocator = &DEFAULT_ALLOCATOR;
/**
* HTAllocator malloc function that delegates to #refcount_malloc.
* @param size The number of bytes to allocate
* @parma alloc The #RefcountAllocator to use
* @return The result of #refcount_malloc
*/
static void *refcount_ht_malloc(size_t size, void *alloc) {
return refcount_malloc(alloc, size);
}
/**
* HTAllocator free function that delegates to #refcount_free.
* @param ptr The pointer to free
* @parma alloc The #RefcountAllocator to use
*/
static void refcount_ht_free(void *ptr, void *alloc) {
refcount_free(alloc, ptr);
}
/**
* Create a new HTAllocator that delegates to src.
* @param src The #RefcountAllocator to delegate to
* @param dest A pointer to a #HTAllocator to initialize
*/
void refcount_allocator_to_ht_allocator(const RefcountAllocator *src,
HTAllocator *dest) {
dest->malloc = refcount_ht_malloc;
dest->free = refcount_ht_free;
dest->user_data = (void *) src;
}

View File

@ -4,31 +4,26 @@
*/
#include "refcount/list.h"
#include <stdarg.h>
#include <stdbool.h>
#include <stdlib.h>
/**
* Build a #RefcountList from a number of elements.
*
* > that this will return NULL if count is 0 or if a memory allocation error
* > occurred. However, as no allocation is preformed is count is 0, these
* > errors should be mutually exclusive.
*
* Like #refcount_list_build, but lets you specify the allocator to use and
* takes a va_list instead of variable arguments.
* @param count The number of elements given
* @param ... The elements from which to build the list
* @param alloc The allocator to use
* @param args The va_list to use to get the elements
* @return The built list, or NULL if a memory allocation error occurred
*/
RefcountList *refcount_list_build(int count, ...) {
va_list args;
va_start(args, count);
RefcountList *refcount_list_build_full_va(int count,
const RefcountAllocator *alloc,
va_list args) {
RefcountList *start = NULL;
RefcountList *end;
while (count--) {
RefcountList *new_list = refcount_malloc(sizeof(RefcountList));
RefcountList *new_list = refcount_malloc(alloc, sizeof(RefcountList));
if (!new_list) {
refcount_list_free(start, NULL);
va_end(args);
return NULL;
}
new_list->data = va_arg(args, void *);
@ -43,28 +38,30 @@ RefcountList *refcount_list_build(int count, ...) {
end = new_list;
}
}
va_end(args);
return start;
}
/**
* Return a new #RefcountList that is a copy of list. This does no allocation if
* the input list is NULL.
* Same as #refcount_list_copy, but lets you specify the allocator.
* @param list The list to copy
* @param callback The function to copy each element of the list, or NULL
* @param user_data Extra data to be passed to the copy callback
* @param alloc The allocator to use
* @return The copy, or NULL if an allocation error occurred
*/
RefcountList *refcount_list_copy(RefcountList *list,
refcount_list_copy_callback_t callback) {
RefcountList *refcount_list_copy_full(const RefcountList *list,
refcount_list_copy_callback_t callback,
void *user_data,
const RefcountAllocator *alloc) {
RefcountList *start = NULL;
RefcountList *end;
while (list) {
RefcountList *new_end = refcount_malloc(sizeof(RefcountList));
RefcountList *new_end = refcount_malloc(alloc, sizeof(RefcountList));
if (!new_end) {
refcount_list_free(start, NULL);
refcount_list_free_full(start, NULL, alloc);
return NULL;
}
new_end->data = callback ? callback(list->data) : list->data;
new_end->data = callback ? callback(list->data, user_data) : list->data;
new_end->next = NULL;
if (!start) {
start = new_end;
@ -101,16 +98,18 @@ RefcountList *refcount_list_reverse(RefcountList *list) {
/**
* Free a #RefcountList.
* @param list The list to free
* @param alloc The allocator to use
* @param callback The function to call to free each member, or NULL
*/
void refcount_list_free(RefcountList *list,
refcount_list_destroy_callback_t callback) {
void refcount_list_free_full(RefcountList *list,
refcount_list_destroy_callback_t callback,
const RefcountAllocator *alloc) {
while (list) {
RefcountList *next = list->next;
if (callback) {
callback(list->data);
}
refcount_free(list);
refcount_free(alloc, list);
list = next;
}
}
@ -120,16 +119,17 @@ void refcount_list_free(RefcountList *list,
* @param list The list to free
* @param callback The function to call to free each member, or NULL
* @param data Extra data to pass to the second argument of the callback
* @param alloc The allocator to use
*/
void refcount_list_free_with_data(RefcountList *list,
refcount_list_foreach_callback_t callback,
void *data) {
void refcount_list_free_with_data_full(
RefcountList *list, refcount_list_destroy_with_data_callback_t callback,
void *data, const RefcountAllocator *alloc) {
while (list) {
RefcountList *next = list->next;
if (callback) {
callback(list->data, data);
}
refcount_free(list);
refcount_free(alloc, list);
list = next;
}
}
@ -139,7 +139,7 @@ void refcount_list_free_with_data(RefcountList *list,
* @param list The list
* @return The length of the list
*/
size_t refcount_list_length(RefcountList *list) {
size_t refcount_list_length(const RefcountList *list) {
size_t len = 0;
while (list) {
list = list->next;
@ -171,41 +171,23 @@ RefcountList *refcount_list_drop(RefcountList *list, size_t n) {
* @param n The index of the element
* @return The Nth element of the list
*/
void *refcount_list_nth(RefcountList *list, size_t n) {
RefcountList *link = refcount_list_drop(list, n);
void *refcount_list_nth(const RefcountList *list, size_t n) {
RefcountList *link = refcount_list_drop((RefcountList *) list, n);
return link ? link->data : NULL;
}
/**
* Remove the first element from a #RefcountList. Note that you must save the
* return value as it is the new value of the list. If the list is a sub-list,
* the state of the larger list is undefined after this call.
*
* @param list The list
* @param callback The function to call to free the removed, or NULL
* @return The new value of the list
*
* @see refcount_list_remove
*/
RefcountList *refcount_list_pop(RefcountList *list,
refcount_list_destroy_callback_t callback) {
return refcount_list_remove(list, list, callback);
}
/**
* Remove a link from a #RefcountList. Note that you must save the return
* value as it is the new value of the list.
*
* If the given list or link is NULL, this does nothing. It is undefined
* behavior if link is not in list.
*
* The same as #refcount_list_remove, but lets you specify the allocator to use.
* @param list The list
* @param link The link to remove, or NULL
* @param callback The function to call to free the member, or NULL
* @param alloc The allocator to use
* @return The new value of the list
*/
RefcountList *refcount_list_remove(RefcountList *list, RefcountList *link,
refcount_list_destroy_callback_t callback) {
RefcountList *
refcount_list_remove_full(RefcountList *list, RefcountList *link,
refcount_list_destroy_callback_t callback,
const RefcountAllocator *alloc) {
if (!list || !link) {
return list;
}
@ -219,20 +201,22 @@ RefcountList *refcount_list_remove(RefcountList *list, RefcountList *link,
if (callback) {
callback(link->data);
}
refcount_free(link);
refcount_free(alloc, link);
return list == link ? rest : list;
}
/**
* Add an element to the start of a #RefcountList. Note that you must save the
* return value as it is the new value of the list.
* The same as #refcount_list_push, but lets you specify the allocator to
* use.
* @param list The list
* @param element The element to add
* @param alloc The allocator to use
* @return The new value of the list, or NULL if allocation of the new node
* failed
*/
RefcountList *refcount_list_push(RefcountList *list, void *element) {
RefcountList *new_head = refcount_malloc(sizeof(RefcountList));
RefcountList *refcount_list_push_full(RefcountList *list, void *element,
const RefcountAllocator *alloc) {
RefcountList *new_head = refcount_malloc(alloc, sizeof(RefcountList));
if (!new_head) {
return NULL;
}
@ -283,15 +267,17 @@ RefcountList *refcount_list_join(RefcountList *list1, RefcountList *list2) {
}
/**
* Add an element to the end of a #RefcountList. Note that you must save the
* return value as it is the new value of the list.
* The same as #refcount_list_push_back, but lets you specify the allocator to
* use.
* @param list The list
* @param element The element to add
* @param alloc The allocator to use
* @return The new value of the list, or NULL if allocation of the new node
* failed
*/
RefcountList *refcount_list_push_back(RefcountList *list, void *element) {
RefcountList *new_end = refcount_malloc(sizeof(RefcountList));
RefcountList *refcount_list_push_back_full(RefcountList *list, void *element,
const RefcountAllocator *alloc) {
RefcountList *new_end = refcount_malloc(alloc, sizeof(RefcountList));
if (!new_end) {
return NULL;
}

View File

@ -45,14 +45,21 @@ RefcountContext *refcount_default_context = NULL;
RefcountContext *
refcount_make_context(size_t entry_offset,
refcount_held_refs_callback_t held_refs_callback,
refcount_destroy_callback_t destroy_callback) {
RefcountContext *ctx = refcount_malloc(sizeof(RefcountContext));
refcount_destroy_callback_t destroy_callback,
void *user_data, const RefcountAllocator *alloc) {
if (!alloc) {
alloc = refcount_global_allocator;
}
RefcountContext *ctx = refcount_malloc(alloc, sizeof(RefcountContext));
if (!ctx) {
return NULL;
}
ctx->entry_offset = entry_offset;
ctx->held_refs_callback = held_refs_callback;
ctx->destroy_callback = destroy_callback;
ctx->user_data = user_data;
ctx->alloc = *alloc;
refcount_allocator_to_ht_allocator(&ctx->alloc, &ctx->ht_alloc);
ctx->static_objects = NULL;
ctx->gc_roots = NULL;
@ -66,12 +73,13 @@ refcount_make_context(size_t entry_offset,
* @param ctx The #RefcountContext
*/
void refcount_context_destroy(RefcountContext *ctx) {
refcount_list_free_with_data(
ctx->static_objects, refcount_context_deinit_static_as_callback, ctx);
refcount_list_free_with_data_full(
ctx->static_objects, refcount_context_deinit_static_as_callback, ctx,
&ctx->alloc);
// TODO process gc_roots
refcount_context_garbage_collect(ctx);
refcount_free(ctx);
refcount_free(&ctx->alloc, ctx);
}
/**
@ -85,37 +93,65 @@ void refcount_context_destroy(RefcountContext *ctx) {
* @param obj The object to initialize
*/
void refcount_context_init_obj(RefcountContext *ctx, void *obj) {
ENTRY->is_static = false;
ENTRY->gc_root = NULL;
ENTRY->ref_count = 0;
if (obj) {
ENTRY->is_static = false;
ENTRY->gc_root = NULL;
ENTRY->ref_count = 0;
}
}
/**
* Register a static object in a context.
* @param ctx The #RefcountContext
* @param obj The object to register
* @return True on success, false otherwise
*/
void refcount_context_init_static(RefcountContext *ctx, void *obj) {
bool refcount_context_init_static(RefcountContext *ctx, void *obj) {
ENTRY->is_static = true;
ctx->static_objects = refcount_list_push(ctx->static_objects, obj);
ctx->static_objects =
refcount_list_push_full(ctx->static_objects, obj, &ctx->alloc);
if (!ctx->static_objects) {
return false;
}
ENTRY->static_entry = ctx->static_objects;
return true;
}
/**
* Return the references held by an object.
* @param ctx The #RefcountContext
* @param obj The object
* @param refs Where to store the refs
* @return True on success
*/
static inline bool obj_held_refs(RefcountContext *ctx, void *obj,
RefcountList **refs) {
if (ctx->held_refs_callback) {
return ctx->held_refs_callback(obj, refs, ctx->user_data);
}
return true;
}
/**
* Unregister a static object in a context.
* @param ctx The #RefcountContext
* @param obj The object to unregister
* @return True on success, false otherwise
*/
void refcount_context_deinit_static(RefcountContext *ctx, void *obj) {
bool refcount_context_deinit_static(RefcountContext *ctx, void *obj) {
if (refcount_context_is_static(ctx, obj)) {
ctx->static_objects = refcount_list_remove(ctx->static_objects,
ENTRY->static_entry, NULL);
RefcountList *held_refs =
ctx->held_refs_callback ? ctx->held_refs_callback(obj) : NULL;
refcount_list_free_with_data(held_refs,
refcount_context_unref_as_callback, ctx);
RefcountList *held_refs = NULL;
if (!obj_held_refs(ctx, obj, &held_refs)) {
return false;
}
ctx->static_objects = refcount_list_remove_full(
ctx->static_objects, ENTRY->static_entry, NULL, &ctx->alloc);
refcount_list_free_with_data_full(
held_refs, refcount_context_unref_as_callback, ctx, &ctx->alloc);
return true;
} else {
BREAKPOINT(DEINIT_STATIC, ctx, obj)
return false;
}
}
@ -126,7 +162,9 @@ void refcount_context_deinit_static(RefcountContext *ctx, void *obj) {
* @return The input object
*/
void *refcount_context_ref(RefcountContext *ctx, void *obj) {
if (!ENTRY->is_static) {
if (!obj) {
return NULL;
} else if (!ENTRY->is_static) {
++ENTRY->ref_count;
}
return obj;
@ -137,12 +175,18 @@ void *refcount_context_ref(RefcountContext *ctx, void *obj) {
* already tracked object.
* @param ctx the #RefcountContext
* @param obj The object to track
* @return True on success
*/
static void track_gc_root(RefcountContext *ctx, void *obj) {
static bool track_gc_root(RefcountContext *ctx, void *obj) {
if (!ENTRY->gc_root) {
ctx->gc_roots = refcount_list_push(ctx->gc_roots, obj);
ctx->gc_roots =
refcount_list_push_full(ctx->gc_roots, obj, &ctx->alloc);
if (!ctx->gc_roots) {
return false;
}
ENTRY->gc_root = ctx->gc_roots;
}
return true;
}
/**
@ -152,7 +196,8 @@ static void track_gc_root(RefcountContext *ctx, void *obj) {
* @param obj The object to untrack
*/
static void remove_gc_root(RefcountContext *ctx, void *obj) {
ctx->gc_roots = refcount_list_remove(ctx->gc_roots, ENTRY->gc_root, NULL);
ctx->gc_roots = refcount_list_remove_full(ctx->gc_roots, ENTRY->gc_root,
NULL, &ctx->alloc);
ENTRY->gc_root = NULL;
}
@ -162,14 +207,19 @@ static void remove_gc_root(RefcountContext *ctx, void *obj) {
* @param ctx The #RefcountContext
* @param obj The object to unref
* @param queue A double pointer to a #RefcountList acting as a queue
* @param toplevel The toplevel object that triggered this unref. It is ignored
* so that it is not freed twice in the specific case that an object with a
* floating ref and a reference cycle is freed
* @return NULL if the reference count fell to 0, the given object otherwise
*/
static void *unref_to_queue(RefcountContext *ctx, void *obj,
RefcountList **queue) {
RefcountList **queue, void *toplevel) {
if (ENTRY->is_static) {
return obj;
} else if (obj == toplevel) {
return NULL;
} else if (ENTRY->ref_count <= 1) {
*queue = refcount_list_push(*queue, obj);
*queue = refcount_list_push_full(*queue, obj, &ctx->alloc);
return NULL;
}
--ENTRY->ref_count;
@ -183,6 +233,7 @@ static void *unref_to_queue(RefcountContext *ctx, void *obj,
*/
struct ContextAndQueue {
RefcountContext *ctx; //!< The context.
void *toplevel; //!< Toplevel object that triggered the unref.
RefcountList **queue; //!< The queue.
};
@ -193,27 +244,49 @@ struct ContextAndQueue {
*/
static void unref_to_queue_as_callback(void *obj, void *ctx_and_queue_raw) {
struct ContextAndQueue *ctx_and_queue = ctx_and_queue_raw;
unref_to_queue(ctx_and_queue->ctx, obj, ctx_and_queue->queue);
unref_to_queue(ctx_and_queue->ctx, obj, ctx_and_queue->queue,
ctx_and_queue->toplevel);
}
/**
* Destroy an object by calling it's destructor.
* @param ctx The #RefcountContext
* @param obj The object to destroy
*/
static inline void destroy_object(RefcountContext *ctx, void *obj) {
remove_gc_root(ctx, obj);
if (ctx->destroy_callback) {
ctx->destroy_callback(obj, ctx->user_data);
}
}
/**
* Continually release held references and object held in a queue.
* @param ctx The #RefcountContext
* @param queue The queue
* @param toplevel Toplevel object that triggered the unref
*/
static void process_unref_queue(RefcountContext *ctx, RefcountList *queue) {
struct ContextAndQueue ctx_and_queue = {.ctx = ctx, .queue = &queue};
static void process_unref_queue(RefcountContext *ctx, RefcountList *queue,
void *toplevel) {
struct ContextAndQueue ctx_and_queue = {
.ctx = ctx, .queue = &queue, .toplevel = toplevel};
while (queue) {
void *cur = refcount_list_peek(queue);
queue = refcount_list_pop(queue, NULL);
RefcountList *held_refs =
ctx->held_refs_callback ? ctx->held_refs_callback(cur) : NULL;
refcount_list_free_with_data(held_refs, unref_to_queue_as_callback,
&ctx_and_queue);
remove_gc_root(ctx, cur);
if (ctx->destroy_callback) {
ctx->destroy_callback(cur);
RefcountList *held_refs = NULL;
queue = refcount_list_pop_full(queue, NULL, &ctx->alloc);
if (!cur) {
continue;
}
if (obj_held_refs(ctx, cur, &held_refs)) {
// I don't really know how else to handle this as I can't think of a
// good way to undo all the unrefs that have already been processed,
// so we can't really make this atomic without going over all
// objects twice.
refcount_list_free_with_data_full(held_refs,
unref_to_queue_as_callback,
&ctx_and_queue, &ctx->alloc);
}
destroy_object(ctx, cur);
}
}
@ -226,10 +299,13 @@ static void process_unref_queue(RefcountContext *ctx, RefcountList *queue) {
* object
*/
void *refcount_context_unref(RefcountContext *ctx, void *obj) {
if (!obj) {
return NULL;
}
RefcountList *queue = NULL;
obj = unref_to_queue(ctx, obj, &queue);
process_unref_queue(ctx, queue);
return obj;
void *retval = unref_to_queue(ctx, obj, &queue, NULL);
process_unref_queue(ctx, queue, obj);
return retval;
}
/**
@ -242,10 +318,13 @@ void *refcount_context_unref(RefcountContext *ctx, void *obj) {
* @return The input object
*/
void *refcount_context_float(RefcountContext *ctx, void *obj) {
if (ENTRY->is_static) {
if (!obj) {
return NULL;
} else if (ENTRY->is_static) {
return obj;
} else if (!ENTRY->ref_count) {
BREAKPOINT(FLOAT, ctx, obj);
return obj;
}
if (--ENTRY->ref_count) {
track_gc_root(ctx, obj);
@ -254,3 +333,108 @@ void *refcount_context_float(RefcountContext *ctx, void *obj) {
}
return obj;
}
/**
* Set of hash table functions used in #gc_check_root.
*/
static const HTTableFunctions ROOT_COUNTS_FNS = {
.hash = ht_intptr_hash_callback,
.equal = ht_intptr_equal_callback,
.destroy_key = NULL,
.destroy_value = NULL,
.user_data = NULL,
};
struct ContextAndRootPtr {
RefcountContext *ctx;
RefcountList **root_ptr;
bool did_update;
};
static bool free_roots_foreach(void *obj, void *ignored, void *user_data) {
struct ContextAndRootPtr *data = user_data;
if (*data->root_ptr == REFCOUNT_OBJECT_ENTRY(data->ctx, obj)->gc_root) {
*data->root_ptr = (*data->root_ptr)->next;
data->did_update = true;
}
destroy_object(data->ctx, obj);
return false;
}
/**
* Check the root pointed to by the double pointer root_ptr. After the call,
* root_ptr is set to the next root to be checked.
* @param ctx The context
* @param root_ptr Double pointer to one of the GC roots
* @return True on success
*/
static bool check_gc_root(RefcountContext *ctx, RefcountList **root_ptr) {
HTTable *counts = ht_new(&ROOT_COUNTS_FNS, &ctx->ht_alloc, NULL);
if (!counts) {
*root_ptr = (*root_ptr)->next;
return false;
}
RefcountList *root = *root_ptr;
RefcountList *queue = NULL;
if (!obj_held_refs(ctx, root->data, &queue)) {
ht_free(counts);
*root_ptr = (*root_ptr)->next;
return false;
}
size_t seen_objects = 0;
size_t clear_objects = 0;
// ignore allocation errors until I decide how to deal with them (never)
while (queue) {
void *obj = queue->data;
if (!obj || refcount_context_is_static(ctx, obj)) {
goto next;
}
uintptr_t count;
if (ht_has(counts, obj)) {
count = HT_UUNSTUFF(ht_get(counts, obj));
} else {
count = REFCOUNT_OBJECT_ENTRY(ctx, obj)->ref_count;
++seen_objects;
}
if (count > 0) {
ht_insert(counts, obj, HT_STUFF(--count));
if (count == 0) {
++clear_objects;
}
obj_held_refs(ctx, root->data, &queue);
}
next:
queue = refcount_list_pop_full(queue, NULL, &ctx->alloc);
}
if (seen_objects == clear_objects) {
struct ContextAndRootPtr data = {
.ctx = ctx, .root_ptr = root_ptr, .did_update = false};
ht_foreach(counts, free_roots_foreach, &data);
if (!data.did_update) {
*root_ptr = (*root_ptr)->next;
}
} else {
*root_ptr = (*root_ptr)->next;
}
ht_free(counts);
return true;
}
/**
* Run the garbage collector on a context.
* @param cts The #RefcountContext
* @return False if an error occurred, true otherwise
*/
bool refcount_context_garbage_collect(RefcountContext *ctx) {
if (!ctx->held_refs_callback) {
// no loops possible
return true;
}
RefcountList *root = ctx->gc_roots;
while (root) {
if (!check_gc_root(ctx, &root)) {
return false;
}
}
return true;
}