Skip to content

Commit 07fcd93

Browse files
authored
Merge pull request #10 from true-async/6-proper-handling-of-internal-references-to-zvals-for-coroutines-and-scopes
6 proper handling of internal references to zvals for coroutines and scopes
2 parents 21dae99 + d860c8a commit 07fcd93

15 files changed

+615
-8
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.3.0] - TBD
11+
12+
### Added
13+
- **Garbage Collection Support**: Implemented comprehensive GC handlers for async objects
14+
- Added `async_coroutine_object_gc()` function to track all ZVALs in coroutine structures
15+
- Added `async_scope_object_gc()` function to track ZVALs in scope structures
16+
- Proper GC tracking for context HashTables (values and keys)
17+
- GC support for finally handlers, exception handlers, and function call parameters
18+
- GC tracking for waker events, internal context, and nested async structures
19+
- Prevents memory leaks in complex async applications with circular references
20+
21+
### Fixed
22+
- Memory management improvements for long-running async applications
23+
- Proper cleanup of coroutine and scope objects during garbage collection cycles
24+
1025
## [0.2.0] - TBD
1126

1227
### Added

coroutine.c

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "scope.h"
2626
#include "zend_common.h"
2727
#include "zend_exceptions.h"
28+
#include "zend_generators.h"
2829
#include "zend_ini.h"
2930

3031
#define METHOD(name) PHP_METHOD(Async_Coroutine, name)
@@ -1182,6 +1183,138 @@ zend_coroutine_t *async_new_coroutine(zend_async_scope_t *scope)
11821183
return &coroutine->coroutine;
11831184
}
11841185

