Skip to content

Commit 02b2f3f

Browse files
committed
async: Async\Coroutine::current()/resume() — engine tracks current from intercept binding, deferred wake
1 parent 73eadfe commit 02b2f3f

5 files changed

Lines changed: 184 additions & 1 deletion

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
Async\Coroutine: current() tracks the running coroutine; resume() defers a wake
3+
--FILE--
4+
<?php
5+
Async\SchedulerHook::register('test', new class extends Async\AbstractScheduler {
6+
public SplQueue $ready;
7+
public function __construct() { $this->ready = new SplQueue(); }
8+
public function interceptFiber(Fiber $fiber): ?object {
9+
return new class($fiber) {
10+
public function __construct(public readonly Fiber $fiber) {}
11+
};
12+
}
13+
public function enqueue(object $coroutine): bool {
14+
$this->ready->enqueue($coroutine);
15+
return true;
16+
}
17+
public function suspend(bool $fromMain, bool $isBailout): bool {
18+
if (!$fromMain) {
19+
return true;
20+
}
21+
while (!$this->ready->isEmpty()) {
22+
$fiber = $this->ready->dequeue()->fiber;
23+
$fiber->isStarted() ? $fiber->resume() : $fiber->start();
24+
}
25+
return true;
26+
}
27+
});
28+
29+
// No coroutine is current in the main flow.
30+
var_dump(Async\Coroutine::current());
31+
32+
$waiter = null;
33+
34+
$w = new Fiber(function () use (&$waiter): void {
35+
$waiter = Async\Coroutine::current(); // the running coroutine object
36+
echo "waiter: is-coroutine=", var_export($waiter !== null, true), "\n";
37+
Fiber::suspend(); // await
38+
echo "waiter: resumed\n";
39+
});
40+
41+
$r = new Fiber(function () use (&$waiter): void {
42+
echo "resolver: resume waiter\n";
43+
Async\Coroutine::resume($waiter); // deferred wake (no immediate switch)
44+
echo "resolver: done\n";
45+
});
46+
47+
$w->start();
48+
$r->start();
49+
echo "main: end\n";
50+
?>
51+
--EXPECT--
52+
NULL
53+
main: end
54+
waiter: is-coroutine=true
55+
resolver: resume waiter
56+
resolver: done
57+
waiter: resumed

Zend/zend_fibers.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,9 +975,16 @@ static void zend_fiber_scheduler_switch(zend_fiber *fiber, zval *return_value)
975975
const bool saved_context = ZEND_ASYNC_IN_SCHEDULER_CONTEXT;
976976
ZEND_ASYNC_IN_SCHEDULER_CONTEXT = false;
977977

978+
/* The coroutine bound to this fiber (via the scheduler's intercept-fiber
979+
* return) is the current one for the duration of its body. Restored when it
980+
* suspends and hands control back here. */
981+
zend_coroutine_t *saved_coroutine = ZEND_ASYNC_CURRENT_COROUTINE;
982+
ZEND_ASYNC_CURRENT_COROUTINE = coroutine;
983+
978984
zend_fiber_transfer transfer = zend_fiber_resume_internal(
979985
fiber, Z_ISUNDEF(value) ? NULL : &value, is_error);
980986

987+
ZEND_ASYNC_CURRENT_COROUTINE = saved_coroutine;
981988
ZEND_ASYNC_IN_SCHEDULER_CONTEXT = saved_context;
982989

983990
zval_ptr_dtor(&value);

Zend/zend_scheduler_hook.c

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,11 +579,87 @@ ZEND_METHOD(Async_AbstractScheduler, contextUnset)
579579
RETURN_FALSE;
580580
}
581581

