diff --git a/CMakeLists.txt b/CMakeLists.txt index 9054188..858bae5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,23 @@ cmake_minimum_required(VERSION 3.11) -set(CMAKE_C_STANDARD 99) +set(REFCOUNT_USE_THREADS + ON + CACHE BOOL "Weather or not RefCount should be thread aware.") + +if(REFCOUNT_USE_THREADS) + set(CMAKE_C_STANDARD 11) +else() + set(CMAKE_C_STANDARD 99) +endif() project( refcount VERSION 1.0 LANGUAGES C) +# For configuring config.h (I like this name better) +set(REFCOUNT_HAS_THREADS "${REFCOUNT_USE_THREADS}") + include(FetchContent) FetchContent_Declare( ht @@ -19,13 +30,17 @@ 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) 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 -Wpedantic) +target_include_directories(refcount + PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/include") if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING) add_subdirectory(test/) diff --git a/README.md b/README.md index 3470fe1..ccb59dc 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,30 @@ # RefCount RefCount is a reference counting (thus the name) and garbage collection library -written in standard C99. +written in C. + +RefCount has support for + - breaking dependency chains via garbage collection + - Multiple distinct contexts allowing the tracking of many types of objects, + each with a possibly different memory allocation implementation + - Safely running in multi-threaded environments ### Building +The only external requirement is [ht](https://git.zander.im/Zander671/ht). If +you build using the instructions below, CMake will automatically download and +build the dependency for you. + +For multi threading support, you need a C11 compiler that supports atomic types +and has the `` header. Without threading support, you only need a C99 +compiler. + You can build using: ```sh -cmake -B build +cmake -B build . +# replace with above with +# cmake -B build -DREFCOUNT_USE_THREADS=OFF +# to build without threads (change the "OFF" to "ON" to re-enable threads) make -C build ``` diff --git a/include/refcount/allocator.h b/include/refcount/allocator.h index 5645083..2bd9e08 100644 --- a/include/refcount/allocator.h +++ b/include/refcount/allocator.h @@ -5,6 +5,7 @@ #define INCLUDED_REFCOUNT_ALLOCATOR_H #include +#include #include #include diff --git a/include/refcount/config.h.in b/include/refcount/config.h.in new file mode 100644 index 0000000..1648b96 --- /dev/null +++ b/include/refcount/config.h.in @@ -0,0 +1,12 @@ +#ifndef INCLUDED_REFCOUNT_CONFIG_H +#define INCLUDED_REFCOUNT_CONFIG_H + +#cmakedefine REFCOUNT_HAS_THREADS + +#if defined(REFCOUNT_HAS_THREADS) \ + && (!defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L) +# error \ + "RefCount needs to be compiled with at least C11 for thread support to work." +#endif + +#endif diff --git a/include/refcount/list.h b/include/refcount/list.h index 35efc88..076a9bc 100644 --- a/include/refcount/list.h +++ b/include/refcount/list.h @@ -5,6 +5,7 @@ #define INCLUDED_REFCOUNT_LIST_H #include +#include #include #include // for NULL diff --git a/include/refcount/refcount.h b/include/refcount/refcount.h index 799d341..b010784 100644 --- a/include/refcount/refcount.h +++ b/include/refcount/refcount.h @@ -5,10 +5,16 @@ #define INCLUDED_REFCOUNT_REFCOUNT_H #include +#include #include #include #include +#ifdef REFCOUNT_HAS_THREADS +# include +# include +#endif + #ifdef __cplusplus extern "C" { #endif @@ -57,29 +63,12 @@ typedef struct RefcountContext { RefcountList *static_objects; //!< List of static objects registered with //!< this context. RefcountList *gc_roots; //!< List of garbage collector roots. +#ifdef REFCOUNT_HAS_THREADS + mtx_t so_mtx; //impl.counted.ref_count; } -void refcount_context_init_obj(const RefcountContext *ctx, void *obj); +bool refcount_context_init_obj(const RefcountContext *ctx, void *obj); bool refcount_context_init_static(RefcountContext *ctx, void *obj); @@ -199,19 +191,11 @@ RefcountWeakref *refcount_context_make_weakref(const RefcountContext *ctx, void refcount_context_destroy_weakref(const RefcountContext *ctx, RefcountWeakref *wr); -/** - * Return weather the object referenced by a weak reference still exists. - * @param ctx The #RefcountContext - * @param wr The weak reference - * @return Weather the reference is still valid - */ -static inline bool refcount_context_weakref_is_valid(const RefcountContext *ctx, - RefcountWeakref *wr) { - return wr->data; -} +bool refcount_context_weakref_is_valid(const RefcountContext *ctx, + RefcountWeakref *wr); void *refcount_context_ref_weakref(const RefcountContext *ctx, - const RefcountWeakref *wr); + RefcountWeakref *wr); /** * Convenience function that calls #refcount_context_ref_weakref followed by @@ -238,9 +222,6 @@ static inline void *refcount_context_strengthen(const RefcountContext *ctx, static inline RefcountWeakref *refcount_context_weaken(RefcountContext *ctx, void *obj) { RefcountWeakref *wr = refcount_context_make_weakref(ctx, obj); - if (!wr) { - return NULL; - } refcount_context_unref(ctx, obj); return wr; } diff --git a/src/refcount.c b/src/refcount.c index 108a947..22d2354 100644 --- a/src/refcount.c +++ b/src/refcount.c @@ -10,22 +10,31 @@ #include /** - * Callback to be executed when a program error occurred. A sane to default is - * to print a message and then abort. This is not used to report normal - * recoverable errors, but rather to report fatal logic errors. + * Internal magic macro. */ -refcount_debug_callback_t refcount_debug_breakpoint = NULL; +#define ENTRY (REFCOUNT_OBJECT_ENTRY(ctx, obj)) -/** - * Call the breakpoint handler if one is installed. - * @param oper The failing operation - * @param ctx The #RefcountContext - * @param obj The object on which the operation tried to act - */ -#define BREAKPOINT(oper, ctx, obj) \ - if (refcount_debug_breakpoint) { \ - refcount_debug_breakpoint(REFCOUNT_OPER_##oper, (ctx), (obj)); \ - } +// Some utility macros for threads +#ifdef REFCOUNT_HAS_THREADS +# define lock_mutex(m) (mtx_lock(m) == thrd_success) +# define unlock_mutex(m) (mtx_unlock(m)) +# define store_wr_count(obj, c) \ + atomic_store_explicit(obj, (c), memory_order_relaxed) +# define inc_wr_count(obj) \ + (atomic_fetch_add_explicit(obj, 1, memory_order_relaxed)) +# define dec_wr_count(obj) \ + (atomic_fetch_sub_explicit(obj, 1, memory_order_relaxed)) +# define lock_entry_mtx(obj) (lock_mutex(&obj->weak_ref->mtx)) +# define unlock_entry_mtx(obj) (unlock_mutex(&obj->weak_ref->mtx)) +#else +# define lock_mutex(m) true +# define unlock_mutex(m) true +# define store_wr_count(obj, c) (*(obj) = (c)) +# define inc_wr_count(obj) ((*(obj))++) +# define dec_wr_count(obj) ((*(obj))--) +# define lock_entry_mtx(obj) true +# define unlock_entry_mtx(obj) true +#endif /** * Default context to use for functions that do not take a context. You must @@ -56,6 +65,17 @@ refcount_make_context(size_t entry_offset, if (!ctx) { return NULL; } +#ifdef REFCOUNT_HAS_THREADS + if (mtx_init(&ctx->so_mtx, mtx_plain) != thrd_success) { + refcount_free(alloc, ctx); + return NULL; + } + if (mtx_init(&ctx->gr_mtx, mtx_recursive) != thrd_success) { + refcount_free(alloc, ctx); + mtx_destroy(&ctx->so_mtx); + return NULL; + } +#endif ctx->entry_offset = entry_offset; ctx->held_refs_callback = held_refs_callback; ctx->destroy_callback = destroy_callback; @@ -79,28 +99,55 @@ void refcount_context_destroy(RefcountContext *ctx) { ctx->static_objects, refcount_context_deinit_static_as_callback, ctx, &ctx->alloc); +#ifdef REFCOUNT_HAS_THREADS + mtx_destroy(&ctx->so_mtx); + mtx_destroy(&ctx->gr_mtx); +#endif + refcount_context_garbage_collect(ctx); refcount_free(&ctx->alloc, ctx); } /** - * Internal magic macro. + * Initialize the weak_ref field on an object. + * @param ctx The #RefcountContext + * @param obj The object + * @return True on success */ -#define ENTRY (REFCOUNT_OBJECT_ENTRY(ctx, obj)) +static bool init_obj_weakref(const RefcountContext *ctx, void *obj) { + ENTRY->weak_ref = refcount_malloc(&ctx->alloc, sizeof(RefcountWeakref)); + if (!ENTRY->weak_ref) { + return false; + } + ENTRY->weak_ref->data = obj; +#ifdef REFCOUNT_HAS_THREADS + if (mtx_init(&ENTRY->weak_ref->mtx, mtx_recursive) != thrd_success) { + refcount_free(&ctx->alloc, ENTRY->weak_ref); + return false; + } +#endif + store_wr_count(&ENTRY->weak_ref->ref_count, 1); + return true; +} /** * Initialize the #RefcountEntry member of an object. * @param ctx The #RefcountContext * @param obj The object to initialize + * @return True on success, false on failure. Note that you don't have to do + * anything special to clean up the #RefcountEntry structure on failure and it + * is safe to call this a second time on the same object (though it will + * probably fail for the same reason). */ -void refcount_context_init_obj(const RefcountContext *ctx, void *obj) { +bool refcount_context_init_obj(const RefcountContext *ctx, void *obj) { if (obj) { ENTRY->is_static = false; ENTRY->impl.counted.gc_root = NULL; ENTRY->impl.counted.ref_count = 0; - ENTRY->weak_refs = NULL; + init_obj_weakref(ctx, obj); } + return true; } /** @@ -110,15 +157,25 @@ void refcount_context_init_obj(const RefcountContext *ctx, void *obj) { * @return True on success, false otherwise */ bool refcount_context_init_static(RefcountContext *ctx, void *obj) { - ENTRY->is_static = true; - ctx->static_objects = - refcount_list_push_full(ctx->static_objects, obj, &ctx->alloc); - if (!ctx->static_objects) { + if (!lock_mutex(&ctx->so_mtx)) { return false; } + bool success = false; + ENTRY->is_static = true; + if (!init_obj_weakref(ctx, obj)) { + goto end; + } + RefcountList *new_static_objects = + refcount_list_push_full(ctx->static_objects, obj, &ctx->alloc); + if (!new_static_objects) { + goto end; + } + ctx->static_objects = new_static_objects; ENTRY->impl.static_entry = ctx->static_objects; - ENTRY->weak_refs = NULL; - return true; + success = true; +end: + unlock_mutex(&ctx->so_mtx); + return success; } /** @@ -137,24 +194,18 @@ static inline bool obj_held_refs(RefcountContext *ctx, void *obj, } /** - * Call back used from #clear_weak_references. - * @param wr_raw The #RefcountWeakref object - */ -static void clear_weak_reference_callback(void *wr_raw) { - RefcountWeakref *wr = wr_raw; - wr->entry = NULL; - wr->data = NULL; -} - -/** - * Clear all weak references of an object without freeing them. + * Remove a reference from a weakref, possibly freeing it if it's reference + * count falls to zero. * @param ctx The #RefcountContext - * @param obj The object to clear + * @param wr The weak reference */ -static inline void clear_weak_references(const RefcountContext *ctx, - void *obj) { - refcount_list_free_full(ENTRY->weak_refs, clear_weak_reference_callback, - &ctx->alloc); +static void unref_weakref(const RefcountContext *ctx, RefcountWeakref *wr) { + if (dec_wr_count(&wr->ref_count) == 1) { +#ifdef REFCOUNT_HAS_THREADS + mtx_destroy(&wr->mtx); +#endif + refcount_free(&ctx->alloc, wr); + } } /** @@ -164,21 +215,32 @@ static inline void clear_weak_references(const RefcountContext *ctx, * @return True on success, false otherwise */ bool refcount_context_deinit_static(RefcountContext *ctx, void *obj) { - if (refcount_context_is_static(ctx, obj)) { - RefcountList *held_refs = NULL; - if (!obj_held_refs(ctx, obj, &held_refs)) { - return false; - } - clear_weak_references(ctx, obj); - ctx->static_objects = refcount_list_remove_full( - ctx->static_objects, ENTRY->impl.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) + if (!lock_mutex(&ctx->so_mtx)) { return false; } + bool success = false; + if (!refcount_context_is_static(ctx, obj)) { + goto end; + } + RefcountList *held_refs = NULL; + if (!obj_held_refs(ctx, obj, &held_refs)) { + goto end; + } + if (!lock_entry_mtx(ENTRY)) { + refcount_list_free_full(held_refs, NULL, &ctx->alloc); + goto end; + } + ENTRY->weak_ref->data = NULL; + unref_weakref(ctx, ENTRY->weak_ref); + unlock_entry_mtx(ENTRY); + ctx->static_objects = refcount_list_remove_full( + ctx->static_objects, ENTRY->impl.static_entry, NULL, &ctx->alloc); + refcount_list_free_with_data_full( + held_refs, refcount_context_unref_as_callback, ctx, &ctx->alloc); + success = true; +end: + unlock_mutex(&ctx->so_mtx); + return success; } /** @@ -190,9 +252,14 @@ bool refcount_context_deinit_static(RefcountContext *ctx, void *obj) { void *refcount_context_ref(const RefcountContext *ctx, void *obj) { if (!obj) { return NULL; - } else if (!ENTRY->is_static) { + } + if (!lock_entry_mtx(ENTRY)) { + return obj; + } + if (!ENTRY->is_static) { ++ENTRY->impl.counted.ref_count; } + unlock_entry_mtx(ENTRY); return obj; } @@ -204,15 +271,22 @@ void *refcount_context_ref(const RefcountContext *ctx, void *obj) { * @return True on success */ static bool track_gc_root(RefcountContext *ctx, void *obj) { + if (!lock_mutex(&ctx->gr_mtx)) { + return false; + } + bool success = false; if (!ENTRY->impl.counted.gc_root) { ctx->gc_roots = refcount_list_push_full(ctx->gc_roots, obj, &ctx->alloc); if (!ctx->gc_roots) { - return false; + goto end; } ENTRY->impl.counted.gc_root = ctx->gc_roots; } - return true; + success = true; +end: + unlock_mutex(&ctx->gr_mtx); + return success; } /** @@ -222,9 +296,12 @@ static bool 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_full( - ctx->gc_roots, ENTRY->impl.counted.gc_root, NULL, &ctx->alloc); - ENTRY->impl.counted.gc_root = NULL; + if (lock_mutex(&ctx->gr_mtx)) { + ctx->gc_roots = refcount_list_remove_full( + ctx->gc_roots, ENTRY->impl.counted.gc_root, NULL, &ctx->alloc); + ENTRY->impl.counted.gc_root = NULL; + unlock_mutex(&ctx->gr_mtx); + } } /** @@ -246,13 +323,22 @@ static void *unref_to_queue(RefcountContext *ctx, void *obj, return obj; } else if (obj == toplevel) { return NULL; - } else if (ENTRY->impl.counted.ref_count <= 1) { - *queue = refcount_list_push_full(*queue, obj, &ctx->alloc); - return NULL; } - --ENTRY->impl.counted.ref_count; - track_gc_root(ctx, obj); - return obj; + if (!lock_entry_mtx(ENTRY)) { + // if this fails, we prefer a memory leak to causing undefined behavior + // and possibly crashing + return obj; + } + if (ENTRY->impl.counted.ref_count <= 1) { + *queue = refcount_list_push_full(*queue, obj, &ctx->alloc); + unlock_entry_mtx(ENTRY); + return NULL; + } else { + --ENTRY->impl.counted.ref_count; + track_gc_root(ctx, obj); + unlock_entry_mtx(ENTRY); + return obj; + } } /** @@ -282,8 +368,13 @@ static void unref_to_queue_as_callback(void *obj, void *ctx_and_queue_raw) { * @param obj The object to destroy */ static inline void destroy_object(RefcountContext *ctx, void *obj) { - clear_weak_references(ctx, obj); + if (!lock_entry_mtx(ENTRY)) { + return; + } remove_gc_root(ctx, obj); + ENTRY->weak_ref->data = NULL; + unlock_entry_mtx(ENTRY); + unref_weakref(ctx, ENTRY->weak_ref); if (ctx->destroy_callback) { ctx->destroy_callback(obj, ctx->user_data); } @@ -351,15 +442,16 @@ void *refcount_context_float(RefcountContext *ctx, void *obj) { return NULL; } else if (ENTRY->is_static) { return obj; - } else if (!ENTRY->impl.counted.ref_count) { - BREAKPOINT(FLOAT, ctx, obj); - return obj; + } + if (!lock_entry_mtx(ENTRY)) { + return NULL; } if (--ENTRY->impl.counted.ref_count) { track_gc_root(ctx, obj); } else { remove_gc_root(ctx, obj); } + unlock_entry_mtx(ENTRY); return obj; } @@ -475,6 +567,9 @@ ptrdiff_t refcount_context_garbage_collect(RefcountContext *ctx) { // no loops possible return 0; } + if (!lock_mutex(&ctx->gr_mtx)) { + return -1; + } ptrdiff_t total_cleared = 0; RefcountList *root = ctx->gc_roots; while (root) { @@ -484,6 +579,7 @@ ptrdiff_t refcount_context_garbage_collect(RefcountContext *ctx) { } total_cleared += res; } + unlock_mutex(&ctx->gr_mtx); return total_cleared; } @@ -495,25 +591,12 @@ ptrdiff_t refcount_context_garbage_collect(RefcountContext *ctx) { * NULL to indicate that the referenced object is no longer in existence. * @param ctx The #RefcountContext * @param obj The object for which to create a weak reference - * @return The newly created weak reference, or NULL if an allocated error - * occurred. + * @return The newly created weak reference */ RefcountWeakref *refcount_context_make_weakref(const RefcountContext *ctx, void *obj) { - RefcountWeakref *wr = refcount_malloc(&ctx->alloc, sizeof(RefcountWeakref)); - if (!wr) { - return NULL; - } - RefcountList *new_weak_refs = - refcount_list_push_full(ENTRY->weak_refs, wr, &ctx->alloc); - if (!new_weak_refs) { - refcount_free(&ctx->alloc, wr); - return NULL; - } - ENTRY->weak_refs = new_weak_refs; - wr->entry = new_weak_refs; - wr->data = obj; - return wr; + inc_wr_count(&ENTRY->weak_ref->ref_count); + return ENTRY->weak_ref; } /** @@ -524,12 +607,24 @@ RefcountWeakref *refcount_context_make_weakref(const RefcountContext *ctx, */ void refcount_context_destroy_weakref(const RefcountContext *ctx, RefcountWeakref *wr) { - if (wr->data) { - void *obj = wr->data; - ENTRY->weak_refs = refcount_list_remove_full( - ENTRY->weak_refs, wr->entry, NULL, &ctx->alloc); + unref_weakref(ctx, wr); +} + +/** + * Return weather the object referenced by a weak reference still exists. + * @param ctx The #RefcountContext + * @param wr The weak reference + * @return Weather the reference is still valid + */ +bool refcount_context_weakref_is_valid(const RefcountContext *ctx, + RefcountWeakref *wr) { + // we need the locks because accessing the data member is not atomic + if (!lock_mutex(&wr->mtx)) { + return NULL; // we can't be sure, so play it safe } - refcount_free(&ctx->alloc, wr); + bool is_valid = wr->data; + unlock_mutex(&wr->mtx); + return is_valid; } /** @@ -540,9 +635,14 @@ void refcount_context_destroy_weakref(const RefcountContext *ctx, * @return The newly referenced object, or NULL */ void *refcount_context_ref_weakref(const RefcountContext *ctx, - const RefcountWeakref *wr) { - if (!wr->data) { - return NULL; + RefcountWeakref *wr) { + if (!lock_mutex(&wr->mtx)) { + return NULL; // we can't be sure, so play it safe } - return refcount_context_ref(ctx, wr->data); + void *obj = NULL; + if (wr->data) { + obj = refcount_context_ref(ctx, wr->data); + } + unlock_mutex(&wr->mtx); + return obj; } diff --git a/test/test_refcount.c b/test/test_refcount.c index 559f616..104f8ae 100644 --- a/test/test_refcount.c +++ b/test/test_refcount.c @@ -1,3 +1,4 @@ +#undef NDEBUG #include "alloc.h" #include @@ -130,7 +131,6 @@ int main(int argc, const char **argv) { RefcountWeakref *x = refcount_context_weaken(c, a); w = refcount_context_make_weakref(c, a); assert(refcount_context_num_refs(c, a) == 1); - assert(w != x); refcount_context_destroy_weakref(c, x); refcount_context_ref(c, a); @@ -138,7 +138,6 @@ int main(int argc, const char **argv) { x = refcount_context_weaken(c, a); assert(refcount_context_num_refs(c, a) == 1); - assert(w != x); assert(refcount_context_weakref_is_valid(c, w)); assert(refcount_context_weakref_is_valid(c, x));