1186+
static HashTable *async_coroutine_object_gc(zend_object *object, zval **table, int *num)
1187+
{
1188+
async_coroutine_t *coroutine = (async_coroutine_t *)ZEND_ASYNC_OBJECT_TO_EVENT(object);
1189+
zend_get_gc_buffer *buf = zend_get_gc_buffer_create();
1190+
1191+
/* Always add basic ZVALs from coroutine structure */
1192+
zend_get_gc_buffer_add_zval(buf, &coroutine->coroutine.result);
1193+
1194+
/* Add objects that may be present */
1195+
if (coroutine->coroutine.exception) {
1196+
zend_get_gc_buffer_add_obj(buf, coroutine->coroutine.exception);
1197+
}
1198+
1199+
if (coroutine->deferred_cancellation) {
1200+
zend_get_gc_buffer_add_obj(buf, coroutine->deferred_cancellation);
1201+
}
1202+
1203+
/* Add finally handlers if present */
1204+
if (coroutine->finally_handlers) {
1205+
zval *val;
1206+
ZEND_HASH_FOREACH_VAL(coroutine->finally_handlers, val) {
1207+
zend_get_gc_buffer_add_zval(buf, val);
1208+
} ZEND_HASH_FOREACH_END();
1209+
}
1210+
1211+
/* Add internal context HashTable if present */
1212+
if (coroutine->coroutine.internal_context) {
1213+
zval *val;
1214+
ZEND_HASH_FOREACH_VAL(coroutine->coroutine.internal_context, val) {
1215+
zend_get_gc_buffer_add_zval(buf, val);
1216+
} ZEND_HASH_FOREACH_END();
1217+
}
1218+
1219+
/* Add fcall function name and parameters if present */
1220+
if (coroutine->coroutine.fcall) {
1221+
zend_get_gc_buffer_add_zval(buf, &coroutine->coroutine.fcall->fci.function_name);
1222+
1223+
/* Add function parameters */
1224+
if (coroutine->coroutine.fcall->fci.param_count > 0 && coroutine->coroutine.fcall->fci.params) {
1225+
for (uint32_t i = 0; i < coroutine->coroutine.fcall->fci.param_count; i++) {
1226+
zend_get_gc_buffer_add_zval(buf, &coroutine->coroutine.fcall->fci.params[i]);
1227+
}
1228+
}
1229+
}
1230+
1231+
/* Add waker-related ZVALs if present */
1232+
if (coroutine->coroutine.waker) {
1233+
zend_get_gc_buffer_add_zval(buf, &coroutine->coroutine.waker->result);
1234+
1235+
if (coroutine->coroutine.waker->error) {
1236+
zend_get_gc_buffer_add_obj(buf, coroutine->coroutine.waker->error);
1237+
}
1238+
1239+
/* Add events HashTable contents */
1240+
zval *event_val;
1241+
zval zval_object;
1242+
ZEND_HASH_FOREACH_VAL(&coroutine->coroutine.waker->events, event_val) {
1243+
1244+
zend_async_event_t *event = (zend_async_event_t *) Z_PTR_P(event_val);
1245+
1246+
if (ZEND_ASYNC_EVENT_IS_REFERENCE(event) || ZEND_ASYNC_EVENT_IS_ZEND_OBJ(event)) {
1247+
ZVAL_OBJ(&zval_object, ZEND_ASYNC_EVENT_TO_OBJECT(event));
1248+
zend_get_gc_buffer_add_zval(buf, &zval_object);
1249+
}
1250+
} ZEND_HASH_FOREACH_END();
1251+
1252+
/* Add triggered events if present */
1253+
if (coroutine->coroutine.waker->triggered_events) {
1254+
ZEND_HASH_FOREACH_VAL(coroutine->coroutine.waker->triggered_events, event_val) {
1255+
zend_get_gc_buffer_add_zval(buf, event_val);
1256+
} ZEND_HASH_FOREACH_END();
1257+
}
1258+
}
1259+
1260+
/* Add context ZVALs if present */
1261+
if (coroutine->coroutine.context) {
1262+
/* Cast to actual context implementation to access HashTables */
1263+
async_context_t *context = (async_context_t *)coroutine->coroutine.context;
1264+
1265+
/* Add all values from context->values HashTable */
1266+
zval *val;
1267+
ZEND_HASH_FOREACH_VAL(&context->values, val) {
1268+
zend_get_gc_buffer_add_zval(buf, val);
1269+
} ZEND_HASH_FOREACH_END();
1270+
1271+
/* Add all object keys from context->keys HashTable */
1272+
ZEND_HASH_FOREACH_VAL(&context->keys, val) {
1273+
zend_get_gc_buffer_add_zval(buf, val);
1274+
} ZEND_HASH_FOREACH_END();
1275+
}
1276+
1277+
/* Check if we should traverse execution stack (similar to fibers) */
1278+
if (coroutine->context.status != ZEND_FIBER_STATUS_SUSPENDED ||
1279+
!coroutine->execute_data) {
1280+
zend_get_gc_buffer_use(buf, table, num);
1281+
return NULL;
1282+
}
1283+
1284+
/* Traverse execution stack for suspended coroutines */
1285+
HashTable *lastSymTable = NULL;
1286+
zend_execute_data *ex = coroutine->execute_data;
1287+
for (; ex; ex = ex->prev_execute_data) {
1288+
HashTable *symTable;
1289+
if (ZEND_CALL_INFO(ex) & ZEND_CALL_GENERATOR) {
1290+
zend_generator *generator = (zend_generator*)ex->return_value;
1291+
if (!(generator->flags & ZEND_GENERATOR_CURRENTLY_RUNNING)) {
1292+
continue;
1293+
}
1294+
symTable = zend_generator_frame_gc(buf, generator);
1295+
} else {
1296+
symTable = zend_unfinished_execution_gc_ex(ex,
1297+
ex->func && ZEND_USER_CODE(ex->func->type) ? ex->call : NULL,
1298+
buf, false);
1299+
}
1300+
if (symTable) {
1301+
if (lastSymTable) {
1302+
zval *val;
1303+
ZEND_HASH_FOREACH_VAL(lastSymTable, val) {
1304+
if (EXPECTED(Z_TYPE_P(val) == IS_INDIRECT)) {
1305+
val = Z_INDIRECT_P(val);
1306+
}
1307+
zend_get_gc_buffer_add_zval(buf, val);
1308+
} ZEND_HASH_FOREACH_END();
1309+
}
1310+
lastSymTable = symTable;
1311+
}
1312+
}
1313+
1314+
zend_get_gc_buffer_use(buf, table, num);
1315+
return lastSymTable;
1316+
}
1317+
11851318
static zend_object_handlers coroutine_handlers;
11861319