582+
/////////////////////////////////////////////////////////////////////
583+
/// Async\Coroutine
584+
/////////////////////////////////////////////////////////////////////
585+
586+
ZEND_METHOD(Async_Coroutine, current)
587+
{
588+
ZEND_PARSE_PARAMETERS_NONE();
589+
590+
zend_coroutine_t *coro = ZEND_ASYNC_CURRENT_COROUTINE;
591+
592+
if (coro == NULL) {
593+
RETURN_NULL();
594+
}
595+
596+
zend_object *object = ZEND_COROUTINE_OBJECT(coro);
597+
598+
if (object == NULL) {
599+
RETURN_NULL();
600+
}
601+
602+
GC_ADDREF(object);
603+
RETURN_OBJ(object);
604+
}
605+
606+
ZEND_METHOD(Async_Coroutine, resume)
607+
{
608+
zval *coroutine;
609+
zend_object *error = NULL;
610+
611+
ZEND_PARSE_PARAMETERS_START(1, 2)
612+
Z_PARAM_OBJECT(coroutine)
613+
Z_PARAM_OPTIONAL
614+
Z_PARAM_OBJ_OF_CLASS_OR_NULL(error, zend_ce_throwable)
615+
ZEND_PARSE_PARAMETERS_END();
616+
617+
/* A deferred wake: route the coroutine back to the scheduler's resume()
618+
* hook to be re-queued (never an immediate fiber switch). Fall back to
619+
* enqueue() when the scheduler does not override resume(). */
620+
php_async_hook_id id;
621+
622+
if (PHP_ASYNC_HOOK(PHP_ASYNC_HOOK_RESUME)->set) {
623+
id = PHP_ASYNC_HOOK_RESUME;
624+
} else if (PHP_ASYNC_HOOK(PHP_ASYNC_HOOK_ENQUEUE)->set) {
625+
id = PHP_ASYNC_HOOK_ENQUEUE;
626+
} else {
627+
zend_throw_error(NULL, "The registered scheduler cannot resume coroutines");
628+
RETURN_THROWS();
629+
}
630+
631+
zval args[2];
632+
ZVAL_COPY(&args[0], coroutine);
633+
uint32_t argc = 1;
634+
635+
if (id == PHP_ASYNC_HOOK_RESUME) {
636+
if (error != NULL) {
637+
ZVAL_OBJ(&args[1], error);
638+
GC_ADDREF(error);
639+
} else {
640+
ZVAL_NULL(&args[1]);
641+
}
642+
argc = 2;
643+
}
644+
645+
php_async_hook_call_bool(id, argc, args);
646+
647+
zval_ptr_dtor(&args[0]);
648+
if (argc == 2) {
649+
zval_ptr_dtor(&args[1]);
650+
}
651+
652+
if (UNEXPECTED(EG(exception))) {
653+
RETURN_THROWS();
654+
}
655+
}
656+
582657
void zend_register_scheduler_hook(void)
583658
{
584659
async_ce_Scheduler = register_class_Async_Scheduler();
585660
async_ce_AbstractScheduler = register_class_Async_AbstractScheduler(async_ce_Scheduler);
586661
register_class_Async_SchedulerHook();
662+
register_class_Async_Coroutine();
587663
}
588664

589665
void zend_scheduler_hook_request_shutdown(void)

Zend/zend_scheduler_hook.stub.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,20 @@ public static function getModule(): ?string {}
104104
*/
105105
public static function defer(callable $task): void {}
106106
}
107+
108+
/**
109+
* The coroutine currently on the stack, and the way to wake a suspended one.
110+
*
111+
* The engine tracks the current coroutine from the object the scheduler
112+
* returned from Async\Scheduler::interceptFiber() — there is no setter.
113+
* resume() is a *deferred* wake: it routes the coroutine back to the
114+
* scheduler's resume() hook to be re-queued, never an immediate fiber switch.
115+
*/
116+
final class Coroutine
117+
{
118+
/** The coroutine object running on the current stack, or null in the main flow. */
119+
public static function current(): ?object {}
120+
121+
/** Wake a suspended coroutine: hand it to the scheduler to be run later. */
122+
public static function resume(object $coroutine, ?\Throwable $error = null): void {}
123+
}

Zend/zend_scheduler_hook_arginfo.h

Lines changed: 27 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)