Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions coroutine.c
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,11 @@ METHOD(getAwaitingInfo)

METHOD(cancel)
{
zend_object *exception;
zend_object *exception = NULL;

ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_OBJ_OF_CLASS(exception, zend_ce_cancellation_exception)
ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL;
Z_PARAM_OBJ_OF_CLASS_OR_NULL(exception, zend_ce_cancellation_exception)
ZEND_PARSE_PARAMETERS_END();

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

zend_async_waker_destroy(&coroutine->coroutine);

if (coroutine->coroutine.extended_dispose != NULL) {
const zend_async_coroutine_dispose dispose = coroutine->coroutine.extended_dispose;
coroutine->coroutine.extended_dispose = NULL;
dispose(&coroutine->coroutine);
}

zend_exception_restore();

// If the exception was handled by any handler, we do not propagate it further.
Expand Down
2 changes: 1 addition & 1 deletion coroutine.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function getAwaitingInfo(): array {}
/**
* Cancel the coroutine.
*/
public function cancel(\CancellationException $cancellationException): void {}
public function cancel(?\CancellationException $cancellationException = null): void {}

/**
* Define a callback to be executed when the coroutine is finished.
Expand Down
6 changes: 3 additions & 3 deletions coroutine_arginfo.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 550ad17a2db4553cda2febb49bb18249b62b8415 */
* Stub hash: bee063ddc53348cc4a6ad23b9fc3415acc6d86f2 */

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_getId, 0, 0, IS_LONG, 0)
ZEND_END_ARG_INFO()
Expand Down Expand Up @@ -44,8 +44,8 @@ ZEND_END_ARG_INFO()

#define arginfo_class_Async_Coroutine_getAwaitingInfo arginfo_class_Async_Coroutine_getTrace

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_cancel, 0, 1, IS_VOID, 0)
ZEND_ARG_OBJ_INFO(0, cancellationException, CancellationException, 0)
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_cancel, 0, 0, IS_VOID, 0)
ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, cancellationException, CancellationException, 1, "null")
ZEND_END_ARG_INFO()

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Async_Coroutine_onFinally, 0, 1, IS_VOID, 0)
Expand Down
57 changes: 57 additions & 0 deletions tests/gc/001-gc_destructor_basic_suspend.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
--TEST--
GC 001: Basic suspend in destructor
--FILE--
<?php

use function Async\spawn;
use function Async\suspend;

class TestObject {
public $value;

public function __construct($value) {
$this->value = $value;
echo "Created: {$this->value}\n";
}

public function __destruct() {
echo "Destructor start: {$this->value}\n";

// Suspend in destructor - this is the key test scenario
echo "Suspended in destructor: {$this->value}\n";
suspend(); // No parameters - just yield control

echo "Destructor end: {$this->value}\n";
}
}

echo "Starting test\n";

// Create object that will be garbage collected
$obj = new TestObject("test-object");

// Remove reference so object becomes eligible for GC
unset($obj);

echo "After unset\n";

// Force garbage collection
gc_collect_cycles();

echo "After GC\n";

// Start a coroutine to continue execution after destructor suspend
spawn(function() {
echo "Test complete\n";
});

?>
--EXPECT--
Starting test
Created: test-object
Destructor start: test-object
Suspended in destructor: test-object
Destructor end: test-object
After unset
After GC
Test complete
65 changes: 65 additions & 0 deletions tests/gc/002-gc_destructor_spawn_coroutine.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
--TEST--
GC 002: Spawn new coroutine in destructor
--FILE--
<?php

use function Async\spawn;
use function Async\await;
use function Async\suspend;

class TestObject {
public $value;

public function __construct($value) {
$this->value = $value;
echo "Created: {$this->value}\n";
}

public function __destruct() {
echo "Destructor start: {$this->value}\n";

// Spawn new coroutine in destructor - this is the key test scenario
spawn(function() {
echo "Spawned coroutine running\n";
suspend();
echo "Spawned coroutine complete\n";
});

echo "Coroutine spawned in destructor: {$this->value}\n";

echo "Destructor end: {$this->value}\n";
}
}

