diff --git a/context.c b/context.c index 6ae87de..e6c6335 100644 --- a/context.c +++ b/context.c @@ -365,18 +365,30 @@ static zend_object *context_object_create(zend_class_entry *class_entry) return &context->std; } -static void context_object_destroy(zend_object *object) +static void context_free(zend_object *object) { async_context_t *context = ZEND_OBJECT_TO_CONTEXT(object); - // Destroy hash tables zend_hash_destroy(&context->values); zend_hash_destroy(&context->keys); + zend_object_std_dtor(object); } -static void context_free(zend_object *object) +static HashTable *context_get_gc(zend_object *object, zval **table, int *num) { - zend_object_std_dtor(object); + async_context_t *context = ZEND_OBJECT_TO_CONTEXT(object); + zend_get_gc_buffer *buf = zend_get_gc_buffer_create(); + zval *value; + + ZEND_HASH_FOREACH_VAL(&context->values, value) { + zend_get_gc_buffer_add_zval(buf, value); + } ZEND_HASH_FOREACH_END(); + + ZEND_HASH_FOREACH_VAL(&context->keys, value) { + zend_get_gc_buffer_add_zval(buf, value); + } ZEND_HASH_FOREACH_END(); + zend_get_gc_buffer_use(buf, table, num); + return NULL; } void async_register_context_ce(void) @@ -391,8 +403,8 @@ void async_register_context_ce(void) context_handlers = std_object_handlers; context_handlers.offset = offsetof(async_context_t, std); context_handlers.clone_obj = NULL; - context_handlers.dtor_obj = context_object_destroy; context_handlers.free_obj = context_free; + context_handlers.get_gc = context_get_gc; async_ce_context->default_object_handlers = &context_handlers; -} \ No newline at end of file +} diff --git a/coroutine.c b/coroutine.c index fbad520..c8e1668 100644 --- a/coroutine.c +++ b/coroutine.c @@ -329,25 +329,10 @@ static HashTable *async_coroutine_object_gc(zend_object *object, zval **table, i } } - /* Add context ZVALs if present */ + /* The coroutine owns the Context object, which exposes its values and keys to GC. */ if (coroutine->coroutine.context) { - /* Cast to actual context implementation to access HashTables */ async_context_t *context = (async_context_t *) coroutine->coroutine.context; - - /* Add all values from context->values HashTable */ - zval *val; - ZEND_HASH_FOREACH_VAL(&context->values, val) - { - zend_get_gc_buffer_add_zval(buf, val); - } - ZEND_HASH_FOREACH_END(); - - /* Add all object keys from context->keys HashTable */ - ZEND_HASH_FOREACH_VAL(&context->keys, val) - { - zend_get_gc_buffer_add_zval(buf, val); - } - ZEND_HASH_FOREACH_END(); + zend_get_gc_buffer_add_obj(buf, &context->std); } async_fiber_context_t *fiber_context = coroutine->fiber_context; diff --git a/scope.c b/scope.c index ecefbaa..a40b503 100644 --- a/scope.c +++ b/scope.c @@ -1448,25 +1448,10 @@ static HashTable *scope_object_gc(zend_object *object, zval **table, int *num) ZEND_HASH_FOREACH_END(); } - /* Add context ZVALs if present */ + /* The scope owns the Context object, which exposes its values and keys to GC. */ if (scope->scope.context) { - /* Cast to actual context implementation to access HashTables */ async_context_t *context = (async_context_t *) scope->scope.context; - - /* Add all values from context->values HashTable */ - zval *val; - ZEND_HASH_FOREACH_VAL(&context->values, val) - { - zend_get_gc_buffer_add_zval(buf, val); - } - ZEND_HASH_FOREACH_END(); - - /* Add all object keys from context->keys HashTable */ - ZEND_HASH_FOREACH_VAL(&context->keys, val) - { - zend_get_gc_buffer_add_zval(buf, val); - } - ZEND_HASH_FOREACH_END(); + zend_get_gc_buffer_add_obj(buf, &context->std); } zend_get_gc_buffer_use(buf, table, num); @@ -1751,4 +1736,4 @@ static bool async_scope_call_finally_handlers(async_scope_t *scope) } else { return true; } -} \ No newline at end of file +} diff --git a/tests/context/014-context_cycles_gc.phpt b/tests/context/014-context_cycles_gc.phpt new file mode 100644 index 0000000..6486625 --- /dev/null +++ b/tests/context/014-context_cycles_gc.phpt @@ -0,0 +1,61 @@ +--TEST-- +Context values and object keys are visible to normal cycle collection +--FILE-- +set($context, 'value'); + } else { + $context->set('key', $context); + } + + return WeakReference::create($context); +} + +$valueCycle = abandonContext(false); +$keyCycle = abandonContext(true); + +// Fill the GC root buffer and let the collector run on its normal schedule. +$threshold = gc_status()['threshold']; +for ($i = 0; $i < $threshold + 64; $i++) { + abandonContext(($i & 1) !== 0); +} + +var_dump($valueCycle->get() === null); +var_dump($keyCycle->get() === null); + +$coroutineContext = null; +Async\await(Async\spawn(function () use (&$coroutineContext): void { + $context = Async\coroutine_context(); + $coroutineContext = WeakReference::create($context); + $context->set('self', $context); +})); + +gc_collect_cycles(); +Async\delay(1); +var_dump($coroutineContext->get() === null); + +$scope = new Async\Scope(); +$scopeContext = null; +Async\await($scope->spawn(function () use (&$scopeContext): void { + $context = Async\current_context(); + $scopeContext = WeakReference::create($context); + $context->set($context, 'self'); +})); +$scope->dispose(); +unset($scope); + +gc_collect_cycles(); +Async\delay(1); +var_dump($scopeContext->get() === null); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true)