11871320
void async_register_coroutine_ce(void)
@@ -1197,6 +1330,7 @@ void async_register_coroutine_ce(void)
11971330
coroutine_handlers.clone_obj = NULL;
11981331
coroutine_handlers.dtor_obj = coroutine_object_destroy;
11991332
coroutine_handlers.free_obj = coroutine_free;
1333+
coroutine_handlers.get_gc = async_coroutine_object_gc;
12001334
}
12011335

12021336
//////////////////////////////////////////////////////////////////////

scope.c

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,84 @@ static void scope_destroy(zend_object *object)
12291229
}
12301230
}
12311231

1232+
static HashTable *scope_object_gc(zend_object *object, zval **table, int *num)
1233+
{
1234+
async_scope_object_t *scope_obj = (async_scope_object_t *)object;
1235+
async_scope_t *scope = scope_obj->scope;
1236+
1237+
if (scope == NULL) {
1238+
*table = NULL;
1239+
*num = 0;
1240+
return NULL; // No scope to collect
1241+
}
1242+
1243+
zend_get_gc_buffer *buf = zend_get_gc_buffer_create();
1244+
1245+
/* Add exception handler ZVALs if present */
1246+
if (scope->exception_fci) {
1247+
zend_get_gc_buffer_add_zval(buf, &scope->exception_fci->function_name);
1248+
}
1249+
1250+
/* Add child exception handler ZVALs if present */
1251+
if (scope->child_exception_fci) {
1252+
zend_get_gc_buffer_add_zval(buf, &scope->child_exception_fci->function_name);
1253+
}
1254+
1255+
/* Add finally handlers if present */
1256+
if (scope->finally_handlers) {
1257+
zval *val;
1258+
ZEND_HASH_FOREACH_VAL(scope->finally_handlers, val) {
1259+
zend_get_gc_buffer_add_zval(buf, val);
1260+
} ZEND_HASH_FOREACH_END();
1261+
}
1262+
1263+
/* Add context ZVALs if present */
1264+
if (scope->scope.context) {
1265+
/* Cast to actual context implementation to access HashTables */
1266+
async_context_t *context = (async_context_t *)scope->scope.context;
1267+
1268+
/* Add all values from context->values HashTable */
1269+
zval *val;
1270+
ZEND_HASH_FOREACH_VAL(&context->values, val) {
1271+
zend_get_gc_buffer_add_zval(buf, val);
1272+
} ZEND_HASH_FOREACH_END();
1273+
1274+
/* Add all object keys from context->keys HashTable */
1275+
ZEND_HASH_FOREACH_VAL(&context->keys, val) {
1276+
zend_get_gc_buffer_add_zval(buf, val);
1277+
} ZEND_HASH_FOREACH_END();
1278+
}
1279+
1280+
zend_get_gc_buffer_use(buf, table, num);
1281+
return NULL;
1282+
}
1283+
1284+
static void scope_object_free(zend_object *object)
1285+
{
1286+
async_scope_object_t *scope_object = (async_scope_object_t *) object;
1287+
1288+
async_scope_t *scope = scope_object->scope;
1289+
1290+
if (scope == NULL) {
1291+
return;
1292+
}
1293+
1294+
scope_object->scope = NULL;
1295+
scope->scope.scope_object = NULL;
1296+
zend_object_std_dtor(&scope_object->std);
1297+
1298+
// At this point, the user-defined Scope object is about to be destroyed.
1299+
// This means we are obligated to cancel the Scope and all its child Scopes along with their coroutines.
1300+
// However, the Scope itself will not be destroyed.
1301+
if (false == scope->scope.try_to_dispose(&scope->scope)) {
1302+
zend_object *exception = async_new_exception(
1303+
async_ce_cancellation_exception, "Scope is being disposed due to object destruction"
1304+
);
1305+
1306+
ZEND_ASYNC_SCOPE_CANCEL(&scope->scope, exception, true, ZEND_ASYNC_SCOPE_IS_DISPOSE_SAFELY(&scope->scope));
1307+
}
1308+
}
1309+
12321310
void async_register_scope_ce(void)
12331311
{
12341312
async_ce_scope_provider = register_class_Async_ScopeProvider();
@@ -1242,6 +1320,8 @@ void async_register_scope_ce(void)
12421320

12431321
async_scope_handlers.clone_obj = NULL;
12441322
async_scope_handlers.dtor_obj = scope_destroy;
1323+
async_scope_handlers.get_gc = scope_object_gc;
1324+
async_scope_handlers.free_obj = scope_object_free;
12451325
}
12461326