echo "Starting test\n";

// Create object that will be garbage collected
$obj = new TestObject("test-object");

// Remove reference so object becomes eligible for GC
unset($obj);

echo "After unset\n";

// Force garbage collection
gc_collect_cycles();

echo "After GC\n";

// Continue execution to let spawned coroutines complete
spawn(function() {
echo "Test complete\n";
});

?>
--EXPECT--
Starting test
Created: test-object
Destructor start: test-object
Coroutine spawned in destructor: test-object
Destructor end: test-object
After unset
After GC
Spawned coroutine running
Test complete
Spawned coroutine complete
77 changes: 77 additions & 0 deletions tests/gc/003-gc_destructor_resume_other.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
--TEST--
GC 003: Resume other coroutine from destructor
--FILE--
<?php

use function Async\spawn;
use function Async\await;
use function Async\suspend;

// Global variable to store suspended coroutine
$suspended_coroutine = null;

class TestObject {
public $value;

public function __construct($value) {
$this->value = $value;
echo "Created: {$this->value}\n";
}

public function __destruct() {
global $suspended_coroutine;

echo "Destructor start: {$this->value}\n";

// Resume the previously suspended coroutine
if ($suspended_coroutine !== null) {
echo "Resuming other coroutine from destructor\n";
$result = await($suspended_coroutine);
echo "Other coroutine result: {$result}\n";
}

echo "Destructor end: {$this->value}\n";
}
}

echo "Starting test\n";

// Start a coroutine that will be suspended
$suspended_coroutine = spawn(function() {
echo "Other coroutine start\n";
suspend(); // Simulate async work
echo "Other coroutine end\n";
return "other-result";
});

// Create object that will be garbage collected
$obj = new TestObject("test-object");

// Remove reference so object becomes eligible for GC
unset($obj);

echo "After unset\n";

// Force garbage collection - this will trigger destructor
gc_collect_cycles();

echo "After GC\n";

// Continue execution
spawn(function() {
echo "Test complete\n";
});

?>
--EXPECT--
Starting test
Created: test-object
Destructor start: test-object
Resuming other coroutine from destructor
Other coroutine start
Other coroutine end
Other coroutine result: other-result
Destructor end: test-object
After unset
After GC
Test complete
78 changes: 78 additions & 0 deletions tests/gc/004-gc_destructor_exception_with_suspend.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
--TEST--
GC 004: Exception handling with suspend in destructor
--FILE--
<?php

use function Async\spawn;
use function Async\suspend;

class TestObject {
public $value;
public $should_throw;

public function __construct($value, $should_throw = false) {
$this->value = $value;
$this->should_throw = $should_throw;
echo "Created: {$this->value}\n";
}

public function __destruct() {
echo "Destructor start: {$this->value}\n";

try {
// Suspend in destructor
echo "Suspended in destructor: {$this->value}\n";
suspend();

if ($this->should_throw) {
throw new Exception("Test exception after suspend");
}

echo "Destructor middle: {$this->value}\n";

} catch (Exception $e) {
echo "Exception caught in destructor: {$e->getMessage()}\n";
}

echo "Destructor end: {$this->value}\n";
}
}

spawn(function() {
echo "Starting test\n";

// Test 1: Normal case without exception
echo "=== Test 1: Normal case ===\n";
$obj1 = new TestObject("normal", false);
unset($obj1);
gc_collect_cycles();

suspend();

// Test 2: Exception case
echo "=== Test 2: Exception case ===\n";
$obj2 = new TestObject("exception", true);
unset($obj2);
gc_collect_cycles();

suspend();

echo "Test complete\n";
});

?>
--EXPECT--
Starting test
=== Test 1: Normal case ===
Created: normal
Destructor start: normal
Suspended in destructor: normal
Destructor middle: normal
Destructor end: normal
=== Test 2: Exception case ===
Created: exception
Destructor start: exception
Suspended in destructor: exception
Exception caught in destructor: Test exception after suspend
Destructor end: exception
Test complete
Loading