diff --git a/CMakeLists.txt b/CMakeLists.txt index 858bae5..e524787 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,12 @@ set(REFCOUNT_USE_THREADS ON CACHE BOOL "Weather or not RefCount should be thread aware.") +if(REFCOUNT_USE_THREADS) + message("Building with thread support, setting CMAKE_C_STANDARD to 11.") +else() + message("Building without thread support, setting CMAKE_C_STANDARD to 99.") +endif() + if(REFCOUNT_USE_THREADS) set(CMAKE_C_STANDARD 11) else() @@ -22,7 +28,7 @@ include(FetchContent) FetchContent_Declare( ht GIT_REPOSITORY https://git.zander.im/Zander671/ht.git - GIT_TAG 9a1b271cfdc8ab203f9d6aa6a83cc4523de422be) + GIT_TAG c6bdb38bb77d45f9d5083706723c84c37db56c9c) FetchContent_MakeAvailable(ht) @@ -30,8 +36,8 @@ 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_compile_options(-fsanitize=address,leak,undefined) +add_link_options(-fsanitize=address,leak,undefined) configure_file(include/refcount/config.h.in include/refcount/config.h @ONLY) diff --git a/include/refcount/refcount.h b/include/refcount/refcount.h index b010784..0be0fb3 100644 --- a/include/refcount/refcount.h +++ b/include/refcount/refcount.h @@ -80,8 +80,8 @@ refcount_make_context(size_t entry_offset, void refcount_context_destroy(RefcountContext *ctx); /** - * Structure holding the required information for a weak reference. That is, a - * reference that does not prevent an object from being de-allocated. + * Opaque structure holding the required information for a weak reference. That + * is, a reference that does not prevent an object from being de-allocated. */ typedef struct RefcountWeakref { #ifdef REFCOUNT_HAS_THREADS @@ -94,8 +94,18 @@ typedef struct RefcountWeakref { } RefcountWeakref; /** - * Opaque struct holding reference data for an object. This should be included - * as a non-pointer member of the struct you are trying to track, e.g. + * Destructor callback type. Called before and object is freed after having its + * reference count drop to zero. This function can prevent the given object + * from being freed by increasing its reference count. + * @param obj The object being destroyed + * @param user_data Arbitrary user_provided data + */ +typedef void (*refcount_destructor_callback_t)(void *obj, void *user_data); + +/** + * Opaque struct holding reference data for an object. This should be + * included as a non-pointer member of the struct you are trying to track, + * e.g. * @code * struct A { * int my_num; @@ -106,7 +116,8 @@ typedef struct RefcountWeakref { */ typedef struct RefcountEntry { bool is_static; //!< Whether the object is static. - RefcountWeakref *weak_ref; //alloc, entry); +} + +/** + * Initialize the destructor table for an object. + * @param ctx The #RefcountContext + * @param obj The object to initialize + * @return True on success + */ +static bool init_obj_destructor_table(const RefcountContext *ctx, void *obj) { + ENTRY->destructors = ht_new( + &(HTTableFunctions) { + .equal = ht_intptr_equal_callback, + .hash = ht_intptr_hash_callback, + .destroy_key = NULL, + .destroy_value = free_destructor_entry_callback, + .user_data = (void *) ctx, + }, + &ctx->ht_alloc, NULL); + return ENTRY->destructors; +} + /** * Initialize the #RefcountEntry member of an object. * @param ctx The #RefcountContext @@ -145,7 +183,13 @@ bool refcount_context_init_obj(const RefcountContext *ctx, void *obj) { ENTRY->is_static = false; ENTRY->impl.counted.gc_root = NULL; ENTRY->impl.counted.ref_count = 0; - init_obj_weakref(ctx, obj); + if (!init_obj_destructor_table(ctx, obj)) { + return false; + } + if (!init_obj_weakref(ctx, obj)) { + ht_free(ENTRY->destructors); + return false; + } } return true; } @@ -162,7 +206,11 @@ bool refcount_context_init_static(RefcountContext *ctx, void *obj) { } bool success = false; ENTRY->is_static = true; + if (!init_obj_destructor_table(ctx, obj)) { + goto end; + } if (!init_obj_weakref(ctx, obj)) { + refcount_free(&ctx->alloc, ENTRY->destructors); goto end; } RefcountList *new_static_objects = @@ -208,6 +256,40 @@ static void unref_weakref(const RefcountContext *ctx, RefcountWeakref *wr) { } } +/** + * Used to pass two values to #call_object_destructors_foreach_callback. + */ +struct ContextAndObject { + const RefcountContext *ctx; //!< The #RefcountContext. + void *obj; //!< The object to check. +}; + +/** + * Callback used from #call_object_destructors. + * @param key The hash table key + * @param entry_raw The #DestructorEntry + * @param ctx_and_obj_raw The #ContextAndObject + * @return True if the object's refcount is non-zero, false otherwise + */ +static bool call_object_destructors_foreach_callback(void *key, void *entry_raw, + void *ctx_and_obj_raw) { + struct ContextAndObject *ctx_and_obj = ctx_and_obj_raw; + const struct DestructorEntry *entry = entry_raw; + entry->callback(ctx_and_obj->obj, entry->user_data); + // if the refcount has increased past 0, stop looping + return refcount_context_num_refs(ctx_and_obj->ctx, ctx_and_obj->obj); +} + +/** + * Call descructors for an object. + * @param ctx The #RefcountContext + * @param obj The object to call descructors for + */ +static void call_object_destructors(const RefcountContext *ctx, void *obj) { + ht_foreach(ENTRY->destructors, call_object_destructors_foreach_callback, + &(struct ContextAndObject) {.ctx = ctx, .obj = obj}); +} + /** * Unregister a static object in a context. * @param ctx The #RefcountContext @@ -231,6 +313,8 @@ bool refcount_context_deinit_static(RefcountContext *ctx, void *obj) { goto end; } ENTRY->weak_ref->data = NULL; + call_object_destructors(ctx, obj); + ht_free(ENTRY->destructors); unref_weakref(ctx, ENTRY->weak_ref); unlock_entry_mtx(ENTRY); ctx->static_objects = refcount_list_remove_full( @@ -330,7 +414,13 @@ static void *unref_to_queue(RefcountContext *ctx, void *obj, return obj; } if (ENTRY->impl.counted.ref_count <= 1) { - *queue = refcount_list_push_full(*queue, obj, &ctx->alloc); + ENTRY->impl.counted.ref_count = 0; + call_object_destructors(ctx, obj); + if (!ENTRY->impl.counted.ref_count) { + // if we still have no refs after calling destructors, it's really + // time to free this object + *queue = refcount_list_push_full(*queue, obj, &ctx->alloc); + } unlock_entry_mtx(ENTRY); return NULL; } else { @@ -373,6 +463,7 @@ static inline void destroy_object(RefcountContext *ctx, void *obj) { } remove_gc_root(ctx, obj); ENTRY->weak_ref->data = NULL; + ht_free(ENTRY->destructors); unlock_entry_mtx(ENTRY); unref_weakref(ctx, ENTRY->weak_ref); if (ctx->destroy_callback) { @@ -381,7 +472,7 @@ static inline void destroy_object(RefcountContext *ctx, void *obj) { } /** - * Continually release held references and object held in a queue. + * Continually release held references and objects held in a queue. * @param ctx The #RefcountContext * @param queue The queue * @param toplevel Toplevel object that triggered the unref @@ -646,3 +737,50 @@ void *refcount_context_ref_weakref(const RefcountContext *ctx, unlock_mutex(&wr->mtx); return obj; } + +/** + * Register a destructor to be called right before an object is freed. If the + * destructor adds a new reference to the object, the object is not freed. The + * destructor will then be run again the next time the object is about to be + * freed. + * + * Note that if a destructor already exists for the given key, it will be + * replaced without calling it. + * @param ctx The #RefcountContext + * @param obj The object onto which to register the destructor + * @param key An arbitrary value that can be later used to unregister the + * destructor + * @param callback The destructor itself + * @param user_data Extra data to pass to the destructor + * @return True on success, false on failure. On failure, nothing is registered. + */ +bool refcount_context_add_destructor(const RefcountContext *ctx, void *obj, + void *key, + refcount_destructor_callback_t callback, + void *user_data) { + struct DestructorEntry *entry = + refcount_malloc(&ctx->alloc, sizeof(struct DestructorEntry)); + if (!entry) { + return false; + } + entry->callback = callback; + entry->user_data = user_data; + if (!ht_insert(ENTRY->destructors, key, entry)) { + refcount_free(&ctx->alloc, entry); + return false; + } + return true; +} + +/** + * Unregister a destructor from the given object. The destructor will not be + * called. If no destructor exists for the given key, this will do nothing. + * @param ctx The #RefcountContext + * @param obj The object for which to unregister the destructor + * @param key The destructors key + * @return True on success, false on error. On error, nothing is unregistered. + */ +bool refcount_context_remove_destructor(const RefcountContext *ctx, void *obj, + void *key) { + return ht_remove(ENTRY->destructors, key); +} diff --git a/test/test_refcount.c b/test/test_refcount.c index 104f8ae..4b3ce86 100644 --- a/test/test_refcount.c +++ b/test/test_refcount.c @@ -32,11 +32,23 @@ void destroy_callback(void *a_raw, void *ignored) { counting_free(a); } +void reref_destructor(void *a, void *ctx_raw) { + const RefcountContext *ctx = ctx_raw; + refcount_context_ref(ctx, a); +} + int main(int argc, const char **argv) { RefcountContext *c = refcount_make_context(offsetof(A, refcount), held_refs_callback, destroy_callback, NULL, &COUNTING_ALLOCATOR); + A static_a = { + .num = 0, + .str = counting_strdup("static"), + .next = make_a(c, 0, "in static"), + }; + refcount_context_init_static(c, &static_a); + A *a = make_a(c, 10, "Hello world\n"); assert(!refcount_context_is_static(c, a)); assert(refcount_context_num_refs(c, a) == 0); @@ -148,6 +160,20 @@ int main(int argc, const char **argv) { refcount_context_destroy_weakref(c, w); refcount_context_destroy_weakref(c, x); + a = make_a(c, 10, "test destructor"); + assert(refcount_context_num_refs(c, a) == 0); + int key; + assert(refcount_context_add_destructor(c, a, &key, reref_destructor, c)); + assert(refcount_context_num_refs(c, a) == 0); + assert(!refcount_context_unref(c, a)); + assert(refcount_context_num_refs(c, a) == 1); + assert(refcount_context_remove_destructor(c, a, &key)); + assert(refcount_context_num_refs(c, a) == 1); + assert(!refcount_context_unref(c, a)); + + refcount_context_deinit_static(c, &static_a); + counting_free(static_a.str); + refcount_context_destroy(c); check_allocator_status();