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

@ -131,6 +131,44 @@ static bool init_obj_weakref(const RefcountContext *ctx, void *obj) {
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.
* @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);
}