Skip to content

Async core#22561

Draft
EdmondDantes wants to merge 1 commit into
php:masterfrom
true-async:async-core
Draft

Async core#22561
EdmondDantes wants to merge 1 commit into
php:masterfrom
true-async:async-core

Conversation

@EdmondDantes

@EdmondDantes EdmondDantes commented Jul 2, 2026

Copy link
Copy Markdown

A lightweight asynchronous core without complex logic.

The idea is:
https://github.com/true-async/php-async-core-rfc/blob/main/RFC.md

At the moment, both the code and the RFC are still under development. I'd be happy to hear your ideas and feedback.

@EdmondDantes EdmondDantes marked this pull request as draft July 2, 2026 17:48
@EdmondDantes EdmondDantes force-pushed the async-core branch 4 times, most recently from 615d050 to ed9fe03 Compare July 2, 2026 18:11
@EdmondDantes EdmondDantes force-pushed the async-core branch 9 times, most recently from 635afa0 to f3b272d Compare July 3, 2026 09:08
The engine gains the ability to activate a concurrent execution mode.
A scheduler - a C extension or a PHP library - registers a set of
hooks through a single call and takes control of concurrency. The core
contains NO implementation: no scheduler, no queues, no event system;
it is hook routing plus the minimal mechanism only the engine can
perform (context switching, the GC destructor walker, the coroutine
lifecycle status).

Core ABI (Zend/zend_async_API.h):

  - zend_coroutine_t: lifecycle status packed into the flags word,
    modifier flags, object_offset for the single-allocation embed
    pattern (or a stored object pointer via OBJ_REF), an awaiting_info
    diagnostics hook. No embedded wait state.
  - scheduler slots registered once per process via a versioned struct:
    new_coroutine, enqueue_coroutine, suspend(from_main, is_bailout),
    resume, cancel, launch, shutdown, get_class_ce, call_on_main_stack,
    context accessors (userland zval keys + internal numeric keys with
    a core-owned key registry), intercept_fiber, gc_destructors, defer.
  - zend_async_microtask_t: a thin one-shot task (handler, dtor, 32-bit
    ref_count, named flag bits incl. is_cancelled). The queue is
    provider-owned; the defer slot only routes the pointer.
  - per-thread globals: state, current/main coroutine, live coroutine
    count, the in_scheduler_context flag, the PHP bridge hook storage.

Engine integration:

  - the scheduler is not lazy: it launches right before the script code
    runs (main.c, phpdbg) and receives the after-main handover; after
    destructors the API deactivates.
  - fibers: intercept_fiber links a fiber to a coroutine (the hook
    returns the coroutine to bind, created by the scheduler, or NULL
    for the legacy low-level path). There is NO switching API: inside
    scheduler code (in_scheduler_context, set around hook invocations)
    the plain Fiber API on a bound fiber performs the direct context
    switch; in application code the same calls park the value and
    route through the hooks. Deferred starts take an owned deep copy
    of their arguments. Fiber::suspend() is unchanged.
  - GC: the destructor phase is an around-interceptor. The engine keeps
    the walker, the once-per-object guarantee and the phase cursor, and
    re-runs missed destructors after the hook as a safety net; the hook
    brackets the run with provider logic (open a completion group, run,
    await everything the destructors spawned, transitively). Each
    destructor executes as application code (the scheduler flag is
    dropped around it). The executor reaches the hook as a Closure over
    an unregistered internal function - unreachable from application
    code.

PHP bridge (Zend/zend_scheduler_hook.c): final class Async\
SchedulerHook - hook-name constants, register(module, hooks) (throws
when a scheduler is already registered), getModule(), defer()
(forwards to the DEFER hook). Hooks are stored by numeric index in the
per-thread globals; each slot is backed by a C thunk forwarding to the
stored callable.

Tests (Zend/tests/async, 13): registration semantics, constants,
launch-at-registration, managed-fiber lifecycle through a plain-Fiber
scheduler loop (start/suspend/resume/finish, throw at the suspension
point, uncaught exception propagation, after-main drain, direct-
inside/routed-outside context semantics), low-level fibers untouched
by a null intercept, microtask forwarding, and a GC cycle whose
destructors spawn managed fibers awaited by the hook. Full async,
fibers and gc suites pass (194/194, debug build - leak-checked).

RFC and integration reference:
https://github.com/true-async/php-async-core-rfc
Comment thread Zend/zend_async_API.c
Comment on lines +311 to +315
if ((api->version >> 16) != ZEND_ASYNC_API_VERSION_MAJOR) {
zend_error(E_CORE_WARNING,
"Module %s was compiled against an incompatible Async API version", module);
return false;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not do separate API versioning from the PHP module API.

* when a scheduler is already registered (by a C extension or by an
* earlier PHP call) throws an Error.
*/
public static function register(string $module, array $hooks): bool {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add @param $module to explain what exactly module is supposed to mean? From my understanding simply a name to be returned with SchedulerHook::getModule()?

Comment thread Zend/zend_gc.c
zend_hrtime_t dtor_start_time = zend_hrtime();
if (EXPECTED(!EG(active_fiber))) {
gc_call_destructors(GC_FIRST_ROOT, end, NULL);
GC_G(dtor_phase_active) = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly does this provide over GC_G(active)?

Also isn't this a regression over the current behaviour, which always allows destructor calls?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also isn't this a regression over the current behaviour, which always allows destructor calls?

100% no. As for the flag, I'll take a look at it a bit later.

/**
* Activation point for the concurrent mode.
*
* A scheduler is registered by handing register() a map of hook name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give a rough example/rationale when it's beneficial to have custom userland hooks for the scheduler, rather than using pure fibers (like e.g. revolt does)?
From my uninformed perspective, it only makes sense for extensions to control this at that layer, not so much for userland?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give a rough example/rationale when it's beneficial to have custom userland hooks for the scheduler, rather than using pure fibers (like e.g. revolt does)? From my uninformed perspective, it only makes sense for extensions to control this at that layer, not so much for userland?

The idea is this:
https://github.com/true-async/php-async-core-rfc/blob/main/RFC.md

This can be viewed as a single API group together with the Poll API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants