Skip to content

Commit 894d977

Browse files
committed
#7: Refactoring of Zend GC logic for coroutines. Added GC context for traversing nodes. Edge cases fixed.
+ Additionally, for the Coroutine::cancel() method, the mandatory parameter has been removed and is now optional.
1 parent 07fcd93 commit 894d977

16 files changed

+1120
-7
lines changed

coroutine.c

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,11 @@ METHOD(getAwaitingInfo)
256256

257257
METHOD(cancel)
258258
{
259-
zend_object *exception;
259+
zend_object *exception = NULL;
260260

261-
ZEND_PARSE_PARAMETERS_START(1, 1)
262-
Z_PARAM_OBJ_OF_CLASS(exception, zend_ce_cancellation_exception)
261+
ZEND_PARSE_PARAMETERS_START(0, 1)
262+
Z_PARAM_OPTIONAL;
263+
Z_PARAM_OBJ_OF_CLASS_OR_NULL(exception, zend_ce_cancellation_exception)
263264
ZEND_PARSE_PARAMETERS_END();
264265

265266
ZEND_ASYNC_CANCEL(&THIS_COROUTINE->coroutine, exception, false);
@@ -582,6 +583,12 @@ void async_coroutine_finalize(zend_fiber_transfer *transfer, async_coroutine_t *
582583

583584
zend_async_waker_destroy(&coroutine->coroutine);
584585

586+
if (coroutine->coroutine.extended_dispose != NULL) {
587+
const zend_async_coroutine_dispose dispose = coroutine->coroutine.extended_dispose;
588+
coroutine->coroutine.extended_dispose = NULL;
589+
dispose(&coroutine->coroutine);
590+
}
591+
585592
zend_exception_restore();
586593

587594
// If the exception was handled by any handler, we do not propagate it further.

coroutine.stub.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public function getAwaitingInfo(): array {}
105105
/**
106106
* Cancel the coroutine.
107107
*/
108-
public function cancel(\CancellationException $cancellationException): void {}
108+
public function cancel(?\CancellationException $cancellationException = null): void {}
109109

110110
/**
111111
* Define a callback to be executed when the coroutine is finished.

coroutine_arginfo.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: 550ad17a2db4553cda2febb49bb18249b62b8415 */
2+
* Stub hash: bee063ddc53348cc4a6ad23b9fc3415acc6d86f2 */
33

44
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getId, 0, 0, IS_LONG, 0)
55
ZEND_END_ARG_INFO()
@@ -44,8 +44,8 @@ ZEND_END_ARG_INFO()
4444

4545
#define arginfo_class_Async_Coroutine_getAwaitingInfo arginfo_class_Async_Coroutine_getTrace
4646

47-
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_cancel, 0, 1, IS_VOID, 0)
48-
ZEND_ARG_OBJ_INFO(0, cancellationException, CancellationException, 0)
47+
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_cancel, 0, 0, IS_VOID, 0)
48+
ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancellationException, CancellationException, 1, "null")
4949
ZEND_END_ARG_INFO()
5050

