Skip to content

Commit 0b1ba73

Browse files
committed
async: interface-based scheduler registration (Async\Scheduler + AbstractScheduler)
1 parent 796800e commit 0b1ba73

4 files changed

Lines changed: 311 additions & 151 deletions

File tree

Zend/zend_async_API.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,9 @@ typedef struct {
514514
typedef struct {
515515
bool active;
516516
zend_string *module;
517+
/* The registered Async\Scheduler instance; one ref held for the request.
518+
* The bound hook methods borrow this object. */
519+
zend_object *scheduler;
517520
php_async_hook_t hooks[PHP_ASYNC_HOOK_COUNT];
518521
} php_async_handlers_t;
519522

Zend/zend_scheduler_hook.c

Lines changed: 97 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -35,79 +35,67 @@
3535
#include "zend_exceptions.h"
3636
#include "zend_closures.h"
3737

38-
/* The hook names, indexed by php_async_hook_id: used only to map the
39-
* incoming array keys (the Async\SchedulerHook class constants). */
38+
/* The Async\Scheduler method name for each hook (lower-cased, as stored in the
39+
* class function table), indexed by php_async_hook_id. */
4040
static const struct {
4141
const char *name;
4242
size_t len;
43-
} php_async_hook_names[PHP_ASYNC_HOOK_COUNT] = {
43+
} php_async_hook_methods[PHP_ASYNC_HOOK_COUNT] = {
4444
[PHP_ASYNC_HOOK_LAUNCH] = { ZEND_STRL("launch") },
4545
[PHP_ASYNC_HOOK_SHUTDOWN] = { ZEND_STRL("shutdown") },
46-
[PHP_ASYNC_HOOK_INTERCEPT_FIBER] = { ZEND_STRL("intercept_fiber") },
47-
[PHP_ASYNC_HOOK_ENQUEUE] = { ZEND_STRL("enqueue_coroutine") },
46+
[PHP_ASYNC_HOOK_INTERCEPT_FIBER] = { ZEND_STRL("interceptfiber") },
47+
[PHP_ASYNC_HOOK_ENQUEUE] = { ZEND_STRL("enqueue") },
4848
[PHP_ASYNC_HOOK_SUSPEND] = { ZEND_STRL("suspend") },
4949
[PHP_ASYNC_HOOK_RESUME] = { ZEND_STRL("resume") },
5050
[PHP_ASYNC_HOOK_CANCEL] = { ZEND_STRL("cancel") },
51-
[PHP_ASYNC_HOOK_CONTEXT_FIND] = { ZEND_STRL("context_find") },
52-
[PHP_ASYNC_HOOK_CONTEXT_SET] = { ZEND_STRL("context_set") },
53-
[PHP_ASYNC_HOOK_CONTEXT_UNSET] = { ZEND_STRL("context_unset") },
51+
[PHP_ASYNC_HOOK_CONTEXT_FIND] = { ZEND_STRL("contextfind") },
52+
[PHP_ASYNC_HOOK_CONTEXT_SET] = { ZEND_STRL("contextset") },
53+
[PHP_ASYNC_HOOK_CONTEXT_UNSET] = { ZEND_STRL("contextunset") },
5454
[PHP_ASYNC_HOOK_DEFER] = { ZEND_STRL("defer") },
5555
};
5656

57+
/* Class entries, filled by zend_register_scheduler_hook(). */
58+
static zend_class_entry *async_ce_Scheduler;
59+
static zend_class_entry *async_ce_AbstractScheduler;
60+
5761
/* The storage lives in the per-thread async globals: correct under ZTS,
5862
* request-local by construction. */
5963
#define PHP_ASYNC_HANDLERS (ZEND_ASYNC_G(scheduler_hooks))
6064
#define PHP_ASYNC_HOOK(id) (&PHP_ASYNC_HANDLERS.hooks[(id)])
6165

62-
/* Read the callable for `id` from the incoming array into storage. */
63-
static bool php_async_hook_take(HashTable *array, php_async_hook_id id)
66+
/* Bind hook `id` to the scheduler's method of the same name. A hook is
67+
* considered "provided" only when the method is overridden below
68+
* Async\AbstractScheduler; a method that resolves to AbstractScheduler is its
69+
* inert default, so the engine keeps its own behaviour for that hook. */
70+
static void php_async_hook_bind(zend_object *scheduler, php_async_hook_id id)
6471
{
6572
php_async_hook_t *hook = PHP_ASYNC_HOOK(id);
66-
zval *entry = zend_hash_str_find(array, php_async_hook_names[id].name, php_async_hook_names[id].len);
73+
zend_function *fn = zend_hash_str_find_ptr(&scheduler->ce->function_table,
74+
php_async_hook_methods[id].name, php_async_hook_methods[id].len);
6775

68-
if (entry == NULL) {
76+
if (fn == NULL || fn->common.scope == async_ce_AbstractScheduler) {
6977
hook->set = false;
70-
return true;
78+
return;
7179
}
7280

73-
char *error = NULL;
74-
75-
if (zend_fcall_info_init(entry, 0, &hook->fci, &hook->fcc, NULL, &error) != SUCCESS) {
76-
zend_type_error("Async scheduler hook \"%s\" must be a valid callable: %s",
77-
php_async_hook_names[id].name, error != NULL ? error : "unknown error");
78-
if (error != NULL) {
79-
efree(error);
80-
}
81-
82-
return false;
83-
}
81+
memset(&hook->fci, 0, sizeof(hook->fci));
82+
memset(&hook->fcc, 0, sizeof(hook->fcc));
8483

85-
if (error != NULL) {
86-
efree(error);
87-
}
84+
hook->fci.size = sizeof(hook->fci);
85+
hook->fci.object = scheduler;
86+
ZVAL_UNDEF(&hook->fci.function_name);
8887

89-
/* Keep the callable alive for the whole request. */
90-
Z_TRY_ADDREF(hook->fci.function_name);
91-
if (hook->fci.object != NULL) {
92-
GC_ADDREF(hook->fci.object);
93-
}
88+
hook->fcc.function_handler = fn;
89+
hook->fcc.object = scheduler;
90+
hook->fcc.called_scope = scheduler->ce;
9491

9592
hook->set = true;
96-
return true;
9793
}
9894

9995
static void php_async_hook_release(php_async_hook_t *hook)
10096
{
101-
if (!hook->set) {
102-
return;
103-
}
104-
105-
zval_ptr_dtor(&hook->fci.function_name);
106-
107-
if (hook->fci.object != NULL) {
108-
OBJ_RELEASE(hook->fci.object);
109-
}
110-
97+
/* The scheduler object (and thus the bound method) is owned once by the
98+
* handlers container; nothing per-hook to release. */
11199
hook->set = false;
112100
}
113101

@@ -383,6 +371,11 @@ static void php_async_handlers_reset(void)
383371
php_async_hook_release(PHP_ASYNC_HOOK(id));
384372
}
385373

374+
if (PHP_ASYNC_HANDLERS.scheduler != NULL) {
375+
OBJ_RELEASE(PHP_ASYNC_HANDLERS.scheduler);
376+
PHP_ASYNC_HANDLERS.scheduler = NULL;
377+
}
378+
386379
if (PHP_ASYNC_HANDLERS.module != NULL) {
387380
zend_string_release(PHP_ASYNC_HANDLERS.module);
388381
PHP_ASYNC_HANDLERS.module = NULL;
@@ -451,11 +444,11 @@ static void php_async_build_api(zend_async_scheduler_api_t *api)
451444
ZEND_METHOD(Async_SchedulerHook, register)
452445
{
453446
zend_string *module;
454-
HashTable *hooks;
447+
zend_object *scheduler;
455448

456449
ZEND_PARSE_PARAMETERS_START(2, 2)
457450
Z_PARAM_STR(module)
458-
Z_PARAM_ARRAY_HT(hooks)
451+
Z_PARAM_OBJ_OF_CLASS(scheduler, async_ce_Scheduler)
459452
ZEND_PARSE_PARAMETERS_END();
460453

461454
/* A scheduler is registered once per process — by a C extension or by
@@ -466,12 +459,11 @@ ZEND_METHOD(Async_SchedulerHook, register)
466459
}
467460

468461
for (php_async_hook_id id = 0; id < PHP_ASYNC_HOOK_COUNT; id++) {
469-
if (!php_async_hook_take(hooks, id)) {
470-
php_async_handlers_reset();
471-
RETURN_THROWS();
472-
}
462+
php_async_hook_bind(scheduler, id);
473463
}
474464

465+
PHP_ASYNC_HANDLERS.scheduler = scheduler;
466+
GC_ADDREF(scheduler);
475467
PHP_ASYNC_HANDLERS.module = zend_string_copy(module);
476468
PHP_ASYNC_HANDLERS.active = true;
477469

@@ -533,8 +525,64 @@ ZEND_METHOD(Async_SchedulerHook, defer)
533525
}
534526
}
535527

528+
/////////////////////////////////////////////////////////////////////
529+
/// Async\AbstractScheduler default hooks
530+
/////////////////////////////////////////////////////////////////////
531+
532+
/* These bodies are the inert defaults: the bridge detects them by scope and
533+
* keeps the engine's own behaviour instead of calling them. They only run when
534+
* userland invokes them directly; the argument count/types are already checked
535+
* against the arginfo at the call boundary, so the bodies just return. */
536+
537+
ZEND_METHOD(Async_AbstractScheduler, interceptFiber)
538+
{
539+
RETURN_NULL();
540+
}
541+
542+
ZEND_METHOD(Async_AbstractScheduler, resume)
543+
{
544+
RETURN_FALSE;
545+
}
546+
547+
ZEND_METHOD(Async_AbstractScheduler, cancel)
548+
{
549+
RETURN_FALSE;
550+
}
551+
552+
ZEND_METHOD(Async_AbstractScheduler, defer)
553+
{
554+
RETURN_FALSE;
555+
}
556+
557+
ZEND_METHOD(Async_AbstractScheduler, launch)
558+
{
559+
RETURN_TRUE;
560+
}
561+
562+
ZEND_METHOD(Async_AbstractScheduler, shutdown)
563+
{
564+
RETURN_TRUE;
565+
}
566+
567+
ZEND_METHOD(Async_AbstractScheduler, contextFind)
568+
{
569+
RETURN_NULL();
570+
}
571+
572+
ZEND_METHOD(Async_AbstractScheduler, contextSet)
573+
{
574+
RETURN_FALSE;
575+
}
576+
577+
ZEND_METHOD(Async_AbstractScheduler, contextUnset)
578+
{
579+
RETURN_FALSE;
580+
}
581+
536582
void zend_register_scheduler_hook(void)
537583
{
584+
async_ce_Scheduler = register_class_Async_Scheduler();
585+
async_ce_AbstractScheduler = register_class_Async_AbstractScheduler(async_ce_Scheduler);
538586
register_class_Async_SchedulerHook();
539587
}
540588

Zend/zend_scheduler_hook.stub.php

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,103 @@
44

55
namespace Async;
66

7+
/**
8+
* A scheduler plugs into the concurrent mode by implementing this interface
9+
* and handing an instance to Async\SchedulerHook::register(). Each method is
10+
* a scheduling *policy* hook; the engine performs the actual context switches.
11+
*
12+
* Extend Async\AbstractScheduler to implement only the hooks you need.
13+
*/
14+
interface Scheduler
15+
{
16+
/** A coroutine became runnable (freshly created, or its wait ended). */
17+
public function enqueue(object $coroutine): bool;
18+
19+
/** The current flow yields; pick who runs next and switch to them. */
20+
public function suspend(bool $fromMain, bool $isBailout): bool;
21+
22+
/** A fiber is starting: return a coroutine object to adopt it, or null. */
23+
public function interceptFiber(\Fiber $fiber): ?object;
24+
25+
/** Wake a suspended coroutine (deferred: enqueue it to run later). */
26+
public function resume(object $coroutine, ?\Throwable $error = null): bool;
27+
28+
/** Cancel a coroutine. */
29+
public function cancel(object $coroutine, ?\Throwable $error = null): bool;
30+
31+
/** Store a one-shot microtask on the scheduler's queue. */
32+
public function defer(callable $task): bool;
33+
34+
/** Invoked once when the scheduler starts. */
35+
public function launch(): bool;
36+
37+
/** A graceful shutdown has been requested. */
38+
public function shutdown(): bool;
39+
40+
/** Look up a value in a coroutine context. */
41+
public function contextFind(object $context, mixed $key, bool $includeParent): mixed;
42+
43+
/** Set a value in a coroutine context. */
44+
public function contextSet(object $context, mixed $key, mixed $value): bool;
45+
46+
/** Remove a value from a coroutine context. */
47+
public function contextUnset(object $context, mixed $key): bool;
48+
}
49+
50+
/**
51+
* Convenience base: only enqueue() and suspend() are required; every other
52+
* hook has a default, so the engine keeps its own behaviour for the ones you
53+
* do not override.
54+
*/
55+
abstract class AbstractScheduler implements Scheduler
56+
{
57+
abstract public function enqueue(object $coroutine): bool;
58+
59+
abstract public function suspend(bool $fromMain, bool $isBailout): bool;
60+
61+
public function interceptFiber(\Fiber $fiber): ?object {}
62+
63+
public function resume(object $coroutine, ?\Throwable $error = null): bool {}
64+
65+
public function cancel(object $coroutine, ?\Throwable $error = null): bool {}
66+
67+
public function defer(callable $task): bool {}
68+
69+
public function launch(): bool {}
70+
71+
public function shutdown(): bool {}
72+
73+
public function contextFind(object $context, mixed $key, bool $includeParent): mixed {}
74+
75+
public function contextSet(object $context, mixed $key, mixed $value): bool {}
76+
77+
public function contextUnset(object $context, mixed $key): bool {}
78+
}
79+
780
/**
881
* Activation point for the concurrent mode.
982
*
10-
* A scheduler is registered by handing register() a map of hook name
11-
* (the class constants below) to callable. There is exactly one scheduler
12-
* per process, so the class is used only through its static methods.
83+
* A scheduler is registered by handing register() an object that implements
84+
* Async\Scheduler. There is exactly one scheduler per process, so the class
85+
* is used only through its static methods.
1386
*/
1487
final class SchedulerHook
1588
{
16-
public const string LAUNCH = 'launch';
17-
public const string SHUTDOWN = 'shutdown';
18-
public const string INTERCEPT_FIBER = 'intercept_fiber';
19-
public const string ENQUEUE = 'enqueue_coroutine';
20-
public const string SUSPEND = 'suspend';
21-
public const string RESUME = 'resume';
22-
public const string CANCEL = 'cancel';
23-
public const string CONTEXT_FIND = 'context_find';
24-
public const string CONTEXT_SET = 'context_set';
25-
public const string CONTEXT_UNSET = 'context_unset';
26-
public const string DEFER = 'defer';
27-
2889
/**
2990
* Registers a scheduler and activates the concurrent mode.
3091
*
31-
* $hooks maps a hook constant to a callable; omitted hooks keep their
32-
* defaults. A scheduler is registered once per process: calling this
33-
* when a scheduler is already registered (by a C extension or by an
34-
* earlier PHP call) throws an Error.
92+
* A scheduler is registered once per process: calling this when a
93+
* scheduler is already registered (by a C extension or by an earlier PHP
94+
* call) throws an Error.
3595
*/
36-
public static function register(string $module, array $hooks): bool {}
96+
public static function register(string $module, Scheduler $scheduler): bool {}
3797

3898
/** Returns the module name of the registered scheduler, or null when none. */
3999
public static function getModule(): ?string {}
40100

41101
/**
42102
* Queues a callable on the scheduler's microtask queue (one-shot,
43-
* runs on the next tick). Forwards to the DEFER hook; the queue and
44-
* its draining belong to the scheduler.
103+
* runs on the next tick). Forwards to the scheduler's defer() hook.
45104
*/
46105
public static function defer(callable $task): void {}
47-
48106
}

0 commit comments

Comments
 (0)