Add destructors

This commit is contained in:
2025-09-07 05:16:23 -07:00
parent 97d1b4a701
commit d088bc11d6
4 changed files with 230 additions and 12 deletions

View File

@ -4,6 +4,12 @@ set(REFCOUNT_USE_THREADS
ON ON
CACHE BOOL "Weather or not RefCount should be thread aware.") 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) if(REFCOUNT_USE_THREADS)
set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD 11)
else() else()
@ -22,7 +28,7 @@ include(FetchContent)
FetchContent_Declare( FetchContent_Declare(
ht ht
GIT_REPOSITORY https://git.zander.im/Zander671/ht.git GIT_REPOSITORY https://git.zander.im/Zander671/ht.git
GIT_TAG 9a1b271cfdc8ab203f9d6aa6a83cc4523de422be) GIT_TAG c6bdb38bb77d45f9d5083706723c84c37db56c9c)
FetchContent_MakeAvailable(ht) FetchContent_MakeAvailable(ht)
@ -30,8 +36,8 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
include(CTest) include(CTest)
endif() endif()
# add_compile_options(-fsanitize=address,leak,undefined) add_compile_options(-fsanitize=address,leak,undefined)
# add_link_options(-fsanitize=address,leak,undefined) add_link_options(-fsanitize=address,leak,undefined)
configure_file(include/refcount/config.h.in include/refcount/config.h @ONLY) configure_file(include/refcount/config.h.in include/refcount/config.h @ONLY)

View File

