Make sure that destructors are called when objects are garbage collected

This commit is contained in:
2025-09-07 15:51:36 -07:00
parent 14fdfbce85
commit b40fcea2c2
2 changed files with 69 additions and 3 deletions

View File

@ -588,6 +588,60 @@ static bool free_roots_foreach(void *obj, void *ignored, void *user_data) {
return false;
}
/**
* @struct ContexAndFlag
* #RefcountContext and a boolean flag.
*/
struct ContextAndFlag {
const RefcountContext *ctx; //!< The context.
bool flag; //!< The flag.
};
/**
* Call the destructors for an object. Used from #call_destructors_for_gc.
* @param obj The object
* @param ignored Ignored
* @param ctx_raw A #ContexAndFlag. The flag is set to the same value as the
* return value.
* @return True if the object's reference count increased above 0
*/
static bool call_destructors_for_gc_callback(void *obj, void *ignored,
void *ctx_and_flag_raw) {
struct ContextAndFlag *ctx_and_flag = ctx_and_flag_raw;
const RefcountContext *ctx = ctx_and_flag->ctx;
uint64_t old_ref_count = ENTRY->impl.counted.ref_count;
ENTRY->impl.counted.ref_count = 0;
call_object_destructors(ctx, obj);
ctx_and_flag->flag = ENTRY->impl.counted.ref_count;
if (!ENTRY->impl.counted.ref_count) {
// the object can still be saved by another object in the reference loop
// having its refcount incremented. Therefore, we restore the original
// reference count.
ENTRY->impl.counted.ref_count = old_ref_count;
}
return ctx_and_flag->flag;
}
/**
* Call destructors for a hash table where the keys are objects. Stop if any
* object had it's reference count increase past 0.
*
* > Calling this will set the reference count of all objects in counts to 0!
*
* @param ctx The #RefcountContext
* @param counts The table for which to call destructors
* @return True if an object had its reference count increase above 0.
*/
static bool call_destructors_for_gc(const RefcountContext *ctx,
HTTable *counts) {
struct ContextAndFlag ctx_and_flag = {
.ctx = ctx,
.flag = false,
};
ht_foreach(counts, call_destructors_for_gc_callback, &ctx_and_flag);
return ctx_and_flag.flag;
}
/**
* 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.
@ -634,18 +688,25 @@ static ptrdiff_t check_gc_root(RefcountContext *ctx, RefcountList **root_ptr) {
obj_held_refs(ctx, obj, &queue);
}
}
if (seen_objects == clear_objects) {
ptrdiff_t freed_count = 0;
if (seen_objects == clear_objects
&& !call_destructors_for_gc(ctx, counts)) {
// all objects still have a refcount of zero, even after calling
// destructors, proceed with freeing them
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;
}
freed_count = seen_objects;
} else {
// either something still had a reference, or it was re-refed by its
// destructor. Either way, don't free anything
*root_ptr = (*root_ptr)->next;
}
ht_free(counts);
return seen_objects == clear_objects ? seen_objects : 0;
return freed_count;
}
/**

View File

@ -114,6 +114,9 @@ int main(int argc, const char **argv) {
refcount_context_ref(c, a);
refcount_context_unref(c, a);
assert(refcount_context_num_refs(c, a) == 1);
int key;
A *a_with_destructor = a;
assert(refcount_context_add_destructor(c, a, &key, reref_destructor, c));
a = NULL;
for (char i = 'a'; i <= 'z'; ++i) {
@ -124,6 +127,9 @@ int main(int argc, const char **argv) {
assert(refcount_context_num_refs(c, a) == 0);
refcount_context_unref(c, a);
assert(refcount_context_garbage_collect(c) == 0);
assert(refcount_context_num_refs(c, a_with_destructor) == 1);
assert(refcount_context_remove_destructor(c, a_with_destructor, &key));
assert(refcount_context_garbage_collect(c) == 26);
a = make_a(c, 0, "a");
@ -162,7 +168,6 @@ int main(int argc, const char **argv) {
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));