Skip to content

Commit 38fa354

Browse files
authored
Merge pull request #71 from true-async/fiber-support
Fiber support
2 parents 8821279 + 01dd33a commit 38fa354

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1578
-22
lines changed

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,23 @@ All notable changes to the Async extension for PHP will be documented in this fi
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.5.0] - 2025-10-31
8+
## [0.5.0] - 2025-12-31
99

1010
### Added
11+
- **Fiber Support**: Full integration of PHP Fibers with TrueAsync coroutine system
12+
- `Fiber::suspend()` and `Fiber::resume()` work in async scheduler context
13+
- `Fiber::getCoroutine()` method to access fiber's coroutine
14+
- Fiber status methods (isStarted, isSuspended, isRunning, isTerminated)
15+
- Support for nested fibers and fiber-coroutine interactions
16+
- Comprehensive test coverage for all fiber scenarios
1117
- **TrueAsync API**: Added `ZEND_ASYNC_SCHEDULER_LAUNCH()` macro for scheduler initialization
18+
- **TrueAsync API**: Updated to version 0.8.0 with fiber support
19+
20+
### Fixed
21+
- **Critical GC Bug**: Fixed garbage collection crash during coroutine cancellation when exception occurs in main coroutine while GC is running
22+
- Fixed double free in `zend_fiber_object_destroy()`
23+
- Fixed `stream_select()` for `timeout == NULL` case in async context
24+
- Fixed fiber memory leaks and improved GC logic
1225

1326
### Changed
1427
- **Deadlock Detection**: Replaced warnings with structured exception handling

