From 36abc66ac57d8561f01af8f4ac4458dc3ac3fb73 Mon Sep 17 00:00:00 2001 From: Alexander Rosenberg Date: Sat, 30 Aug 2025 21:21:02 -0700 Subject: [PATCH] Initial garbage colletor implementation --- CMakeLists.txt | 16 +- include/refcount/allocator.h | 89 +++++------ include/refcount/list.h | 283 ++++++++++++++++++++++++++++++++--- include/refcount/refcount.h | 61 ++++++-- src/allocator.c | 40 ++++- src/list.c | 114 +++++++------- src/refcount.c | 262 +++++++++++++++++++++++++++----- test/CMakeLists.txt | 2 +- test/alloc.h | 1 - test/test_list.c | 8 +- test/test_refcount.c | 47 ++++-- 11 files changed, 709 insertions(+), 214 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b1c13da..26ea3f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.11) set(CMAKE_C_STANDARD 11) @@ -7,13 +7,25 @@ project( VERSION 1.0 LANGUAGES C) +include(FetchContent) +FetchContent_Declare( + ht + GIT_REPOSITORY https://git.zander.im/Zander671/ht.git + GIT_TAG 9a1b271cfdc8ab203f9d6aa6a83cc4523de422be) + +FetchContent_MakeAvailable(ht) + if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) include(CTest) endif() +add_compile_options(-fsanitize=address,leak,undefined) +add_link_options(-fsanitize=address,leak,undefined) + add_library(refcount src/allocator.c src/list.c src/refcount.c) +target_link_libraries(refcount PUBLIC ht) target_include_directories(refcount PUBLIC include/) -target_compile_options(refcount PRIVATE -Wall) +target_compile_options(refcount PRIVATE -Wall -Wpedantic) if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING) add_subdirectory(test/) diff --git a/include/refcount/allocator.h b/include/refcount/allocator.h index e0e06b9..d850872 100644 --- a/include/refcount/allocator.h +++ b/include/refcount/allocator.h @@ -4,6 +4,8 @@ #ifndef INCLUDED_REFCOUNT_ALLOCATOR_H #define INCLUDED_REFCOUNT_ALLOCATOR_H +#include +#include #include #ifdef __cplusplus @@ -14,12 +16,27 @@ extern "C" { * A set of the three main C standard allocators. Each member function should * behave the same as its standard library counterpart. That is, for example, * free should allow NULL as an input. + * + * To allow e.g. keeping track of allocations in different contexts, each + * function also takes a second user_data pointer argument. */ typedef struct RefcountAllocator { - void *(*malloc)(size_t); //!< malloc implementation for the allocator. - void *(*realloc)(void *, - size_t); //!< realloc implementation for the allocator. - void (*free)(void *); //!< free implementation for the allocator. + union { + void *(*malloc)(size_t size); //!< malloc implementation which takes no + //!< data. + void *(*malloc_with_data)( + size_t size, + void *user_data); //!< malloc implementation that takes a data + //!< argument. + }; + union { + void (*free)(void *ptr); //!< free implementation which takes no data. + void (*free_with_data)(void *ptr, + void *user_data); //!< free implementation that + //!< takes a data argument. + }; + bool pass_data; + void *user_data; } RefcountAllocator; extern const RefcountAllocator *refcount_global_allocator; @@ -30,67 +47,29 @@ extern const RefcountAllocator *refcount_global_allocator; * @param size The number of bytes to allocate * @return The newly allocate pointer, or NULL if allocation failed */ -static inline void *refcount_allocator_malloc(const RefcountAllocator *alloc, - size_t size) { +static inline void *refcount_malloc(const RefcountAllocator *alloc, + size_t size) { + if (alloc->pass_data) { + return alloc->malloc_with_data(size, alloc->user_data); + } return alloc->malloc(size); } -/** - * Reallocate memory using a #RefcountAllocator. - * @param alloc The allocator to use - * @param old_ptr The pointer to reallocate - * @param size The number of bytes to allocate - * @return The resized pointer, a newly allocated pointer, or NULL if - * allocation failed - */ -static inline void *refcount_allocator_realloc(const RefcountAllocator *alloc, - void *old_ptr, size_t size) { - return alloc->realloc(old_ptr, size); -} - /** * Free memory using a #RefcountAllocator. * @param alloc The allocator to use * @param ptr The pointer to free */ -static inline void refcount_allocator_free(const RefcountAllocator *alloc, - void *ptr) { - alloc->free(ptr); +static inline void refcount_free(const RefcountAllocator *alloc, void *ptr) { + if (alloc->pass_data) { + alloc->free_with_data(ptr, alloc->user_data); + } else { + alloc->free(ptr); + } } -/** - * Allocate memory using the global #RefcountAllocator. - * @param size The number of bytes to allocate - * @return The newly allocate pointer, or NULL if allocation failed - * - * @see refcount_allocator_malloc - */ -static inline void *refcount_malloc(size_t size) { - return refcount_allocator_malloc(refcount_global_allocator, size); -} - -/** - * Reallocate memory using the global #RefcountAllocator. - * @param old_ptr The pointer to reallocate - * @param size The number of bytes to allocate - * @return The resized pointer, a newly allocated pointer, or NULL if - * allocation failed - * - * @see refcount_allocator_realloc - */ -static inline void *refcount_realloc(void *old_ptr, size_t size) { - return refcount_allocator_realloc(refcount_global_allocator, old_ptr, size); -} - -/** - * Free memory using the global #RefcountAllocator. - * @param ptr The pointer to free - * - * @see refcount_allocator_free - */ -static inline void refcount_free(void *ptr) { - refcount_allocator_free(refcount_global_allocator, ptr); -} +void refcount_allocator_to_ht_allocator(const RefcountAllocator *src, + HTAllocator *dest); #ifdef __cplusplus } diff --git a/include/refcount/list.h b/include/refcount/list.h index 531bf23..35efc88 100644 --- a/include/refcount/list.h +++ b/include/refcount/list.h @@ -5,6 +5,7 @@ #define INCLUDED_REFCOUNT_LIST_H #include +#include #include // for NULL #ifdef __cplusplus @@ -24,42 +25,130 @@ typedef struct RefcountList { /** * Callback to used to free list elements. + * @param ptr The pointer to free * @see refcount_list_free */ -typedef void (*refcount_list_destroy_callback_t)(void *); +typedef void (*refcount_list_destroy_callback_t)(void *ptr); /** - * Callback to used when looping over the list with extra data. - * @see refcount_list_free_with_data + * Callback to used to free list elements. + * @param ptr The pointer to free + * @param user_data Arbitrary user specified data + * @see refcount_list_free */ -typedef void (*refcount_list_foreach_callback_t)(void *, void *); +typedef void (*refcount_list_destroy_with_data_callback_t)(void *ptr, + void *user_data); /** * Callback used when copying elements from one list to another. + * @param ptr The element to copy + * @param user_data Arbitrary user specified data + * @return The new copy, or NULL if an error occurred * @see refcount_list_copy */ -typedef void *(*refcount_list_copy_callback_t)(void *); +typedef void *(*refcount_list_copy_callback_t)(const void *ptr, + void *user_data); -RefcountList *refcount_list_build(int count, ...); +RefcountList *refcount_list_build_full_va(int count, + const RefcountAllocator *alloc, + va_list args); -RefcountList *refcount_list_copy(RefcountList *list, - refcount_list_copy_callback_t callback); +/** + * Like #refcount_list_build, but lets you specify the allocator to use. + * @param count The number of elements given + * @param alloc The allocator to use + * @param ... The elements from which to build the list + * @return The built list, or NULL if a memory allocation error occurred + */ +static inline RefcountList * +refcount_list_build_full(int count, const RefcountAllocator *alloc, ...) { + va_list args; + va_start(args, alloc); + RefcountList *retval = refcount_list_build_full_va(count, alloc, args); + va_end(args); + return retval; +} + +/** + * 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. + * + * @param count The number of elements given + * @param ... The elements from which to build the list + * @return The built list, or NULL if a memory allocation error occurred + */ +static inline RefcountList *refcount_list_build(int count, ...) { + va_list args; + va_start(args, count); + RefcountList *retval = + refcount_list_build_full_va(count, refcount_global_allocator, args); + va_end(args); + return retval; +} + +RefcountList *refcount_list_copy_full(const RefcountList *list, + refcount_list_copy_callback_t callback, + void *user_data, + const RefcountAllocator *alloc); + +/** + * Return a new #RefcountList that is a copy of list. This does no allocation if + * the input list is NULL. + * @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 + * @return The copy, or NULL if an allocation error occurred + */ +static inline RefcountList * +refcount_list_copy(const RefcountList *list, + refcount_list_copy_callback_t callback, void *user_data) { + return refcount_list_copy_full(list, callback, user_data, + refcount_global_allocator); +} RefcountList *refcount_list_reverse(RefcountList *list); -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); -void refcount_list_free_with_data(RefcountList *list, - refcount_list_foreach_callback_t callback, - void *data); +/** + * Free a #RefcountList. + * @param list The list to free + * @param callback The function to call to free each member, or NULL + */ +static inline void +refcount_list_free(RefcountList *list, + refcount_list_destroy_callback_t callback) { + refcount_list_free_full(list, callback, refcount_global_allocator); +} -size_t refcount_list_length(RefcountList *list); +void refcount_list_free_with_data_full( + RefcountList *list, refcount_list_destroy_with_data_callback_t callback, + void *data, const RefcountAllocator *alloc); + +/** + * Free a #RefcountList. + * @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 + */ +static inline void refcount_list_free_with_data( + RefcountList *list, refcount_list_destroy_with_data_callback_t callback, + void *data) { + refcount_list_free_with_data_full(list, callback, data, + refcount_global_allocator); +} + +size_t refcount_list_length(const RefcountList *list); /** * Return the first element in a #RefcountList. * @param list The list * @return The first object in the list */ -static inline void *refcount_list_peek(RefcountList *list) { +static inline void *refcount_list_peek(const RefcountList *list) { if (!list) { return NULL; } @@ -68,21 +157,171 @@ static inline void *refcount_list_peek(RefcountList *list) { RefcountList *refcount_list_drop(RefcountList *list, size_t n); -void *refcount_list_nth(RefcountList *list, size_t n); +void *refcount_list_nth(const RefcountList *list, size_t n); -RefcountList *refcount_list_pop(RefcountList *list, - refcount_list_destroy_callback_t callback); +RefcountList * +refcount_list_remove_full(RefcountList *list, RefcountList *link, + refcount_list_destroy_callback_t callback, + const RefcountAllocator *alloc); -RefcountList *refcount_list_remove(RefcountList *list, RefcountList *link, - refcount_list_destroy_callback_t 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. + * + * @param list The list + * @param link The link to remove, or NULL + * @param callback The function to call to free the member, or NULL + * @return The new value of the list + */ +static inline RefcountList * +refcount_list_remove(RefcountList *list, RefcountList *link, + refcount_list_destroy_callback_t callback) { + return refcount_list_remove_full(list, link, callback, + refcount_global_allocator); +} -RefcountList *refcount_list_push(RefcountList *list, void *element); +/** + * Same as #refcount_list_remove_with_data, but lets you specify the allocator + * to use. + * @param list The list + * @param callback The function to call to free the member, or NULL + * @param link The link to remove, or NULL + * @param user_data Extra data to pass to the callback + * @param alloc The allocator to use + * @return The new value of the list + */ +static inline RefcountList *refcount_list_remove_with_data_full( + RefcountList *list, RefcountList *link, + refcount_list_destroy_with_data_callback_t callback, void *user_data, + const RefcountAllocator *alloc) { + if (list && link && callback) { + callback(link->data, user_data); + } + return refcount_list_remove_full(list, link, NULL, alloc); +} + +/** + * Same as #refcount_list_remove except that the callback takes an additional + * argument. This also does nothing if either of list or link is NULL. + * @param list The list + * @param callback The function to call to free the member, or NULL + * @param link The link to remove, or NULL + * @param user_data Extra data to pass to the callback + * @return The new value of the list + */ +static inline RefcountList *refcount_list_remove_with_data( + RefcountList *list, RefcountList *link, + refcount_list_destroy_with_data_callback_t callback, void *user_data) { + return refcount_list_remove_with_data_full(list, link, callback, user_data, + refcount_global_allocator); +} + +/** + * Same as #refcount_list_pop, but lets you specify the allocator to use. + * + * @param list The list + * @param callback The function to call to free the removed, or NULL + * @param alloc The allocator to use + * @return The new value of the list + * + * @see refcount_list_remove + */ +static inline RefcountList * +refcount_list_pop_full(RefcountList *list, + refcount_list_destroy_callback_t callback, + const RefcountAllocator *alloc) { + return refcount_list_remove_full(list, list, callback, alloc); +} + +/** + * 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 + */ +static inline RefcountList * +refcount_list_pop(RefcountList *list, + refcount_list_destroy_callback_t callback) { + return refcount_list_pop_full(list, callback, refcount_global_allocator); +} + +/** + * Same as #refcount_list_pop_with_data, but lets specify the allocator to use. + * @param list The list + * @param callback The function to call to free the member, or NULL + * @param user_data Extra data to pass to the callback + * @param alloc The allocator to use + * @return The new value of the list + */ +static inline RefcountList *refcount_list_pop_with_data_full( + RefcountList *list, refcount_list_destroy_with_data_callback_t callback, + void *user_data, const RefcountAllocator *alloc) { + if (list && callback) { + callback(list->data, user_data); + } + return refcount_list_pop_full(list, NULL, alloc); +} + +/** + * Same as #refcount_list_pop except that the callback takes an additional + * argument. + * @param list The list + * @param callback The function to call to free the member, or NULL + * @param user_data Extra data to pass to the callback + * @return The new value of the list + */ +static inline RefcountList * +refcount_list_pop_with_data(RefcountList *list, + refcount_list_destroy_with_data_callback_t callback, + void *user_data) { + return refcount_list_pop_with_data_full(list, callback, user_data, + refcount_global_allocator); +} + +RefcountList *refcount_list_push_full(RefcountList *list, void *element, + const RefcountAllocator *alloc); + +/** + * 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. + * @param list The list + * @param element The element to add + * @return The new value of the list, or NULL if allocation of the new node + * failed + */ +static inline RefcountList *refcount_list_push(RefcountList *list, + void *element) { + return refcount_list_push_full(list, element, refcount_global_allocator); +} RefcountList *refcount_list_tail(RefcountList *list); RefcountList *refcount_list_join(RefcountList *list1, RefcountList *list2); -RefcountList *refcount_list_push_back(RefcountList *list, void *element); +RefcountList *refcount_list_push_back_full(RefcountList *list, void *element, + const RefcountAllocator *alloc); + +/** + * 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. + * @param list The list + * @param element The element to add + * @return The new value of the list, or NULL if allocation of the new node + * failed + */ +static inline RefcountList *refcount_list_push_back(RefcountList *list, + void *element) { + return refcount_list_push_back_full(list, element, + refcount_global_allocator); +} #ifdef __cplusplus } diff --git a/include/refcount/refcount.h b/include/refcount/refcount.h index be08ed3..88b840f 100644 --- a/include/refcount/refcount.h +++ b/include/refcount/refcount.h @@ -4,6 +4,7 @@ #ifndef INCLUDED_REFCOUNT_REFCOUNT_H #define INCLUDED_REFCOUNT_REFCOUNT_H +#include #include #include #include @@ -14,15 +15,29 @@ extern "C" { /** * Callback for listing all references held by the passed object. The function - * should take an objects and return a #RefcountList of references held by that - * object. + * should take an object and return a #RefcountList of references held by that + * object. The second parameter is user supplied data. + * + * > The list in the refs parameter will probably not be empty, so it is + * > important that you update it, not replace it. If an errors occurs, you + * > mustn't free anything that you did not add to the list yourself. + * + * @param obj The object + * @param refs A pointer to a #RefcountList that should be updated with the held + * references. + * @param user_data User supplied data pointer + * @return True if the operation was successful, false otherwise */ -typedef RefcountList *(*refcount_held_refs_callback_t)(void *); +typedef bool (*refcount_held_refs_callback_t)(void *obj, RefcountList **refs, + void *user_data); /** * Callback for freeing an object after its reference count drops to 0. It is - * safe the free the object from this callback. + * safe the free the object from this callback. The second parameter is user + * supplied data. + * @param obj The object to free + * @param user_data User supplied data pointer */ -typedef void (*refcount_destroy_callback_t)(void *); +typedef void (*refcount_destroy_callback_t)(void *obj, void *user_data); /** * Context for reference counting a specific type of object. This should be @@ -33,9 +48,11 @@ typedef struct RefcountContext { refcount_held_refs_callback_t held_refs_callback; //!< Callback to list an object's held references. refcount_destroy_callback_t - destroy_callback; //!< Callback to free an object - //!< after its reference count - //!< dropped to 0. + destroy_callback; //!< Callback to free an object after its reference + //!< count drops to 0. + void *user_data; //entry_offset)) + ((RefcountEntry *) (((char *) (obj)) + (ctx)->entry_offset)) /** * Test whether an object is static or not. The object must have been @@ -128,9 +148,9 @@ static inline size_t refcount_context_num_refs(RefcountContext *ctx, void refcount_context_init_obj(RefcountContext *ctx, void *obj); -void refcount_context_init_static(RefcountContext *ctx, void *obj); +bool refcount_context_init_static(RefcountContext *ctx, void *obj); -void refcount_context_deinit_static(RefcountContext *ctx, void *obj); +bool refcount_context_deinit_static(RefcountContext *ctx, void *obj); /** * Unregister a static object in a context. This is suitable to be passed as a @@ -158,6 +178,8 @@ static inline void refcount_context_unref_as_callback(void *obj, void *ctx) { } void *refcount_context_float(RefcountContext *ctx, void *obj); +bool refcount_context_garbage_collect(RefcountContext *ctx); + /** * Same as #refcount_context_is_static, but only operates on the global * context. @@ -242,6 +264,15 @@ static inline void *refcount_float(void *obj) { return refcount_context_float(refcount_default_context, obj); } +/** + * Same as #refcount_context_garbage_collect, but only operates on the globa + * context. + * @return False if an error occured, true otherwise + */ +static inline bool refcount_garbage_collect(void) { + return refcount_context_garbage_collect(refcount_default_context); +} + #ifdef __cplusplus } #endif diff --git a/src/allocator.c b/src/allocator.c index 879f926..844f130 100644 --- a/src/allocator.c +++ b/src/allocator.c @@ -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; +} diff --git a/src/list.c b/src/list.c index 0929de3..be9a6dc 100644 --- a/src/list.c +++ b/src/list.c @@ -4,31 +4,26 @@ */ #include "refcount/list.h" -#include #include #include /** - * 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; } diff --git a/src/refcount.c b/src/refcount.c index e191811..d4a662d 100644 --- a/src/refcount.c +++ b/src/refcount.c @@ -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; +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6424bcb..76decef 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,5 +1,5 @@ link_libraries(refcount) -add_compile_options(-Wall) +add_compile_options(-Wall -Wpedantic) add_executable(test_list test_list.c) add_test(NAME list COMMAND test_list) diff --git a/test/alloc.h b/test/alloc.h index 39099fc..afe9e18 100644 --- a/test/alloc.h +++ b/test/alloc.h @@ -68,7 +68,6 @@ static UNUSED char *counting_strdup(const char *str) { static UNUSED const RefcountAllocator COUNTING_ALLOCATOR = { .malloc = counting_malloc, - .realloc = counting_realloc, .free = counting_free, }; diff --git a/test/test_list.c b/test/test_list.c index 692db42..4b58528 100644 --- a/test/test_list.c +++ b/test/test_list.c @@ -4,7 +4,11 @@ #include #include -int main() { +void *counting_strdup_callback(const void *str, void *ignored) { + return counting_strdup(str); +} + +int main(int argc, const char **argv) { refcount_global_allocator = &COUNTING_ALLOCATOR; assert(refcount_list_length(NULL) == 0); @@ -77,7 +81,7 @@ int main() { assert(strcmp(refcount_list_nth(l, 3), "str4") == 0); assert(strcmp(refcount_list_nth(l, 4), "str5") == 0); - RefcountList *l2 = refcount_list_copy(l, (void *) counting_strdup); + RefcountList *l2 = refcount_list_copy(l, counting_strdup_callback, NULL); refcount_list_free(l, counting_free); diff --git a/test/test_refcount.c b/test/test_refcount.c index f446e24..c79d6d3 100644 --- a/test/test_refcount.c +++ b/test/test_refcount.c @@ -7,49 +7,78 @@ typedef struct A { int num; RefcountEntry refcount; char *str; + struct A *next; } A; A *make_a(RefcountContext *c, int n, const char *s) { A *a = counting_malloc(sizeof(A)); a->num = n; a->str = counting_strdup(s); + a->next = NULL; refcount_context_init_obj(c, a); return a; } -void destroy_callback(void *a_raw) { +bool held_refs_callback(void *a_raw, RefcountList **out, void *ignored) { + A *a = a_raw; + if (a->next) { + *out = refcount_list_push_full(*out, a->next, &COUNTING_ALLOCATOR); + } + return true; +} + +void destroy_callback(void *a_raw, void *ignored) { A *a = a_raw; counting_free(a->str); counting_free(a); } -int main() { - refcount_global_allocator = &COUNTING_ALLOCATOR; - +int main(int argc, const char **argv) { RefcountContext *c = - refcount_make_context(offsetof(A, refcount), NULL, destroy_callback); + refcount_make_context(offsetof(A, refcount), held_refs_callback, + destroy_callback, NULL, &COUNTING_ALLOCATOR); A *a = make_a(c, 10, "Hello world\n"); assert(!refcount_context_is_static(c, a)); assert(refcount_context_num_refs(c, a) == 0); - refcount_context_ref(c, a); + a = refcount_context_ref(c, a); assert(refcount_context_num_refs(c, a) == 1); - refcount_context_ref(c, a); + a = refcount_context_ref(c, a); assert(refcount_context_num_refs(c, a) == 2); - refcount_context_unref(c, a); + a = refcount_context_unref(c, a); assert(refcount_context_num_refs(c, a) == 1); - refcount_context_float(c, a); + a = refcount_context_float(c, a); + assert(a); assert(refcount_context_num_refs(c, a) == 0); refcount_context_ref(c, a); assert(refcount_context_num_refs(c, a) == 1); + a = refcount_context_unref(c, a); + assert(!a); + + a = make_a(c, 10, "Hello World\n"); + A *b = make_a(c, 42, "The answer!"); + a->next = refcount_context_ref(c, b); + assert(refcount_context_num_refs(c, a->next) == 1); + assert(refcount_context_num_refs(c, a) == 0); + refcount_context_unref(c, a); + a = make_a(c, 'a', "a"); + a->next = refcount_context_ref(c, a); + assert(refcount_context_num_refs(c, a) == 1); + + refcount_context_ref(c, a); + refcount_context_unref(c, a); + assert(refcount_context_num_refs(c, a) == 1); + + refcount_context_garbage_collect(c); + refcount_context_destroy(c); check_allocator_status();