12471327
/**
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
Coroutine: GC handler basic functionality
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
// Test that GC handler is registered and functioning
10+
$coroutine = spawn(function() {
11+
return "test_value";
12+
});
13+
14+
// Force garbage collection to ensure our GC handler is called
15+
$collected = gc_collect_cycles();
16+
17+
suspend(); // Suspend to simulate coroutine lifecycle
18+
19+
// Check that coroutine completed successfully
20+
$result = $coroutine->getResult();
21+
var_dump($result);
22+
23+
// Verify GC was called (should return >= 0)
24+
var_dump($collected >= 0);
25+
26+
?>
27+
--EXPECT--
28+
string(10) "test_value"
29+
bool(true)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
--TEST--
2+
Coroutine: GC handler with finally handlers
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
// Test GC with finally handlers containing callable ZVALs
10+
$coroutine = spawn(function() {
11+
return "test_value";
12+
});
13+
14+
// Add finally handler with callable
15+
$coroutine->onFinally(function() {
16+
echo "Finally executed\n";
17+
});
18+
19+
// Force garbage collection
20+
$collected = gc_collect_cycles();
21+
22+
suspend(); // Suspend to simulate coroutine lifecycle
23+
24+
// Wait for completion
25+
$result = $coroutine->getResult();
26+
var_dump($result);
27+
28+
// Verify GC was called
29+
var_dump($collected >= 0);
30+
31+
?>
32+
--EXPECT--
33+
Finally executed
34+
string(10) "test_value"
35+
bool(true)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
Coroutine: GC handler with context data
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
use Async\Context;
9+
10+
// Test GC with coroutine context containing ZVALs
11+
$context = new Context();
12+
$context->set("string_key", "string_value");
13+
14+
// Test object key as well
15+
$obj_key = new stdClass();
16+
$context->set($obj_key, "object_value");
17+
18+
$coroutine = spawn(function() use ($context, $obj_key) {
19+
// Access context to ensure it's tracked by GC
20+
$string_val = $context->get("string_key");
21+
$obj_val = $context->get($obj_key);
22+
return [$string_val, $obj_val];
23+
});
24+
25+
// Force garbage collection to test context ZVAL tracking
26+
$collected = gc_collect_cycles();
27+
28+
suspend(); // Suspend to simulate coroutine lifecycle
29+
30+
// Get result
31+
$result = $coroutine->getResult();
32+
var_dump($result);
33+
34+
// Verify GC was called
35+
var_dump($collected >= 0);
36+
37+
?>
38+
--EXPECT--
39+
array(2) {
40+
[0]=>
41+
string(12) "string_value"
42+
[1]=>
43+
string(12) "object_value"
44+
}
45+
bool(true)

0 commit comments

Comments
 (0)