5151
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_onFinally, 0, 1, IS_VOID, 0)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
GC 001: Basic suspend in destructor
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
class TestObject {
10+
public $value;
11+
12+
public function __construct($value) {
13+
$this->value = $value;
14+
echo "Created: {$this->value}\n";
15+
}
16+
17+
public function __destruct() {
18+
echo "Destructor start: {$this->value}\n";
19+
20+
// Suspend in destructor - this is the key test scenario
21+
echo "Suspended in destructor: {$this->value}\n";
22+
suspend(); // No parameters - just yield control
23+
24+
echo "Destructor end: {$this->value}\n";
25+
}
26+
}
27+
28+
echo "Starting test\n";
29+
30+
// Create object that will be garbage collected
31+
$obj = new TestObject("test-object");
32+
33+
// Remove reference so object becomes eligible for GC
34+
unset($obj);
35+
36+
echo "After unset\n";
37+
38+
// Force garbage collection
39+
gc_collect_cycles();
40+
41+
echo "After GC\n";
42+
43+
// Start a coroutine to continue execution after destructor suspend
44+
spawn(function() {
45+
echo "Test complete\n";
46+
});
47+
48+
?>
49+
--EXPECT--
50+
Starting test
51+
Created: test-object
52+
Destructor start: test-object
53+
Suspended in destructor: test-object
54+
Destructor end: test-object
55+
After unset
56+
After GC
57+
Test complete
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
--TEST--
2+
GC 002: Spawn new coroutine in destructor
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\await;
8+
use function Async\suspend;
9+
10+
class TestObject {
11+
public $value;
12+
13+
public function __construct($value) {
14+
$this->value = $value;
15+
echo "Created: {$this->value}\n";
16+
}
17+
18+
public function __destruct() {
19+
echo "Destructor start: {$this->value}\n";
20+
21+
// Spawn new coroutine in destructor - this is the key test scenario
22+
spawn(function() {
23+
echo "Spawned coroutine running\n";
24+
suspend();
25+
echo "Spawned coroutine complete\n";
26+
});
27+
28+
echo "Coroutine spawned in destructor: {$this->value}\n";
29+
30+
echo "Destructor end: {$this->value}\n";
31+
}
32+
}
33+
34+
echo "Starting test\n";
35+
36+
// Create object that will be garbage collected
37+
$obj = new TestObject("test-object");
38+
39+
// Remove reference so object becomes eligible for GC
40+
unset($obj);
41+
42+
echo "After unset\n";
43+
44+
// Force garbage collection
45+
gc_collect_cycles();
46+
47+
echo "After GC\n";
48+
49+
// Continue execution to let spawned coroutines complete
50+
spawn(function() {
51+
echo "Test complete\n";
52+
});
53+
54+
?>
55+
--EXPECT--
56+
Starting test
57+
Created: test-object
58+
Destructor start: test-object
59+
Coroutine spawned in destructor: test-object
60+
Destructor end: test-object
61+
After unset
62+
After GC
63+
Spawned coroutine running
64+
Test complete
65+
Spawned coroutine complete
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
--TEST--
2+
GC 003: Resume other coroutine from destructor
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\await;
8+
use function Async\suspend;
9+
10+
// Global variable to store suspended coroutine
11+
$suspended_coroutine = null;
12+
13+
class TestObject {
14+
public $value;
15+
16+
public function __construct($value) {
17+
$this->value = $value;
18+
echo "Created: {$this->value}\n";
19+
}
20+
21+
public function __destruct() {
22+
global $suspended_coroutine;
23+
24+
echo "Destructor start: {$this->value}\n";
25+
26+
// Resume the previously suspended coroutine
27+
if ($suspended_coroutine !== null) {
28+
echo "Resuming other coroutine from destructor\n";
29+
$result = await($suspended_coroutine);
30+
echo "Other coroutine result: {$result}\n";
31+
}
32+
33+
echo "Destructor end: {$this->value}\n";
34+
}
35+
}
36+
37+
echo "Starting test\n";
38+
39+
// Start a coroutine that will be suspended
40+
$suspended_coroutine = spawn(function() {
41+
echo "Other coroutine start\n";
42+
suspend(); // Simulate async work
43+
echo "Other coroutine end\n";
44+
return "other-result";
45+
});
46+
47+
// Create object that will be garbage collected
48+
$obj = new TestObject("test-object");
49+
50+
// Remove reference so object becomes eligible for GC
51+
unset($obj);
52+
53+
echo "After unset\n";
54+
55+
// Force garbage collection - this will trigger destructor
56+
gc_collect_cycles();
57+
58+
echo "After GC\n";
59+
60+
// Continue execution
61+
spawn(function() {
62+
echo "Test complete\n";
63+
});
64+
65+
?>
66+
--EXPECT--
67+
Starting test
68+
Created: test-object
69+
Destructor start: test-object
70+
Resuming other coroutine from destructor
71+
Other coroutine start
72+
Other coroutine end
73+
Other coroutine result: other-result
74+
Destructor end: test-object
75+
After unset
76+
After GC
77+
Test complete
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
--TEST--
2+
GC 004: Exception handling with suspend in destructor
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
9+
class TestObject {
10+
public $value;
11+
public $should_throw;
12+
13+
public function __construct($value, $should_throw = false) {
14+
$this->value = $value;
15+
$this->should_throw = $should_throw;
16+
echo "Created: {$this->value}\n";
17+
}
18+
19+
public function __destruct() {
20+
echo "Destructor start: {$this->value}\n";
21+
22+
try {
23+
// Suspend in destructor
24+
echo "Suspended in destructor: {$this->value}\n";
25+
suspend();
26+
27+
if ($this->should_throw) {
28+
throw new Exception("Test exception after suspend");
29+
}
30+
31+
echo "Destructor middle: {$this->value}\n";
32+
33+
} catch (Exception $e) {
34+
echo "Exception caught in destructor: {$e->getMessage()}\n";
35+
}
36+
37+
echo "Destructor end: {$this->value}\n";
38+
}
39+
}
40+
41+
spawn(function() {
42+
echo "Starting test\n";
43+
44+
// Test 1: Normal case without exception
45+
echo "=== Test 1: Normal case ===\n";
46+
$obj1 = new TestObject("normal", false);
47+
unset($obj1);
48+
gc_collect_cycles();
49+
50+
suspend();
51+
52+
// Test 2: Exception case
53+
echo "=== Test 2: Exception case ===\n";
54+
$obj2 = new TestObject("exception", true);
55+
unset($obj2);
56+
gc_collect_cycles();
57+
58+
suspend();
59+
60+
echo "Test complete\n";
61+
});
62+
63+
?>
64+
--EXPECT--
65+
Starting test
66+
=== Test 1: Normal case ===
67+
Created: normal
68+
Destructor start: normal
69+
Suspended in destructor: normal
70+
Destructor middle: normal
71+
Destructor end: normal
72+
=== Test 2: Exception case ===
73+
Created: exception
74+
Destructor start: exception
75+
Suspended in destructor: exception
76+
Exception caught in destructor: Test exception after suspend
77+
Destructor end: exception
78+
Test complete

0 commit comments

Comments
 (0)