@ -80,8 +80,8 @@ refcount_make_context(size_t entry_offset,
void refcount_context_destroy(RefcountContext *ctx); void refcount_context_destroy(RefcountContext *ctx);
/** /**
* Structure holding the required information for a weak reference. That is, a * Opaque structure holding the required information for a weak reference. That
* reference that does not prevent an object from being de-allocated. * is, a reference that does not prevent an object from being de-allocated.
*/ */
typedef struct RefcountWeakref { typedef struct RefcountWeakref {
#ifdef REFCOUNT_HAS_THREADS #ifdef REFCOUNT_HAS_THREADS
@ -94,8 +94,18 @@ typedef struct RefcountWeakref {
} RefcountWeakref; } RefcountWeakref;
/** /**
* Opaque struct holding reference data for an object. This should be included * Destructor callback type. Called before and object is freed after having its
* as a non-pointer member of the struct you are trying to track, e.g. * 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 * @code
* struct A { * struct A {
* int my_num; * int my_num;
@ -106,7 +116,8 @@ typedef struct RefcountWeakref {
*/ */
typedef struct RefcountEntry { typedef struct RefcountEntry {
bool is_static; //!< Whether the object is static. bool is_static; //!< Whether the object is static.
RefcountWeakref *weak_ref; //<! Weakref for this entry. RefcountWeakref *weak_ref; //!< Weakref for this entry.
HTTable *destructors; //!< Hash table of object destructors.
union { union {
struct { struct {
uint64_t ref_count; //!< The object's reference count. uint64_t ref_count; //!< The object's reference count.
@ -226,6 +237,14 @@ static inline RefcountWeakref *refcount_context_weaken(RefcountContext *ctx,
return wr; return wr;
} }
bool refcount_context_add_destructor(const RefcountContext *ctx, void *obj,
void *key,
refcount_destructor_callback_t callback,
void *user_data);
bool refcount_context_remove_destructor(const RefcountContext *ctx, void *obj,
void *key);
/** /**
* Same as #refcount_context_is_static, but only operates on the global * Same as #refcount_context_is_static, but only operates on the global
* context. * context.
@ -367,7 +386,7 @@ static inline void *refcount_strengthen(RefcountWeakref *wr) {
} }
/** /**
* Same as #refcount_context_weaken, but operates ojn the global context. * Same as #refcount_context_weaken, but operates on the global context.
* @param obj The object to create a weak reference for then unref * @param obj The object to create a weak reference for then unref
* @return The newly created weak reference, or NULL if an allocation error * @return The newly created weak reference, or NULL if an allocation error
* occurred. In this case the object is not unrefed. * occurred. In this case the object is not unrefed.
@ -376,6 +395,35 @@ static inline RefcountWeakref *refcount_weaken(void *obj) {
return refcount_context_weaken(refcount_default_context, obj); return refcount_context_weaken(refcount_default_context, obj);
} }
/**
* Same as #refcount_context_add_destructor, but operates on the global context.
* @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.
*/
static inline bool
refcount_add_destructor(void *obj, void *key,
refcount_destructor_callback_t callback,
void *user_data) {
return refcount_context_add_destructor(refcount_default_context, obj, key,
callback, user_data);
}
/**
* Same as #refcount_context_remove_destructor, but operates on the global
* context.
* @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.
*/
static inline bool refcount_remove_destructor(void *obj, void *key) {
return refcount_context_remove_destructor(refcount_default_context, obj,
key);
}
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -131,6 +131,44 @@ static bool init_obj_weakref(const RefcountContext *ctx, void *obj) {
return true; return true;
} }
/**
* @struct DestructorEntry
* Hash table entry used to hold descructor callback information.
*/
struct DestructorEntry {
refcount_destructor_callback_t callback; //!< The callback itself.
void *user_data; //!< User specified data to pass to the callback.
};
/**
* Free a #DestructorEntry using the given context.
* @param entry The entry to free
* @param ctx_raw The #RefcountContext
*/
static void free_destructor_entry_callback(void *entry, void *ctx_raw) {
const RefcountContext *ctx = ctx_raw;
refcount_free(&ctx->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. * Initialize the #RefcountEntry member of an object.
* @param ctx The #RefcountContext * @param ctx The #RefcountContext
@ -145,7 +183,13 @@ bool refcount_context_init_obj(const RefcountContext *ctx, void *obj) {
ENTRY->is_static = false; ENTRY->is_static = false;
ENTRY->impl.counted.gc_root = NULL; ENTRY->impl.counted.gc_root = NULL;
ENTRY->impl.counted.ref_count = 0; 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; return true;
} }
@ -162,7 +206,11 @@ bool refcount_context_init_static(RefcountContext *ctx, void *obj) {
} }
bool success = false; bool success = false;
ENTRY->is_static = true; ENTRY->is_static = true;
if (!init_obj_destructor_table(ctx, obj)) {
goto end;
}
if (!init_obj_weakref(ctx, obj)) { if (!init_obj_weakref(ctx, obj)) {
refcount_free(&ctx->alloc, ENTRY->destructors);
goto end; goto end;
} }
RefcountList *new_static_objects = 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. * Unregister a static object in a context.
* @param ctx The #RefcountContext * @param ctx The #RefcountContext
@ -231,6 +313,8 @@ bool refcount_context_deinit_static(RefcountContext *ctx, void *obj) {
goto end; goto end;
} }
ENTRY->weak_ref->data = NULL; ENTRY->weak_ref->data = NULL;
call_object_destructors(ctx, obj);
ht_free(ENTRY->destructors);
unref_weakref(ctx, ENTRY->weak_ref); unref_weakref(ctx, ENTRY->weak_ref);
unlock_entry_mtx(ENTRY); unlock_entry_mtx(ENTRY);
ctx->static_objects = refcount_list_remove_full( ctx->static_objects = refcount_list_remove_full(
@ -330,7 +414,13 @@ static void *unref_to_queue(RefcountContext *ctx, void *obj,
return obj; return obj;
} }
if (ENTRY->impl.counted.ref_count <= 1) { 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); unlock_entry_mtx(ENTRY);
return NULL; return NULL;
} else { } else {
@ -373,6 +463,7 @@ static inline void destroy_object(RefcountContext *ctx, void *obj) {
} }
remove_gc_root(ctx, obj); remove_gc_root(ctx, obj);
ENTRY->weak_ref->data = NULL; ENTRY->weak_ref->data = NULL;
ht_free(ENTRY->destructors);
unlock_entry_mtx(ENTRY); unlock_entry_mtx(ENTRY);
unref_weakref(ctx, ENTRY->weak_ref); unref_weakref(ctx, ENTRY->weak_ref);
if (ctx->destroy_callback) { 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 ctx The #RefcountContext
* @param queue The queue * @param queue The queue
* @param toplevel Toplevel object that triggered the unref * @param toplevel Toplevel object that triggered the unref
@ -646,3 +737,50 @@ void *refcount_context_ref_weakref(const RefcountContext *ctx,
unlock_mutex(&wr->mtx); unlock_mutex(&wr->mtx);
return obj; 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);
}

View File

@ -32,11 +32,23 @@ void destroy_callback(void *a_raw, void *ignored) {
counting_free(a); 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) { int main(int argc, const char **argv) {
RefcountContext *c = RefcountContext *c =
refcount_make_context(offsetof(A, refcount), held_refs_callback, refcount_make_context(offsetof(A, refcount), held_refs_callback,
destroy_callback, NULL, &COUNTING_ALLOCATOR); 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"); A *a = make_a(c, 10, "Hello world\n");
assert(!refcount_context_is_static(c, a)); assert(!refcount_context_is_static(c, a));
assert(refcount_context_num_refs(c, a) == 0); 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, w);
refcount_context_destroy_weakref(c, x); 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); refcount_context_destroy(c);
check_allocator_status(); check_allocator_status();