coroutine.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,10 @@ zend_coroutine_t *async_new_coroutine(zend_async_scope_t *scope)
343343
{
344344
zend_object *object = coroutine_object_create(async_ce_coroutine);
345345

346-
if (UNEXPECTED(EG(exception))) {
346+
if (UNEXPECTED(object == NULL || EG(exception))) {
347+
if (object != NULL) {
348+
zend_object_release(object);
349+
}
347350
return NULL;
348351
}
349352

coroutine.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#define COROUTINE_H
1818

1919
#include "php_async_api.h"
20+
#include <Zend/zend_fibers.h>
2021
#include <Zend/zend_async_API.h>
2122

2223
/* Fiber context structure for pooling */

libuv_reactor.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,12 @@ bool libuv_reactor_execute(bool no_wait)
168168
{
169169
// OPTIMIZATION: Skip uv_run() if no libuv handles to avoid unnecessary clock_gettime() calls
170170
if (!uv_loop_alive(UVLOOP)) {
171-
return ZEND_ASYNC_ACTIVE_EVENT_COUNT > 0;
171+
return false;
172172
}
173173

174174
const bool has_handles = uv_run(UVLOOP, no_wait ? UV_RUN_NOWAIT : UV_RUN_ONCE);
175175

176-
return ZEND_ASYNC_ACTIVE_EVENT_COUNT > 0 || has_handles;
176+
return has_handles && ZEND_ASYNC_ACTIVE_EVENT_COUNT > 0;
177177
}
178178

179179
/* }}} */

scheduler.c

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,47 @@ static bool resolve_deadlocks(void)
526526
return false;
527527
}
528528

529+
//
530+
// Let’s count the number of coroutine-fibers that are in the YIELD state.
531+
// This state differs from the regular Suspended state in that
532+
// the Fiber has transferred control back to the parent coroutine.
533+
//
534+
zend_long fiber_coroutines_count = 0;
535+
536+
ZEND_HASH_FOREACH_VAL(&ASYNC_G(coroutines), value) {
537+
const zend_coroutine_t *coroutine = (zend_coroutine_t *) Z_PTR_P(value);
538+
539+
if (ZEND_COROUTINE_IS_FIBER(coroutine)
540+
&& ZEND_COROUTINE_IS_YIELD(coroutine)
541+
&& coroutine->extended_data != NULL) {
542+
fiber_coroutines_count++;
543+
}
544+
}
545+
ZEND_HASH_FOREACH_END();
546+
547+
//
548+
// If all coroutines are fiber coroutines in the SUSPENDED state,
549+
// we can simply cancel them without creating a deadlock exception.
550+
//
551+
if (fiber_coroutines_count == real_coroutines) {
552+
553+
ZEND_HASH_FOREACH_VAL(&ASYNC_G(coroutines), value) {
554+
zend_coroutine_t *coroutine = (zend_coroutine_t *) Z_PTR_P(value);
555+
556+
if (ZEND_COROUTINE_IS_FIBER(coroutine)
557+
&& ZEND_COROUTINE_IS_YIELD(coroutine)
558+
&& coroutine->extended_data != NULL) {
559+
ZEND_ASYNC_CANCEL(coroutine, zend_create_graceful_exit(), true);
560+
561+
if (UNEXPECTED(EG(exception) != NULL)) {
562+
return true;
563+
}
564+
}
565+
}
566+
ZEND_HASH_FOREACH_END();
567+
return false;
568+
}
569+
529570
// Create deadlock exception to be set as exit_exception
530571
zend_object *deadlock_exception = async_new_exception(async_ce_deadlock_error,
531572
"Deadlock detected: no active coroutines, %u coroutines in waiting", real_coroutines);
@@ -552,7 +593,7 @@ static bool resolve_deadlocks(void)
552593
ZEND_ASYNC_CANCEL(
553594
&coroutine->coroutine, async_new_exception(async_ce_cancellation_exception, "Deadlock detected"), true);
554595

555-
if (EG(exception) != NULL) {
596+
if (UNEXPECTED(EG(exception) != NULL)) {
556597
return true;
557598
}
558599
}
@@ -1025,6 +1066,16 @@ bool async_scheduler_coroutine_enqueue(zend_coroutine_t *coroutine)
10251066
// save the filename and line number where the coroutine was created
10261067
zend_apply_current_filename_and_line(&coroutine->filename, &coroutine->lineno);
10271068

1069+
// Notify scope that a new coroutine has been enqueued
1070+
zend_async_scope_t *scope = coroutine->scope;
1071+
1072+
if (UNEXPECTED(scope == NULL)) {
1073+
// throw error if the coroutine has no scope
1074+
coroutine->waker->status = ZEND_ASYNC_WAKER_NO_STATUS;
1075+
async_throw_error("The coroutine has no scope assigned");
1076+
return false;
1077+
}
1078+
10281079
if (UNEXPECTED(zend_hash_index_add_ptr(&ASYNC_G(coroutines), ((async_coroutine_t *)coroutine)->std.handle, coroutine) == NULL)) {
10291080
coroutine->waker->status = ZEND_ASYNC_WAKER_IGNORED;
10301081
async_throw_error("Failed to add coroutine to the list");
@@ -1033,8 +1084,6 @@ bool async_scheduler_coroutine_enqueue(zend_coroutine_t *coroutine)
10331084

10341085
ZEND_ASYNC_INCREASE_COROUTINE_COUNT;
10351086

1036-
// Notify scope that a new coroutine has been enqueued
1037-
zend_async_scope_t *scope = coroutine->scope;
10381087
scope->after_coroutine_enqueue(coroutine, scope);
10391088
if (UNEXPECTED(EG(exception))) {
10401089
coroutine->waker->status = ZEND_ASYNC_WAKER_IGNORED;

tests/edge_cases/007-fiber_first_then_spawn.phpt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,11 @@ echo "Test completed\n";
4646
Test: Fiber first, then spawn
4747
Starting Fiber
4848
Inside Fiber
49-
Async exception caught: Cannot spawn a coroutine when async is disabled
49+
Fiber attempting to continue after spawn
50+
Inside spawned coroutine from Fiber
51+
Fiber suspended with: fiber suspended
52+
Resuming Fiber
53+
Coroutine completed
54+
Fiber resumed
55+
Fiber returned: fiber done
5056
Test completed

tests/edge_cases/008-spawn_first_then_fiber.phpt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,14 @@ echo "Test completed\n";
5555
--EXPECTF--
5656
Test: Spawn first, then Fiber
5757
Coroutine spawned, now creating Fiber
58-
Error caught: Cannot create a fiber while an True Async is active
59-
Test completed
58+
Starting Fiber
6059
Coroutine started
61-
Coroutine resumed
60+
Inside Fiber - this should conflict with active scheduler
61+
Coroutine resumed
62+
Fiber suspended with: fiber suspended
63+
Resuming Fiber
64+
Fiber resumed
65+
Fiber completed with: fiber done
66+
Getting coroutine result
67+
Coroutine completed with: coroutine result
68+
Test completed

tests/edge_cases/009-fiber_spawn_destructor.phpt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,18 @@ Created: SpawnInDestructor
100100
Starting some async operations
101101
Unsetting objects to trigger destructors
102102
Destructing: FiberInDestructor
103-
Error in destructor: Cannot create a fiber while an True Async is active
103+
Starting fiber in destructor
104+
Main coroutine running
105+
Fiber running in destructor
106+
Main coroutine resumed
107+
Fiber suspended with: destructor fiber
108+
Fiber resumed in destructor
109+
Fiber completed with: destructor done
104110
Destructor finished: FiberInDestructor
105111
Destructing: SpawnInDestructor
106112
Spawning coroutine in destructor
107113
Waiting for coroutine in destructor
108-
Main coroutine running
109114
Coroutine running in destructor
110-
Main coroutine resumed
111115
Coroutine resumed in destructor
112116
Coroutine completed with: destructor coroutine done
113117
Destructor finished: SpawnInDestructor
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
--TEST--
2+
GC with include in suspended coroutine - symTable double DELREF
3+
--FILE--
4+
<?php
5+
6+
use function Async\spawn;
7+
use function Async\suspend;
8+
use function Async\await;
9+
10+
class Cycle {
11+
public $self;
12+
}
13+
14+
try {
15+
$coroutine = spawn(function() {
16+
$parent1 = "value1";
17+
$parent2 = "value2";
18+
19+
for ($i = 0; $i < 10000; $i++) {
20+
$obj = new Cycle();
21+
$obj->self = $obj;
22+
}
23+
24+
// Suspend - coroutine is now in suspended state
25+
suspend();
26+
27+
// Include inherits parent symTable
28+
// Bug: GC may add same variables twice -> double DELREF
29+
include __DIR__ . '/011-gc_include_symtable_double_delref_included.inc';
30+
31+
echo "parent1: {$parent1}\n";
32+
echo "parent2: {$parent2}\n";
33+
echo "included: {$included}\n";
34+
35+
return "done";
36+
});
37+
38+
$result = await($coroutine);
39+
echo "result: {$result}\n";
40+
41+
} catch (Error $e) {
42+
echo "Error: " . $e->getMessage() . "\n";
43+
}
44+
45+
echo "OK\n";
46+
gc_collect_cycles();
47+
?>
48+
--EXPECTF--
49+
parent1: value1
50+
parent2: value2
51+
included: included_value
52+
result: done
53+
OK
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
// Included file for GC test
3+
$included = "included_value";

0 commit comments

Comments
 (0)