diff --git a/Zend/zend.c b/Zend/zend.c index 2d8a0f455f8b4..de1bb7f7d2bbe 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -41,6 +41,7 @@ #include "Optimizer/zend_optimizer.h" #include "php.h" #include "php_globals.h" +#include "zend_async_API.h" // FIXME: Breaks the declaration of the function below #undef zenderror @@ -1349,6 +1350,10 @@ ZEND_API void zend_deactivate(void) /* {{{ */ /* shutdown_executor() takes care of its own bailout handling */ shutdown_executor(); + // The execution of the True Async API should end here, + // after the GC has been run. + ZEND_ASYNC_ENGINE_SHUTDOWN(); + zend_try { zend_ini_deactivate(); } zend_end_try(); @@ -1946,7 +1951,11 @@ ZEND_API zend_result zend_execute_script(int type, zval *retval, zend_file_handl if (Z_TYPE(EG(user_exception_handler)) != IS_UNDEF) { zend_user_exception_handler(); } - if (EG(exception)) { + + // If we are inside a coroutine, + // we do not call the final error handler, + // as the exception will be handled higher up in the method ZEND_ASYNC_RUN_SCHEDULER_AFTER_MAIN + if (false == ZEND_ASYNC_CURRENT_COROUTINE && EG(exception)) { ret = zend_exception_error(EG(exception), E_ERROR); } } diff --git a/Zend/zend_async_API.c b/Zend/zend_async_API.c new file mode 100644 index 0000000000000..66565e8e78d7c --- /dev/null +++ b/Zend/zend_async_API.c @@ -0,0 +1,1594 @@ +/* ++----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Edmond | + +----------------------------------------------------------------------+ +*/ +#include "zend_async_API.h" +#include "zend_exceptions.h" + +#ifdef ZTS +int zend_async_globals_id = 0; +size_t zend_async_globals_offset = 0; +#else +// The default data structure that is used when no other extensions are present. +zend_async_globals_t zend_async_globals_api = { 0 }; +#endif + +#define ASYNC_THROW_ERROR(error) zend_throw_error(NULL, error); + +/* Forward declarations */ +static void zend_async_main_handlers_shutdown(void); + +static zend_coroutine_t *spawn( + zend_async_scope_t *scope, zend_object *scope_provider, int32_t priority) +{ + ASYNC_THROW_ERROR("Async API is not enabled"); + return NULL; +} + +static void suspend(bool from_main) +{ +} + +static void enqueue_coroutine(zend_coroutine_t *coroutine) +{ + ASYNC_THROW_ERROR("Async API is not enabled"); +} + +static void void_stub(void) +{ +} + +static zend_array *get_coroutines_stub(void) +{ + return NULL; +} + +static zend_future_t *new_future_stub(bool thread_safe, size_t extra_size) +{ + ASYNC_THROW_ERROR("Async API is not enabled"); + return NULL; +} + +static zend_async_channel_t *new_channel_stub( + size_t buffer_size, bool resizable, bool thread_safe, size_t extra_size) +{ + ASYNC_THROW_ERROR("Async API is not enabled"); + return NULL; +} + +static zend_async_group_t *new_group_stub(size_t extra_size) +{ + ASYNC_THROW_ERROR("Async API is not enabled"); + return NULL; +} + +static zend_object *new_future_obj_stub(zend_future_t *future) +{ + ASYNC_THROW_ERROR("Async API is not enabled"); + return NULL; +} + +static zend_object *new_channel_obj_stub(zend_async_channel_t *channel) +{ + ASYNC_THROW_ERROR("Async API is not enabled"); + return NULL; +} + +static void add_microtask_stub(zend_async_microtask_t *microtask) +{ +} + +static zend_array *get_awaiting_info_stub(zend_coroutine_t *coroutine) +{ + return NULL; +} + +static bool spawn_and_throw(zend_object *exception, zend_async_scope_t *scope, int32_t priority) +{ + ASYNC_THROW_ERROR("Async API is not enabled"); + return false; +} + +static zend_async_context_t *new_context(void) +{ + ASYNC_THROW_ERROR("Context API is not enabled"); + return NULL; +} + +/* Internal context stubs removed - using direct implementation */ + +static zend_class_entry *get_class_ce(zend_async_class type) +{ + if (type == ZEND_ASYNC_EXCEPTION_DEFAULT || type == ZEND_ASYNC_EXCEPTION_DNS + || type == ZEND_ASYNC_EXCEPTION_POLL || type == ZEND_ASYNC_EXCEPTION_TIMEOUT + || type == ZEND_ASYNC_EXCEPTION_INPUT_OUTPUT) { + return zend_ce_exception; + } else if (type == ZEND_ASYNC_EXCEPTION_CANCELLATION) { + return zend_ce_error; + } + + return NULL; +} + +static zend_atomic_bool scheduler_lock = { 0 }; +static char *scheduler_module_name = NULL; +zend_async_spawn_t zend_async_spawn_fn = spawn; +zend_async_new_coroutine_t zend_async_new_coroutine_fn = NULL; +zend_async_new_scope_t zend_async_new_scope_fn = NULL; +zend_async_suspend_t zend_async_suspend_fn = suspend; +zend_async_enqueue_coroutine_t zend_async_enqueue_coroutine_fn = enqueue_coroutine; +zend_async_resume_t zend_async_resume_fn = NULL; +zend_async_cancel_t zend_async_cancel_fn = NULL; +zend_async_spawn_and_throw_t zend_async_spawn_and_throw_fn = spawn_and_throw; +zend_async_shutdown_t zend_async_shutdown_fn = void_stub; +zend_async_engine_shutdown_t zend_async_engine_shutdown_fn = void_stub; +zend_async_get_coroutines_t zend_async_get_coroutines_fn = get_coroutines_stub; +zend_async_add_microtask_t zend_async_add_microtask_fn = add_microtask_stub; +zend_async_get_awaiting_info_t zend_async_get_awaiting_info_fn = get_awaiting_info_stub; +zend_async_get_class_ce_t zend_async_get_class_ce_fn = get_class_ce; +zend_async_new_future_t zend_async_new_future_fn = new_future_stub; +zend_async_new_channel_t zend_async_new_channel_fn = new_channel_stub; +zend_async_new_future_obj_t zend_async_new_future_obj_fn = new_future_obj_stub; +zend_async_new_channel_obj_t zend_async_new_channel_obj_fn = new_channel_obj_stub; + +/* GROUP API */ +zend_async_new_group_t zend_async_new_group_fn = new_group_stub; + +static zend_atomic_bool reactor_lock = { 0 }; +static char *reactor_module_name = NULL; +zend_async_reactor_startup_t zend_async_reactor_startup_fn = NULL; +zend_async_reactor_shutdown_t zend_async_reactor_shutdown_fn = NULL; +zend_async_reactor_execute_t zend_async_reactor_execute_fn = NULL; +zend_async_reactor_loop_alive_t zend_async_reactor_loop_alive_fn = NULL; +zend_async_new_socket_event_t zend_async_new_socket_event_fn = NULL; +zend_async_new_poll_event_t zend_async_new_poll_event_fn = NULL; +zend_async_new_timer_event_t zend_async_new_timer_event_fn = NULL; +zend_async_new_signal_event_t zend_async_new_signal_event_fn = NULL; +zend_async_new_process_event_t zend_async_new_process_event_fn = NULL; +zend_async_new_thread_event_t zend_async_new_thread_event_fn = NULL; +zend_async_new_filesystem_event_t zend_async_new_filesystem_event_fn = NULL; + +zend_async_getnameinfo_t zend_async_getnameinfo_fn = NULL; +zend_async_getaddrinfo_t zend_async_getaddrinfo_fn = NULL; +zend_async_freeaddrinfo_t zend_async_freeaddrinfo_fn = NULL; + +zend_async_new_exec_event_t zend_async_new_exec_event_fn = NULL; +zend_async_exec_t zend_async_exec_fn = NULL; + +/* Trigger Event API */ +zend_async_new_trigger_event_t zend_async_new_trigger_event_fn = NULL; + +static zend_string *thread_pool_module_name = NULL; +zend_async_queue_task_t zend_async_queue_task_fn = NULL; + +/* Iterator API */ +zend_async_new_iterator_t zend_async_new_iterator_fn = NULL; + +/* Context API */ +zend_async_new_context_t zend_async_new_context_fn = new_context; + +/* Internal Context API - now uses direct functions */ + +ZEND_API bool zend_async_is_enabled(void) +{ + return scheduler_module_name != NULL && reactor_module_name != NULL; +} + +ZEND_API bool zend_scheduler_is_enabled(void) +{ + return scheduler_module_name != NULL; +} + +ZEND_API bool zend_async_reactor_is_enabled(void) +{ + return reactor_module_name != NULL; +} + +static void internal_globals_ctor(zend_async_globals_t *globals) +{ + globals->state = ZEND_ASYNC_OFF; + zend_atomic_bool_store(&globals->heartbeat, false); + globals->in_scheduler_context = false; + globals->graceful_shutdown = false; + globals->active_coroutine_count = 0; + globals->active_event_count = 0; + globals->coroutine = NULL; + globals->main_scope = NULL; + globals->scheduler = NULL; + globals->exit_exception = NULL; +} + +static void internal_globals_dtor(zend_async_globals_t *globals) +{ +} + +void zend_async_globals_ctor(void) +{ +#ifdef ZTS + ts_allocate_fast_id(&zend_async_globals_id, &zend_async_globals_offset, + sizeof(zend_async_globals_t), (ts_allocate_ctor) internal_globals_ctor, + (ts_allocate_dtor) internal_globals_dtor); + + ZEND_ASSERT(zend_async_globals_id != 0 && "zend_async_globals allocation failed"); + +#else + internal_globals_ctor(&zend_async_globals_api); +#endif +} + +void zend_async_globals_dtor(void) +{ +#ifdef ZTS +#else + internal_globals_dtor(&zend_async_globals_api); +#endif +} + +void zend_async_api_shutdown(void) +{ + zend_async_internal_context_api_shutdown(); + zend_async_main_handlers_shutdown(); +#ifndef ZTS + zend_async_globals_dtor(); +#endif +} + +ZEND_API int zend_async_get_api_version_number(void) +{ + return ZEND_ASYNC_API_VERSION_NUMBER; +} + +ZEND_API bool zend_async_scheduler_register(char *module, bool allow_override, + zend_async_new_coroutine_t new_coroutine_fn, zend_async_new_scope_t new_scope_fn, + zend_async_new_context_t new_context_fn, zend_async_spawn_t spawn_fn, + zend_async_suspend_t suspend_fn, zend_async_enqueue_coroutine_t enqueue_coroutine_fn, + zend_async_resume_t resume_fn, zend_async_cancel_t cancel_fn, + zend_async_spawn_and_throw_t spawn_and_throw_fn, zend_async_shutdown_t shutdown_fn, + zend_async_get_coroutines_t get_coroutines_fn, zend_async_add_microtask_t add_microtask_fn, + zend_async_get_awaiting_info_t get_awaiting_info_fn, + zend_async_get_class_ce_t get_class_ce_fn, zend_async_new_iterator_t new_iterator_fn, + zend_async_new_future_t new_future_fn, zend_async_new_channel_t new_channel_fn, + zend_async_new_future_obj_t new_future_obj_fn, + zend_async_new_channel_obj_t new_channel_obj_fn, zend_async_new_group_t new_group_fn, + zend_async_engine_shutdown_t engine_shutdown_fn) +{ + if (zend_atomic_bool_exchange(&scheduler_lock, 1)) { + return false; + } + + if (scheduler_module_name == module) { + return true; + } + + if (scheduler_module_name != NULL && false == allow_override) { + zend_atomic_bool_store(&scheduler_lock, 0); + zend_error(E_CORE_ERROR, + "The module %s is trying to override Scheduler, which was registered by the module " + "%s.", + module, scheduler_module_name); + return false; + } + + scheduler_module_name = module; + + zend_async_new_coroutine_fn = new_coroutine_fn; + zend_async_new_scope_fn = new_scope_fn; + zend_async_new_context_fn = new_context_fn; + zend_async_spawn_fn = spawn_fn; + zend_async_suspend_fn = suspend_fn; + zend_async_enqueue_coroutine_fn = enqueue_coroutine_fn; + zend_async_resume_fn = resume_fn; + zend_async_cancel_fn = cancel_fn; + zend_async_spawn_and_throw_fn = spawn_and_throw_fn; + zend_async_shutdown_fn = shutdown_fn; + zend_async_get_coroutines_fn = get_coroutines_fn; + zend_async_add_microtask_fn = add_microtask_fn; + zend_async_get_awaiting_info_fn = get_awaiting_info_fn; + zend_async_get_class_ce_fn = get_class_ce_fn; + zend_async_new_iterator_fn = new_iterator_fn; + zend_async_new_future_fn = new_future_fn; + zend_async_new_channel_fn = new_channel_fn; + zend_async_new_future_obj_fn = new_future_obj_fn; + zend_async_new_channel_obj_fn = new_channel_obj_fn; + zend_async_new_group_fn = new_group_fn; + zend_async_engine_shutdown_fn = engine_shutdown_fn; + + zend_atomic_bool_store(&scheduler_lock, 0); + + return true; +} + +ZEND_API bool zend_async_reactor_register(char *module, bool allow_override, + zend_async_reactor_startup_t reactor_startup_fn, + zend_async_reactor_shutdown_t reactor_shutdown_fn, + zend_async_reactor_execute_t reactor_execute_fn, + zend_async_reactor_loop_alive_t reactor_loop_alive_fn, + zend_async_new_socket_event_t new_socket_event_fn, + zend_async_new_poll_event_t new_poll_event_fn, + zend_async_new_timer_event_t new_timer_event_fn, + zend_async_new_signal_event_t new_signal_event_fn, + zend_async_new_process_event_t new_process_event_fn, + zend_async_new_thread_event_t new_thread_event_fn, + zend_async_new_filesystem_event_t new_filesystem_event_fn, + zend_async_getnameinfo_t getnameinfo_fn, zend_async_getaddrinfo_t getaddrinfo_fn, + zend_async_freeaddrinfo_t freeaddrinfo_fn, zend_async_new_exec_event_t new_exec_event_fn, + zend_async_exec_t exec_fn, zend_async_new_trigger_event_t new_trigger_event_fn) +{ + if (zend_atomic_bool_exchange(&reactor_lock, 1)) { + return false; + } + + if (reactor_module_name == module) { + return true; + } + + if (reactor_module_name != NULL && false == allow_override) { + zend_error(E_CORE_ERROR, + "The module %s is trying to override Reactor, which was registered by the module " + "%s.", + module, reactor_module_name); + return false; + } + + reactor_module_name = module; + + zend_async_reactor_startup_fn = reactor_startup_fn; + zend_async_reactor_shutdown_fn = reactor_shutdown_fn; + zend_async_reactor_execute_fn = reactor_execute_fn; + zend_async_reactor_loop_alive_fn = reactor_loop_alive_fn; + + zend_async_new_socket_event_fn = new_socket_event_fn; + zend_async_new_poll_event_fn = new_poll_event_fn; + zend_async_new_timer_event_fn = new_timer_event_fn; + zend_async_new_signal_event_fn = new_signal_event_fn; + + zend_async_new_process_event_fn = new_process_event_fn; + zend_async_new_thread_event_fn = new_thread_event_fn; + zend_async_new_filesystem_event_fn = new_filesystem_event_fn; + + zend_async_getnameinfo_fn = getnameinfo_fn; + zend_async_getaddrinfo_fn = getaddrinfo_fn; + zend_async_freeaddrinfo_fn = freeaddrinfo_fn; + + zend_async_new_exec_event_fn = new_exec_event_fn; + zend_async_exec_fn = exec_fn; + + zend_async_new_trigger_event_fn = new_trigger_event_fn; + + zend_atomic_bool_store(&reactor_lock, 0); + + return true; +} + +ZEND_API void zend_async_thread_pool_register( + zend_string *module, bool allow_override, zend_async_queue_task_t queue_task_fn) +{ + if (thread_pool_module_name != NULL && false == allow_override) { + zend_error(E_CORE_ERROR, + "The module %s is trying to override Thread Pool, which was registered by the " + "module %s.", + ZSTR_VAL(module), ZSTR_VAL(thread_pool_module_name)); + } + + if (thread_pool_module_name != NULL) { + zend_string_release(thread_pool_module_name); + } + + thread_pool_module_name = zend_string_copy(module); + zend_async_queue_task_fn = queue_task_fn; +} + +ZEND_API zend_string *zend_coroutine_gen_info( + zend_coroutine_t *coroutine, char *zend_coroutine_name) +{ + if (zend_coroutine_name == NULL) { + zend_coroutine_name = ""; + } + + if (ZEND_COROUTINE_SUSPENDED(coroutine)) { + return zend_strpprintf(0, "Coroutine spawned at %s:%d, suspended at %s:%d (%s)", + coroutine->filename ? ZSTR_VAL(coroutine->filename) : "", coroutine->lineno, + coroutine->waker->filename ? ZSTR_VAL(coroutine->waker->filename) : "", + coroutine->waker->lineno, zend_coroutine_name); + } else { + return zend_strpprintf(0, "Coroutine spawned at %s:%d (%s)", + coroutine->filename ? ZSTR_VAL(coroutine->filename) : "", coroutine->lineno, + zend_coroutine_name); + } +} + +static void event_callback_dispose(zend_async_event_callback_t *callback, zend_async_event_t *event) +{ + if (callback->ref_count > 1) { + // If the callback is still referenced, we cannot dispose it yet + callback->ref_count--; + return; + } + + callback->ref_count = 0; + efree(callback); +} + +ZEND_API zend_async_event_callback_t *zend_async_event_callback_new( + zend_async_event_callback_fn callback, size_t size) +{ + ZEND_ASSERT((size == 0 || size >= sizeof(zend_async_event_callback_t)) && + "Size must be at least sizeof(zend_async_event_callback_t)"); + + zend_async_event_callback_t *event_callback + = ecalloc(1, size != 0 ? size : sizeof(zend_async_event_callback_t)); + + event_callback->ref_count = 1; + event_callback->callback = callback; + event_callback->dispose = event_callback_dispose; + + return event_callback; +} + +void coroutine_event_callback_dispose( + zend_async_event_callback_t *callback, zend_async_event_t *event); + +ZEND_API zend_coroutine_event_callback_t *zend_async_coroutine_callback_new( + zend_coroutine_t *coroutine, zend_async_event_callback_fn callback, size_t size) +{ + ZEND_ASSERT((size == 0 || size >= sizeof(zend_coroutine_event_callback_t)) && + "Size must be at least sizeof(zend_coroutine_event_callback_t)"); + + zend_coroutine_event_callback_t *coroutine_callback + = ecalloc(1, size != 0 ? size : sizeof(zend_coroutine_event_callback_t)); + + coroutine_callback->base.ref_count = 1; + coroutine_callback->base.callback = callback; + coroutine_callback->coroutine = coroutine; + coroutine_callback->base.dispose = coroutine_event_callback_dispose; + + return coroutine_callback; +} + +////////////////////////////////////////////////////////////////////// +/* Waker API */ +////////////////////////////////////////////////////////////////////// + +static zend_always_inline zend_async_waker_trigger_t *waker_trigger_create( + zend_async_event_t *event, uint32_t initial_capacity) +{ + size_t total_size = sizeof(zend_async_waker_trigger_t) + + (initial_capacity - 1) * sizeof(zend_async_event_callback_t *); + zend_async_waker_trigger_t *trigger = (zend_async_waker_trigger_t *) emalloc(total_size); + + trigger->length = 0; + trigger->capacity = initial_capacity; + trigger->event = event; + + return trigger; +} + +static zend_always_inline zend_async_waker_trigger_t *waker_trigger_add_callback( + zend_async_waker_trigger_t *trigger, zend_async_event_callback_t *callback) +{ + if (trigger->length >= trigger->capacity) { + uint32_t new_capacity = trigger->capacity * 2; + size_t total_size = sizeof(zend_async_waker_trigger_t) + + (new_capacity - 1) * sizeof(zend_async_event_callback_t *); + + zend_async_waker_trigger_t *new_trigger + = (zend_async_waker_trigger_t *) erealloc(trigger, total_size); + new_trigger->capacity = new_capacity; + trigger = new_trigger; + } + + trigger->data[trigger->length++] = callback; + return trigger; +} + +static void waker_events_dtor(zval *item) +{ + zend_async_waker_trigger_t *trigger = Z_PTR_P(item); + zend_async_event_t *event = trigger->event; + trigger->event = NULL; + + if (event != NULL) { + // Remove all callbacks from the event + for (uint32_t i = 0; i < trigger->length; i++) { + if (trigger->data[i] != NULL) { + event->del_callback(event, trigger->data[i]); + } + } + // + // At this point, we explicitly stop the event because it is no longer being listened to by + // our handlers. However, this does not mean the object is destroyed—it may remain in memory + // if something still holds a reference to it. + // + event->stop(event); + ZEND_ASYNC_EVENT_RELEASE(event); + } + + // Free the entire trigger (includes flexible array member) + efree(trigger); +} + +static void waker_triggered_events_dtor(zval *item) +{ + zend_async_event_t *event = Z_PTR_P(item); + + ZEND_ASYNC_EVENT_RELEASE(event); +} + +ZEND_API zend_async_waker_t *zend_async_waker_define(zend_coroutine_t *coroutine) +{ + if (coroutine == NULL) { + coroutine = ZEND_ASYNC_CURRENT_COROUTINE; + } + + if (UNEXPECTED(coroutine == NULL)) { + zend_error(E_CORE_ERROR, "Cannot create waker for a coroutine that is not running"); + return NULL; + } + + if (UNEXPECTED(coroutine->waker != NULL)) { + return coroutine->waker; + } + + return zend_async_waker_new(coroutine); +} + +ZEND_API zend_async_waker_t *zend_async_waker_new(zend_coroutine_t *coroutine) +{ + if (coroutine == NULL) { + coroutine = ZEND_ASYNC_CURRENT_COROUTINE; + } + + if (UNEXPECTED(coroutine == NULL)) { + zend_error(E_CORE_ERROR, "Cannot create waker for a coroutine that is not running"); + return NULL; + } + + if (UNEXPECTED(coroutine->waker != NULL)) { + zend_async_waker_destroy(coroutine); + } + + zend_async_waker_t *waker = pecalloc(1, sizeof(zend_async_waker_t), 0); + + waker->triggered_events = NULL; + waker->error = NULL; + waker->dtor = NULL; + ZVAL_UNDEF(&waker->result); + + zend_hash_init(&waker->events, 2, NULL, waker_events_dtor, 0); + + coroutine->waker = waker; + + return waker; +} + +ZEND_API void zend_async_waker_destroy(zend_coroutine_t *coroutine) +{ + if (UNEXPECTED(coroutine->waker == NULL)) { + return; + } + + if (coroutine->waker->dtor != NULL) { + coroutine->waker->dtor(coroutine); + } + + zend_async_waker_t *waker = coroutine->waker; + coroutine->waker = NULL; + + // default dtor + if (waker->error != NULL) { + zend_object_release(waker->error); + waker->error = NULL; + } + + if (waker->triggered_events != NULL) { + zend_hash_destroy(waker->triggered_events); + efree(waker->triggered_events); + waker->triggered_events = NULL; + } + + if (waker->filename != NULL) { + zend_string_release(waker->filename); + waker->filename = NULL; + waker->lineno = 0; + } + + zval_ptr_dtor(&waker->result); + zend_hash_destroy(&waker->events); + efree(waker); +} + +void coroutine_event_callback_dispose( + zend_async_event_callback_t *callback, zend_async_event_t *event) +{ + if (callback->ref_count > 1) { + // If the callback is still referenced, we cannot dispose it yet + callback->ref_count--; + return; + } else if (UNEXPECTED(callback->ref_count == 0)) { + // Circular free from destructor + return; + } else { + ZEND_ASSERT(callback->ref_count > 0 + && "Callback ref_count must be greater than 0. Memory corruption detected."); + } + + callback->ref_count = 0; + + const zend_coroutine_t *coroutine = ((zend_coroutine_event_callback_t *) callback)->coroutine; + + if (EXPECTED(coroutine != NULL)) { + zend_async_waker_t *waker = coroutine->waker; + + if (event != NULL && waker != NULL) { + // Find the trigger for this event + zval *trigger_zval = zend_hash_index_find(&waker->events, (zend_ulong) event); + + if (trigger_zval != NULL) { + zend_async_waker_trigger_t *trigger = Z_PTR_P(trigger_zval); + + // Remove only this specific callback from the trigger + for (uint32_t i = 0; i < trigger->length; i++) { + if (trigger->data[i] == callback) { + // Move last element to current position (O(1) removal) + trigger->data[i] = trigger->data[--trigger->length]; + break; + } + } + + // If no more callbacks in trigger, remove the entire event + if (trigger->length == 0) { + zend_hash_index_del(&waker->events, (zend_ulong) event); + + if (waker->triggered_events != NULL) { + zend_hash_index_del(waker->triggered_events, (zend_ulong) event); + } + } + } + } + } + + efree(callback); +} + +ZEND_API void zend_async_waker_add_triggered_event( + zend_coroutine_t *coroutine, zend_async_event_t *event) +{ + if (UNEXPECTED(coroutine->waker == NULL)) { + return; + } + + if (coroutine->waker->triggered_events == NULL) { + coroutine->waker->triggered_events = (HashTable *) emalloc(sizeof(HashTable)); + zend_hash_init(coroutine->waker->triggered_events, 2, NULL, waker_triggered_events_dtor, 0); + } + + if (EXPECTED(zend_hash_index_add_ptr( + coroutine->waker->triggered_events, (zend_ulong) event, event) + != NULL)) { + ZEND_ASYNC_EVENT_ADD_REF(event); + } +} + +ZEND_API bool zend_async_waker_is_event_exists( + zend_coroutine_t *coroutine, zend_async_event_t *event) +{ + if (UNEXPECTED(coroutine->waker == NULL)) { + return false; + } + + return zend_hash_index_find(&coroutine->waker->events, (zend_ulong) event) != NULL; +} + +ZEND_API void zend_async_resume_when(zend_coroutine_t *coroutine, zend_async_event_t *event, + const bool trans_event, zend_async_event_callback_fn callback, + zend_coroutine_event_callback_t *event_callback) +{ + zend_exception_save(); + + bool locally_allocated_callback = false; + + if (UNEXPECTED(ZEND_ASYNC_EVENT_IS_CLOSED(event))) { + zend_throw_error(NULL, "The event cannot be used after it has been terminated"); + zend_exception_restore(); + return; + } + + if (UNEXPECTED(callback == NULL && event_callback == NULL)) { + zend_error(E_WARNING, "Callback cannot be NULL"); + + if (trans_event) { + event->dispose(event); + } + + zend_exception_restore(); + return; + } + + if (event_callback == NULL) { + event_callback = emalloc(sizeof(zend_coroutine_event_callback_t)); + event_callback->base.ref_count = 1; + event_callback->base.callback = callback; + event_callback->base.dispose = coroutine_event_callback_dispose; + locally_allocated_callback = true; + } + + // Set up the default dispose function if not set + if (event_callback->base.dispose == NULL) { + event_callback->base.dispose = coroutine_event_callback_dispose; + } + + if (event_callback->base.ref_count == 0) { + event_callback->base.ref_count = 1; + } + + ZEND_ASSERT(event_callback->base.ref_count > 0 && "Callback ref_count must be greater than 0."); + + event_callback->coroutine = coroutine; + event->add_callback(event, &event_callback->base); + + if (UNEXPECTED(EG(exception) != NULL)) { + if (locally_allocated_callback) { + event_callback->base.dispose(&event_callback->base, event); + } + + if (trans_event) { + event->dispose(event); + } + + zend_exception_restore(); + return; + } + + if (EXPECTED(coroutine->waker != NULL)) { + zval *trigger_zval = zend_hash_index_find(&coroutine->waker->events, (zend_ulong) event); + zend_async_waker_trigger_t *trigger; + + if (UNEXPECTED(trigger_zval != NULL)) { + // Event already exists, add callback to existing trigger + trigger = Z_PTR_P(trigger_zval); + trigger = waker_trigger_add_callback(trigger, &event_callback->base); + // Update the hash table entry with potentially new pointer after realloc + Z_PTR_P(trigger_zval) = trigger; + } else { + // New event, create new trigger + trigger = waker_trigger_create(event, 1); + trigger = waker_trigger_add_callback(trigger, &event_callback->base); + + if (UNEXPECTED(zend_hash_index_add_ptr( + &coroutine->waker->events, (zend_ulong) event, trigger) + == NULL)) { + // This should not happen with new events, but handle gracefully + efree(trigger); + + event_callback->coroutine = NULL; + event->del_callback(event, &event_callback->base); + + event_callback->coroutine = NULL; + event->del_callback(event, &event_callback->base); + + if (locally_allocated_callback) { + event_callback->base.dispose(&event_callback->base, event); + } + + if (trans_event) { + event->dispose(event); + } + + zend_throw_error(NULL, "Failed to add event to the waker"); + } else { + if (false == trans_event) { + ZEND_ASYNC_EVENT_ADD_REF(event); + } + } + } + } + + zend_exception_restore(); +} + +ZEND_API void zend_async_waker_callback_resolve(zend_async_event_t *event, + zend_async_event_callback_t *callback, void *result, zend_object *exception) +{ + zend_coroutine_t *coroutine = ((zend_coroutine_event_callback_t *) callback)->coroutine; + + if (exception == NULL && coroutine->waker != NULL) { + + if (coroutine->waker->triggered_events == NULL) { + coroutine->waker->triggered_events = (HashTable *) emalloc(sizeof(HashTable)); + zend_hash_init( + coroutine->waker->triggered_events, 2, NULL, waker_triggered_events_dtor, 0); + } + + if (EXPECTED(zend_hash_index_add_ptr( + coroutine->waker->triggered_events, (zend_ulong) event, event) + != NULL)) { + ZEND_ASYNC_EVENT_ADD_REF(event); + } + + // Copy the result to the waker if it is not NULL + if (ZEND_ASYNC_EVENT_WILL_ZVAL_RESULT(event) && result != NULL) { + ZVAL_COPY(&coroutine->waker->result, result); + } + } + + if (exception != NULL) { + // + // This handler always captures the exception as handled because it passes it to another + // coroutine. As a result, the exception will not propagate further. + // + ZEND_ASYNC_EVENT_SET_EXCEPTION_HANDLED(event); + } + + ZEND_ASYNC_RESUME_WITH_ERROR(coroutine, exception, false); +} + +ZEND_API void zend_async_waker_callback_cancel(zend_async_event_t *event, + zend_async_event_callback_t *callback, void *result, zend_object *exception) +{ + zend_coroutine_t *coroutine = ((zend_coroutine_event_callback_t *) callback)->coroutine; + + if (UNEXPECTED(exception != NULL)) { + ZEND_ASYNC_RESUME_WITH_ERROR(coroutine, exception, false); + } else { + ZEND_ASYNC_RESUME_WITH_ERROR(coroutine, + zend_async_new_exception( + ZEND_ASYNC_EXCEPTION_CANCELLATION, "Operation has been cancelled"), + true); + } +} + +ZEND_API void zend_async_waker_callback_timeout(zend_async_event_t *event, + zend_async_event_callback_t *callback, void *result, zend_object *exception) +{ + if (UNEXPECTED(exception != NULL)) { + ZEND_ASYNC_RESUME_WITH_ERROR( + ((zend_coroutine_event_callback_t *) callback)->coroutine, exception, false); + } else { + exception = zend_async_new_exception( + ZEND_ASYNC_EXCEPTION_TIMEOUT, "Operation has been cancelled by timeout"); + + ZEND_ASYNC_RESUME_WITH_ERROR( + ((zend_coroutine_event_callback_t *) callback)->coroutine, exception, true); + } +} + +ZEND_API zend_async_waker_t *zend_async_waker_new_with_timeout( + zend_coroutine_t *coroutine, const zend_ulong timeout, zend_async_event_t *cancellation) +{ + if (coroutine == NULL) { + coroutine = ZEND_ASYNC_CURRENT_COROUTINE; + } + + if (UNEXPECTED(coroutine == NULL)) { + zend_error(E_CORE_ERROR, "Cannot create waker for a coroutine that is not running"); + return NULL; + } + + zend_async_waker_t *waker = zend_async_waker_new(coroutine); + + if (timeout > 0) { + zend_async_resume_when(coroutine, &ZEND_ASYNC_NEW_TIMER_EVENT(timeout, false)->base, true, + zend_async_waker_callback_resolve, NULL); + } + + if (cancellation != NULL) { + zend_async_resume_when( + coroutine, cancellation, false, zend_async_waker_callback_cancel, NULL); + } + + return waker; +} + +ZEND_API bool zend_async_waker_apply_error(zend_async_waker_t *waker, zend_object *error, + bool tranfer_error, bool override, bool for_cancellation) +{ + if (UNEXPECTED(waker == NULL)) { + if (tranfer_error) { + OBJ_RELEASE(error); + } + return false; + } + + if (EXPECTED(waker->error == NULL)) { + waker->error = error; + if (false == tranfer_error) { + GC_ADDREF(error); + } + return true; + } + + const zend_class_entry *ce_cancellation_exception = ZEND_ASYNC_GET_EXCEPTION_CE(ZEND_ASYNC_EXCEPTION_CANCELLATION); + + if (for_cancellation && instanceof_function(waker->error->ce, ce_cancellation_exception)) { + // If the waker already has a cancellation exception, we do not override it + if (tranfer_error) { + OBJ_RELEASE(error); + } + return false; + } + + if (override) { + zend_exception_set_previous(error, waker->error); + waker->error = error; + } else { + zend_exception_set_previous(waker->error, error); + } + + if (false == tranfer_error) { + GC_ADDREF(error); + } + + return true; +} + +////////////////////////////////////////////////////////////////////// +/* Waker API end */ +////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////// +/* Exception API */ +////////////////////////////////////////////////////////////////////// + +ZEND_API ZEND_COLD zend_object *zend_async_new_exception( + zend_async_class type, const char *format, ...) +{ + if (type == ZEND_ASYNC_CLASS_NO) { + type = ZEND_ASYNC_EXCEPTION_DEFAULT; + } + + zend_class_entry *exception_ce = ZEND_ASYNC_GET_EXCEPTION_CE(type); + zval exception, message_val; + + ZEND_ASSERT(instanceof_function(exception_ce, zend_ce_throwable) + && "Exceptions must implement Throwable"); + + object_init_ex(&exception, exception_ce); + + va_list args; + va_start(args, format); + zend_string *message = zend_vstrpprintf(0, format, args); + va_end(args); + + if (message) { + ZVAL_STR(&message_val, message); + zend_update_property_ex( + exception_ce, Z_OBJ(exception), ZSTR_KNOWN(ZEND_STR_MESSAGE), &message_val); + } + + zend_string_release(message); + + return Z_OBJ(exception); +} + +ZEND_API ZEND_COLD zend_object *zend_async_throw(zend_async_class type, const char *format, ...) +{ + if (type == ZEND_ASYNC_CLASS_NO) { + type = ZEND_ASYNC_EXCEPTION_DEFAULT; + } + + va_list args; + va_start(args, format); + zend_string *message = zend_vstrpprintf(0, format, args); + va_end(args); + + zend_object *obj + = zend_throw_exception(ZEND_ASYNC_GET_EXCEPTION_CE(type), ZSTR_VAL(message), 0); + zend_string_release(message); + return obj; +} + +ZEND_API ZEND_COLD zend_object *zend_async_throw_cancellation(const char *format, ...) +{ + const zend_object *previous = EG(exception); + + if (format == NULL && previous != NULL + && instanceof_function( + previous->ce, ZEND_ASYNC_GET_EXCEPTION_CE(ZEND_ASYNC_EXCEPTION_TIMEOUT))) { + format = "The operation was canceled by timeout"; + } else { + format = format ? format : "The operation was canceled"; + } + + va_list args; + va_start(args, format); + + zend_object *obj = zend_throw_exception_ex( + ZEND_ASYNC_GET_EXCEPTION_CE(ZEND_ASYNC_EXCEPTION_CANCELLATION), 0, format, args); + + va_end(args); + return obj; +} + +ZEND_API ZEND_COLD zend_object *zend_async_throw_timeout( + const char *format, const zend_long timeout) +{ + format = format ? format : "A timeout of %u microseconds occurred"; + + return zend_throw_exception_ex( + ZEND_ASYNC_GET_EXCEPTION_CE(ZEND_ASYNC_EXCEPTION_TIMEOUT), 0, format, timeout); +} + +////////////////////////////////////////////////////////////////////// +/* Exception API end */ +////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////// +/// ### Internal Coroutine Context +/// +/// The internal coroutine context is intended for use in PHP-extensions. +/// The flow is as follows: +/// +/// 1. The extension registers a unique numeric index by `zend_async_internal_context_key_alloc`, +/// described by a static C string. +/// 2. The **TrueAsync API** verifies the uniqueness of the mapping between the index and the C +/// string address. +/// 3. The extension uses `zend_async_internal_context_*` functions to get, set, or unset keys. +/// 4. Keys are automatically destroyed when the coroutine completes. +////////////////////////////////////////////////////////////////////// + +// Global variables for internal context key management +static HashTable *zend_async_context_key_names = NULL; + +#ifdef ZTS +static MUTEX_T zend_async_context_mutex = NULL; +#endif + +void zend_async_init_internal_context_api(void) +{ +#ifdef ZTS + zend_async_context_mutex = tsrm_mutex_alloc(); +#endif + // Initialize key names table - stores string pointers directly + zend_async_context_key_names = pemalloc(sizeof(HashTable), 1); + zend_hash_init(zend_async_context_key_names, 8, NULL, NULL, + 1); // No destructor - we don't own the strings +} + +uint32_t zend_async_internal_context_key_alloc(const char *key_name) +{ +#ifdef ZTS + tsrm_mutex_lock(zend_async_context_mutex); +#endif + + // Check if this string address already has a key + const char **existing_ptr; + ZEND_HASH_FOREACH_NUM_KEY_PTR( + zend_async_context_key_names, zend_ulong existing_key, existing_ptr) + { + if (*existing_ptr == key_name) { + // Found existing key for this string address +#ifdef ZTS + tsrm_mutex_unlock(zend_async_context_mutex); +#endif + return (uint32_t) existing_key; + } + } + ZEND_HASH_FOREACH_END(); + + // Use next available index as key + uint32_t key = zend_hash_num_elements(zend_async_context_key_names) + 1; + + // Store string pointer directly + zend_hash_index_add_ptr(zend_async_context_key_names, key, (void *) key_name); + +#ifdef ZTS + tsrm_mutex_unlock(zend_async_context_mutex); +#endif + + return key; +} + +const char *zend_async_internal_context_key_name(uint32_t key) +{ + if (zend_async_context_key_names == NULL) { + return NULL; + } + +#ifdef ZTS + tsrm_mutex_lock(zend_async_context_mutex); +#endif + + const char *name = zend_hash_index_find_ptr(zend_async_context_key_names, key); + +#ifdef ZTS + tsrm_mutex_unlock(zend_async_context_mutex); +#endif + + return name; +} + +zval *zend_async_internal_context_find(zend_coroutine_t *coroutine, uint32_t key) +{ + if (coroutine == NULL || coroutine->internal_context == NULL) { + return NULL; + } + + return zend_hash_index_find(coroutine->internal_context, key); +} + +void zend_async_internal_context_set(zend_coroutine_t *coroutine, uint32_t key, zval *value) +{ + if (coroutine == NULL) { + return; + } + + // Initialize internal_context if needed + if (coroutine->internal_context == NULL) { + coroutine->internal_context = zend_new_array(0); + } + + // Set the value + zval copy; + ZVAL_COPY(©, value); + zend_hash_index_update(coroutine->internal_context, key, ©); +} + +bool zend_async_internal_context_unset(zend_coroutine_t *coroutine, uint32_t key) +{ + if (coroutine == NULL || coroutine->internal_context == NULL) { + return false; + } + + return zend_hash_index_del(coroutine->internal_context, key) == SUCCESS; +} + +void zend_async_coroutine_internal_context_dispose(zend_coroutine_t *coroutine) +{ + if (coroutine->internal_context != NULL) { + zend_array_release(coroutine->internal_context); + coroutine->internal_context = NULL; + } +} + +void zend_async_internal_context_api_shutdown(void) +{ +#ifdef ZTS + if (zend_async_context_mutex != NULL) { + tsrm_mutex_lock(zend_async_context_mutex); + } +#endif + + if (zend_async_context_key_names != NULL) { + zend_hash_destroy(zend_async_context_key_names); + pefree(zend_async_context_key_names, 1); + zend_async_context_key_names = NULL; + } + +#ifdef ZTS + if (zend_async_context_mutex != NULL) { + tsrm_mutex_unlock(zend_async_context_mutex); + tsrm_mutex_free(zend_async_context_mutex); + zend_async_context_mutex = NULL; + } +#endif +} + +void zend_async_coroutine_internal_context_init(zend_coroutine_t *coroutine) +{ + coroutine->internal_context = NULL; +} +////////////////////////////////////////////////////////////////////// +/* Internal Context API end */ +////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////// +/* Callback Vector Implementation */ +////////////////////////////////////////////////////////////////////// + +/* Private helper functions for iterator management - static inline for performance */ + +/* Register the single iterator - prevents concurrent iterations */ +static zend_always_inline bool zend_async_callbacks_register_iterator( + zend_async_callbacks_vector_t *vector, uint32_t *iterator_index) +{ + if (vector->current_iterator != NULL) { + // Concurrent iteration detected - this is not allowed + return false; + } + + vector->current_iterator = iterator_index; + return true; +} + +/* Unregister the iterator after iteration completes */ +static zend_always_inline void zend_async_callbacks_unregister_iterator( + zend_async_callbacks_vector_t *vector) +{ + vector->current_iterator = NULL; +} + +/* Adjust the active iterator when an element is removed */ +static zend_always_inline void zend_async_callbacks_adjust_iterator( + zend_async_callbacks_vector_t *vector, uint32_t removed_index) +{ + if (vector->current_iterator != NULL && *vector->current_iterator > removed_index) { + (*vector->current_iterator)--; + } +} + +/* Public API implementations */ + +/* Remove a specific callback; order is NOT preserved, but iterator is safely adjusted */ +ZEND_API void zend_async_callbacks_remove( + zend_async_event_t *event, zend_async_event_callback_t *callback) +{ + zend_async_callbacks_vector_t *vector = &event->callbacks; + + for (uint32_t i = 0; i < vector->length; ++i) { + if (vector->data[i] == callback) { + // Adjust the active iterator before performing the removal + zend_async_callbacks_adjust_iterator(vector, i); + + // O(1) removal: move last element to current position + vector->data[i] = vector->length > 0 ? vector->data[--vector->length] : NULL; + callback->dispose(callback, event); + return; + } + } +} + +/* Call all callbacks with safe iterator tracking to handle concurrent modifications */ +ZEND_API void zend_async_callbacks_notify( + zend_async_event_t *event, void *result, zend_object *exception, bool from_handler) +{ + // Protect from self-deletion by incrementing ref count before calling callbacks + ZEND_ASYNC_EVENT_ADD_REF(event); + // Automatically clear exception handled flag + ZEND_ASYNC_EVENT_CLR_EXCEPTION_HANDLED(event); + + // If pre-notify returns false, we stop notifying callbacks + if (false == from_handler && event->notify_handler != NULL) { + event->notify_handler(event, result, exception); + ZEND_ASYNC_EVENT_RELEASE(event); + return; + } + + if (event->callbacks.data == NULL || event->callbacks.length == 0) { + ZEND_ASYNC_EVENT_RELEASE(event); + return; + } + + zend_async_callbacks_vector_t *vector = &event->callbacks; + uint32_t current_index = 0; + + // Register iterator - prevents concurrent iterations + if (!zend_async_callbacks_register_iterator(vector, ¤t_index)) { + zend_error(E_CORE_WARNING, + "Concurrent callback iteration detected - nested notify() calls are not allowed"); + ZEND_ASYNC_EVENT_RELEASE(event); + return; + } + + // Iterate through callbacks with safe index tracking + while (current_index < vector->length) { + // Store current callback (it might be moved during execution) + zend_async_event_callback_t *callback = vector->data[current_index]; + + // Move to next callback + // Note: current_index may have been adjusted by zend_async_callbacks_adjust_iterator + // if a callback was removed during this iteration + current_index++; + + // Execute callback + callback->callback(event, callback, result, exception); + + if (UNEXPECTED(EG(exception) != NULL)) { + break; + } + } + + // Unregister iterator + zend_async_callbacks_unregister_iterator(vector); + + // Dispose the reference we added at the beginning - this may trigger disposal if ref count + // reaches 0 + ZEND_ASYNC_EVENT_RELEASE(event); +} + +/* Call all callbacks and close the event (Like future) */ +ZEND_API void zend_async_callbacks_notify_and_close( + zend_async_event_t *event, void *result, zend_object *exception) +{ + event->stop(event); + ZEND_ASYNC_EVENT_SET_CLOSED(event); + zend_async_callbacks_notify(event, result, exception, false); +} + +/* Free the vector's memory including iterator tracking */ +ZEND_API void zend_async_callbacks_free(zend_async_event_t *event) +{ + if (event->callbacks.data == NULL) { + return; + } + + zend_async_callbacks_vector_t *vector = &event->callbacks; + uint32_t current_index = 0; + + // Register iterator - prevents concurrent iterations + if (!zend_async_callbacks_register_iterator(vector, ¤t_index)) { + zend_error(E_CORE_WARNING, + "Concurrent callback iteration detected - nested free() calls are not allowed"); + return; + } + + // Dispose all callbacks + while (current_index < vector->length) { + zend_async_event_callback_t *callback = vector->data[current_index]; + current_index++; + callback->dispose(callback, event); + } + + // Free memory + efree(vector->data); + + // Unregister iterator + zend_async_callbacks_unregister_iterator(vector); + + // Reset all fields + vector->data = NULL; + vector->length = 0; + vector->capacity = 0; + vector->current_iterator = NULL; +} + +/////////////////////////////////////////////////////////////// +/// Socket Listening API Registration +/////////////////////////////////////////////////////////////// +/* Socket listening stubs */ +static zend_async_listen_event_t *socket_listen_stub( + const char *host, int port, int backlog, size_t extra_size) +{ + ASYNC_THROW_ERROR("Socket listening API is not enabled"); + return NULL; +} + +/* Socket listening function pointers */ +ZEND_API zend_async_socket_listen_t zend_async_socket_listen_fn = socket_listen_stub; + +/* Registration lock for socket listening */ +static zend_atomic_bool socket_listening_lock = { 0 }; +static char *socket_listening_module_name = NULL; + +ZEND_API bool zend_async_socket_listening_register( + char *module, bool allow_override, zend_async_socket_listen_t socket_listen_fn) +{ + if (zend_atomic_bool_exchange(&socket_listening_lock, 1)) { + return false; + } + + if (socket_listening_module_name == module) { + return true; + } + + if (socket_listening_module_name != NULL && false == allow_override) { + zend_error(E_CORE_ERROR, + "The module %s is trying to override Socket Listening API, which was registered by " + "the module %s.", + module, socket_listening_module_name); + return false; + } + + socket_listening_module_name = module; + zend_async_socket_listen_fn = socket_listen_fn; + + return true; +} + +/////////////////////////////////////////////////////////////// +/// Coroutine Switch Handlers Implementation +/////////////////////////////////////////////////////////////// + +ZEND_API void zend_coroutine_switch_handlers_init(zend_coroutine_t *coroutine) +{ + if (coroutine->switch_handlers) { + return; /* Already initialized */ + } + + coroutine->switch_handlers = emalloc(sizeof(zend_coroutine_switch_handlers_vector_t)); + coroutine->switch_handlers->length = 0; + coroutine->switch_handlers->capacity = 0; + coroutine->switch_handlers->data = NULL; + coroutine->switch_handlers->in_execution = false; +} + +ZEND_API void zend_coroutine_switch_handlers_destroy(zend_coroutine_t *coroutine) +{ + if (!coroutine->switch_handlers) { + return; + } + + if (coroutine->switch_handlers->data) { + efree(coroutine->switch_handlers->data); + } + efree(coroutine->switch_handlers); + coroutine->switch_handlers = NULL; +} + +ZEND_API uint32_t zend_coroutine_add_switch_handler( + zend_coroutine_t *coroutine, zend_coroutine_switch_handler_fn handler) +{ + if (!handler) { + zend_error(E_WARNING, "Cannot add NULL switch handler"); + return 0; + } + + if (!coroutine->switch_handlers) { + zend_coroutine_switch_handlers_init(coroutine); + } + + zend_coroutine_switch_handlers_vector_t *vector = coroutine->switch_handlers; + + if (vector->in_execution) { + zend_error(E_WARNING, "Cannot add switch handler during handler execution"); + return 0; + } + + /* Check for duplicate handler */ + for (uint32_t i = 0; i < vector->length; i++) { + if (vector->data[i].handler == handler) { + return i; + } + } + + /* Expand array if needed */ + if (vector->length == vector->capacity) { + vector->capacity = vector->capacity ? vector->capacity * 2 : 4; + vector->data = safe_erealloc( + vector->data, vector->capacity, sizeof(zend_coroutine_switch_handler_t), 0); + } + + /* Add handler */ + uint32_t index = vector->length; + vector->data[index].handler = handler; + vector->length++; + + return index; +} + +ZEND_API bool zend_coroutine_remove_switch_handler( + zend_coroutine_t *coroutine, uint32_t handler_index) +{ + if (!coroutine->switch_handlers) { + return false; + } + + zend_coroutine_switch_handlers_vector_t *vector = coroutine->switch_handlers; + + if (vector->in_execution) { + zend_error(E_WARNING, "Cannot remove switch handler during handler execution"); + return false; + } + + if (handler_index >= vector->length) { + return false; + } + + /* Shift elements to remove the handler */ + for (uint32_t i = handler_index; i < vector->length - 1; i++) { + vector->data[i] = vector->data[i + 1]; + } + + vector->length--; + return true; +} + +ZEND_API void zend_coroutine_call_switch_handlers( + zend_coroutine_t *coroutine, bool is_enter, bool is_finishing) +{ + if (!coroutine->switch_handlers || coroutine->switch_handlers->length == 0) { + return; + } + + zend_coroutine_switch_handlers_vector_t *vector = coroutine->switch_handlers; + + /* Set execution protection flag */ + vector->in_execution = true; + + /* Call all handlers and remove those that return false */ + uint32_t write_index = 0; + for (uint32_t read_index = 0; read_index < vector->length; read_index++) { + bool keep_handler = vector->data[read_index].handler(coroutine, is_enter, is_finishing); + + if (keep_handler) { + /* Keep this handler - move it to write position if needed */ + if (write_index != read_index) { + vector->data[write_index] = vector->data[read_index]; + } + write_index++; + } + /* If keep_handler is false, we skip copying this handler (effectively removing it) */ + } + + /* Update length to reflect removed handlers */ + vector->length = write_index; + + /* Clear execution protection flag */ + vector->in_execution = false; + + /* Free vector memory if no handlers remain */ + if (vector->length == 0 && vector->data != NULL) { + coroutine->switch_handlers = NULL; + efree(vector->data); + vector->data = NULL; + vector->capacity = 0; + efree(vector); + } +} + +/////////////////////////////////////////////////////////////// +/// Global Main Coroutine Switch Handlers Implementation +/////////////////////////////////////////////////////////////// + +static zend_coroutine_switch_handlers_vector_t global_main_coroutine_start_handlers + = { 0, 0, NULL, false }; + +ZEND_API void zend_async_add_main_coroutine_start_handler(zend_coroutine_switch_handler_fn handler) +{ + zend_coroutine_switch_handlers_vector_t *vector = &global_main_coroutine_start_handlers; + + /* Check for duplicate handler */ + for (uint32_t i = 0; i < vector->length; i++) { + if (vector->data[i].handler == handler) { + return; + } + } + + /* Expand vector if needed */ + if (vector->length >= vector->capacity) { + uint32_t new_capacity = vector->capacity ? vector->capacity * 2 : 4; + vector->data = safe_perealloc( + vector->data, new_capacity, sizeof(zend_coroutine_switch_handler_t), 0, 1); + vector->capacity = new_capacity; + } + + /* Add handler */ + vector->data[vector->length].handler = handler; + vector->length++; +} + +ZEND_API void zend_async_call_main_coroutine_start_handlers(zend_coroutine_t *main_coroutine) +{ + zend_coroutine_switch_handlers_vector_t *global_vector = &global_main_coroutine_start_handlers; + + if (global_vector->length == 0) { + return; + } + + /* Initialize main coroutine switch handlers if needed */ + if (main_coroutine->switch_handlers == NULL) { + zend_coroutine_switch_handlers_init(main_coroutine); + } + + /* Copy all global handlers to main coroutine first */ + for (uint32_t i = 0; i < global_vector->length; i++) { + zend_coroutine_add_switch_handler(main_coroutine, global_vector->data[i].handler); + } + + /* Now call the standard switch handlers function which will handle removal logic */ + zend_coroutine_call_switch_handlers(main_coroutine, true, false); +} + +/* Global cleanup function - called during PHP shutdown */ +static void zend_async_main_handlers_shutdown(void) +{ + zend_coroutine_switch_handlers_vector_t *vector = &global_main_coroutine_start_handlers; + + if (vector->data != NULL) { + pefree(vector->data, 1); + vector->data = NULL; + vector->length = 0; + vector->capacity = 0; + vector->in_execution = false; + } +} diff --git a/Zend/zend_async_API.h b/Zend/zend_async_API.h new file mode 100644 index 0000000000000..36a5294309896 --- /dev/null +++ b/Zend/zend_async_API.h @@ -0,0 +1,1640 @@ +/* ++----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: Edmond | + +----------------------------------------------------------------------+ +*/ +#ifndef ZEND_ASYNC_API_H +#define ZEND_ASYNC_API_H + +#include "zend_fibers.h" +#include "zend_globals.h" + +#define ZEND_ASYNC_API "TrueAsync API v0.5.0" +#define ZEND_ASYNC_API_VERSION_MAJOR 0 +#define ZEND_ASYNC_API_VERSION_MINOR 5 +#define ZEND_ASYNC_API_VERSION_PATCH 0 + +#define ZEND_ASYNC_API_VERSION_NUMBER \ + ((ZEND_ASYNC_API_VERSION_MAJOR << 16) | (ZEND_ASYNC_API_VERSION_MINOR << 8) \ + | (ZEND_ASYNC_API_VERSION_PATCH)) + +#ifndef PHP_WIN32 +#include +#endif + +/* Reactor Poll API */ +typedef enum { + ASYNC_READABLE = 1, + ASYNC_WRITABLE = 2, + ASYNC_DISCONNECT = 4, + ASYNC_PRIORITIZED = 8 +} async_poll_event; + +/* Signal Constants */ +#define ZEND_ASYNC_SIGHUP 1 +#define ZEND_ASYNC_SIGINT 2 +#define ZEND_ASYNC_SIGQUIT 3 +#define ZEND_ASYNC_SIGILL 4 +#define ZEND_ASYNC_SIGABRT_COMPAT 6 +#define ZEND_ASYNC_SIGFPE 8 +#define ZEND_ASYNC_SIGKILL 9 +#define ZEND_ASYNC_SIGSEGV 11 +#define ZEND_ASYNC_SIGTERM 15 +#define ZEND_ASYNC_SIGBREAK 21 +#define ZEND_ASYNC_SIGABRT 22 +#define ZEND_ASYNC_SIGWINCH 28 + +// +// Definitions compatibles with proc_open() +// +#ifdef PHP_WIN32 +typedef HANDLE zend_file_descriptor_t; +#define ZEND_FD_NULL NULL +typedef DWORD zend_process_id_t; +typedef HANDLE zend_process_t; +typedef SOCKET zend_socket_t; +#else +typedef int zend_file_descriptor_t; +typedef pid_t zend_process_id_t; +typedef pid_t zend_process_t; +typedef int zend_socket_t; +#define ZEND_FD_NULL 0 +#endif + +/** + * php_exec + * If type==0, only last line of output is returned (exec) + * If type==1, all lines will be printed and last lined returned (system) + * If type==2, all lines will be saved to given array (exec with &$array) + * If type==3, output will be printed binary, no lines will be saved or returned (passthru) + * If type==4, output will be saved to a memory buffer (shell_exec) + */ +typedef enum { + ZEND_ASYNC_EXEC_MODE_EXEC, + ZEND_ASYNC_EXEC_MODE_SYSTEM, + ZEND_ASYNC_EXEC_MODE_EXEC_ARRAY, + ZEND_ASYNC_EXEC_MODE_PASSTHRU, + ZEND_ASYNC_EXEC_MODE_SHELL_EXEC +} zend_async_exec_mode; + +typedef enum { + ZEND_COROUTINE_NORMAL = 0, + ZEND_COROUTINE_HI_PRIORITY = 255 +} zend_coroutine_priority; + +typedef enum { + ZEND_ASYNC_CLASS_NO = 0, + ZEND_ASYNC_CLASS_AWAITABLE = 1, + ZEND_ASYNC_CLASS_COROUTINE = 2, + ZEND_ASYNC_CLASS_SCOPE = 3, + ZEND_ASYNC_CLASS_CONTEXT = 4, + ZEND_ASYNC_CLASS_SCOPE_PROVIDER = 5, + ZEND_ASYNC_CLASS_SPAWN_STRATEGY = 6, + ZEND_ASYNC_CLASS_TIMEOUT = 7, + + ZEND_ASYNC_CLASS_CHANNEL = 10, + ZEND_ASYNC_CLASS_FUTURE = 11, + ZEND_ASYNC_CLASS_GROUP = 12, + + ZEND_ASYNC_EXCEPTION_DEFAULT = 30, + ZEND_ASYNC_EXCEPTION_CANCELLATION = 31, + ZEND_ASYNC_EXCEPTION_TIMEOUT = 32, + ZEND_ASYNC_EXCEPTION_INPUT_OUTPUT = 33, + ZEND_ASYNC_EXCEPTION_POLL = 34, + ZEND_ASYNC_EXCEPTION_DNS = 35, +} zend_async_class; + +/** + * zend_coroutine_t is a Basic data structure that represents a coroutine in the Zend Engine. + */ +typedef struct _zend_coroutine_s zend_coroutine_t; + +/** + * zend_future_t is a data structure that represents a future result container. + */ +typedef struct _zend_future_s zend_future_t; + +/** + * zend_async_channel_t is a data structure that represents a communication channel. + */ +typedef struct _zend_async_channel_s zend_async_channel_t; +typedef struct _zend_async_context_s zend_async_context_t; +typedef struct _zend_async_waker_s zend_async_waker_t; +typedef struct _zend_async_microtask_s zend_async_microtask_t; +typedef struct _zend_async_scope_s zend_async_scope_t; +typedef struct _zend_async_iterator_s zend_async_iterator_t; +typedef struct _zend_async_group_s zend_async_group_t; +typedef struct _zend_fcall_s zend_fcall_t; +typedef void (*zend_coroutine_entry_t)(void); + +/* Channel method function types */ +typedef bool (*zend_channel_send_t)(zend_async_channel_t *channel, zval *value); +typedef bool (*zend_channel_receive_t)(zend_async_channel_t *channel, zval *result); + +/* Coroutine Switch Handlers */ +typedef struct _zend_coroutine_switch_handler_s zend_coroutine_switch_handler_t; +typedef struct _zend_coroutine_switch_handlers_vector_s zend_coroutine_switch_handlers_vector_t; + +typedef bool (*zend_coroutine_switch_handler_fn)( + zend_coroutine_t *coroutine, bool is_enter, /* true = entering coroutine, false = leaving */ + bool is_finishing /* true = coroutine is finishing */ + /* returns: true = keep handler, false = remove handler after execution */ +); + +typedef struct _zend_async_event_s zend_async_event_t; +typedef struct _zend_async_event_callback_s zend_async_event_callback_t; +typedef struct _zend_async_waker_trigger_s zend_async_waker_trigger_t; +typedef struct _zend_coroutine_event_callback_s zend_coroutine_event_callback_t; +typedef void (*zend_async_event_callback_fn)(zend_async_event_t *event, + zend_async_event_callback_t *callback, void *result, zend_object *exception); +typedef void (*zend_async_event_callback_dispose_fn)( + zend_async_event_callback_t *callback, zend_async_event_t *event); +typedef void (*zend_async_event_add_callback_t)( + zend_async_event_t *event, zend_async_event_callback_t *callback); +typedef void (*zend_async_event_del_callback_t)( + zend_async_event_t *event, zend_async_event_callback_t *callback); +typedef void (*zend_async_event_callbacks_notify_t)( + zend_async_event_t *event, void *result, zend_object *exception); +typedef void (*zend_async_event_start_t)(zend_async_event_t *event); +typedef void (*zend_async_event_stop_t)(zend_async_event_t *event); + +/** + * The replay method can be called in several modes: + * If the callback parameter is not NULL, it will be invoked synchronously and immediately. + * If callback is NULL, then the `result` and `exception` parameters will be filled in. + * + * The method will return true if the result was applied. + */ +typedef bool (*zend_async_event_replay_t)(zend_async_event_t *event, + zend_async_event_callback_t *callback, zval *result, zend_object **exception); +typedef void (*zend_async_event_dispose_t)(zend_async_event_t *event); +typedef zend_string *(*zend_async_event_info_t)(zend_async_event_t *event); + +typedef struct _zend_async_poll_event_s zend_async_poll_event_t; +typedef struct _zend_async_timer_event_s zend_async_timer_event_t; +typedef struct _zend_async_signal_event_s zend_async_signal_event_t; +typedef struct _zend_async_filesystem_event_s zend_async_filesystem_event_t; + +typedef struct _zend_async_process_event_s zend_async_process_event_t; +typedef struct _zend_async_thread_event_s zend_async_thread_event_t; +typedef struct _zend_async_trigger_event_s zend_async_trigger_event_t; + +typedef struct _zend_async_dns_nameinfo_s zend_async_dns_nameinfo_t; +typedef struct _zend_async_dns_addrinfo_s zend_async_dns_addrinfo_t; + +typedef struct _zend_async_exec_event_s zend_async_exec_event_t; + +typedef struct _zend_async_listen_event_s zend_async_listen_event_t; + +typedef struct _zend_async_task_s zend_async_task_t; + +/* Internal context typedefs removed - using direct functions */ + +typedef zend_coroutine_t *(*zend_async_new_coroutine_t)(zend_async_scope_t *scope); +typedef zend_async_scope_t *(*zend_async_new_scope_t)( + zend_async_scope_t *parent_scope, bool with_zend_object); +typedef zend_coroutine_t *(*zend_async_spawn_t)( + zend_async_scope_t *scope, zend_object *scope_provider, int32_t priority); +typedef void (*zend_async_suspend_t)(bool from_main); +typedef void (*zend_async_enqueue_coroutine_t)(zend_coroutine_t *coroutine); +typedef void (*zend_async_resume_t)( + zend_coroutine_t *coroutine, zend_object *error, const bool transfer_error); +typedef void (*zend_async_cancel_t)( + zend_coroutine_t *coroutine, zend_object *error, bool transfer_error, const bool is_safely); +typedef bool (*zend_async_spawn_and_throw_t)( + zend_object *exception, zend_async_scope_t *scope, int32_t priority); +typedef void (*zend_async_shutdown_t)(void); +typedef void (*zend_async_engine_shutdown_t)(void); +typedef zend_array *(*zend_async_get_coroutines_t)(void); +typedef void (*zend_async_add_microtask_t)(zend_async_microtask_t *microtask); +typedef zend_array *(*zend_async_get_awaiting_info_t)(zend_coroutine_t *coroutine); +typedef zend_class_entry *(*zend_async_get_class_ce_t)(zend_async_class type); +typedef zend_future_t *(*zend_async_new_future_t)(bool thread_safe, size_t extra_size); +typedef zend_async_channel_t *(*zend_async_new_channel_t)( + size_t buffer_size, bool resizable, bool thread_safe, size_t extra_size); + +typedef zend_async_group_t *(*zend_async_new_group_t)(size_t extra_size); + +typedef zend_object *(*zend_async_new_future_obj_t)(zend_future_t *future); +typedef zend_object *(*zend_async_new_channel_obj_t)(zend_async_channel_t *channel); + +typedef void (*zend_async_reactor_startup_t)(void); +typedef void (*zend_async_reactor_shutdown_t)(void); +typedef bool (*zend_async_reactor_execute_t)(bool no_wait); +typedef bool (*zend_async_reactor_loop_alive_t)(void); + +typedef zend_async_poll_event_t *(*zend_async_new_socket_event_t)( + zend_socket_t socket, async_poll_event events, size_t extra_size); +typedef zend_async_poll_event_t *(*zend_async_new_poll_event_t)(zend_file_descriptor_t fh, + zend_socket_t socket, async_poll_event events, size_t extra_size); +typedef zend_async_timer_event_t *(*zend_async_new_timer_event_t)(const zend_ulong timeout, + const zend_ulong nanoseconds, const bool is_periodic, size_t extra_size); +typedef zend_async_signal_event_t *(*zend_async_new_signal_event_t)(int signum, size_t extra_size); +typedef zend_async_process_event_t *(*zend_async_new_process_event_t)( + zend_process_t process_handle, size_t extra_size); +typedef void (*zend_async_thread_entry_t)(void *arg, size_t extra_size); +typedef zend_async_thread_event_t *(*zend_async_new_thread_event_t)( + zend_async_thread_entry_t entry, void *arg, size_t extra_size); +typedef void (*zend_async_trigger_event_trigger_fn)(zend_async_trigger_event_t *event); +typedef zend_async_trigger_event_t *(*zend_async_new_trigger_event_t)(size_t extra_size); +typedef zend_async_filesystem_event_t *(*zend_async_new_filesystem_event_t)( + zend_string *path, const unsigned int flags, size_t extra_size); + +typedef zend_async_dns_nameinfo_t *(*zend_async_getnameinfo_t)( + const struct sockaddr *addr, int flags, size_t extra_size); +typedef zend_async_dns_addrinfo_t *(*zend_async_getaddrinfo_t)( + const char *node, const char *service, const struct addrinfo *hints, size_t extra_size); +typedef void (*zend_async_freeaddrinfo_t)(struct addrinfo *ai); + +typedef zend_async_exec_event_t *(*zend_async_new_exec_event_t)(zend_async_exec_mode exec_mode, + const char *cmd, zval *return_buffer, zval *return_value, zval *std_error, const char *cwd, + const char *env, size_t extra_size); + +typedef zend_async_listen_event_t *(*zend_async_socket_listen_t)( + const char *host, int port, int backlog, size_t extra_size); + +typedef int (*zend_async_listen_get_local_address_t)( + zend_async_listen_event_t *listen_event, char *host, size_t host_len, int *port); + +typedef int (*zend_async_exec_t)(zend_async_exec_mode exec_mode, const char *cmd, + zval *return_buffer, zval *return_value, zval *std_error, const char *cwd, const char *env, + const zend_ulong timeout); + +typedef void (*zend_async_queue_task_t)(zend_async_task_t *task); + +typedef void (*zend_async_task_run_t)(zend_async_task_t *task); + +typedef void (*zend_async_microtask_handler_t)(zend_async_microtask_t *microtask); + +struct _zend_fcall_s { + zend_fcall_info fci; + zend_fcall_info_cache fci_cache; +}; + +/////////////////////////////////////////////////////////////////// +/// Coroutine Switch Handlers Structures +/////////////////////////////////////////////////////////////////// + +struct _zend_coroutine_switch_handler_s { + zend_coroutine_switch_handler_fn handler; /* Handler function pointer */ +}; + +struct _zend_coroutine_switch_handlers_vector_s { + uint32_t length; /* Number of handlers */ + uint32_t capacity; /* Allocated capacity */ + zend_coroutine_switch_handler_t *data; /* Array of handlers */ + bool in_execution; /* Protection flag during execution */ +}; + +struct _zend_async_microtask_s { + zend_async_microtask_handler_t handler; + zend_async_microtask_handler_t dtor; + bool is_cancelled; + uint32_t ref_count; +}; + +#define ZEND_ASYNC_MICROTASK_ADD_REF(microtask) \ + do { \ + if (microtask != NULL) { \ + microtask->ref_count++; \ + } \ + } while (0) + +#define ZEND_ASYNC_MICROTASK_RELEASE(microtask) \ + do { \ + if (microtask != NULL && microtask->ref_count > 1) { \ + microtask->ref_count--; \ + } else { \ + microtask->ref_count = 0; \ + if (microtask->dtor) { \ + microtask->dtor(microtask); \ + } \ + efree(microtask); \ + } \ + } while (0) + +/////////////////////////////////////////////////////////////////// +/// Async iterator structures +/////////////////////////////////////////////////////////////////// + +typedef void (*zend_async_iterator_method_t)(zend_async_iterator_t *iterator); + +#define ZEND_ASYNC_ITERATOR_FIELDS \ + zend_async_microtask_t microtask; \ + zend_async_scope_t *scope; \ + /* NULLABLE. Custom data for the iterator, can be used to store additional information. */ \ + void *extended_data; \ + /* NULLABLE. An additional destructor that will be called. */ \ + zend_async_iterator_method_t extended_dtor; \ + /* A method that starts the iterator in the current coroutine. */ \ + zend_async_iterator_method_t run; \ + /* A method that starts the iterator in a separate coroutine with the specified priority. */ \ + void (*run_in_coroutine)(zend_async_iterator_t * iterator, int32_t priority); \ + /* The maximum number of concurrent tasks that can be executed at the same time */ \ + unsigned int concurrency; \ + /* Priority for coroutines created by this iterator */ \ + int32_t priority; \ + /* NULLABLE. Exception that stopped the iterator */ \ + zend_object *exception; + +struct _zend_async_iterator_s { + ZEND_ASYNC_ITERATOR_FIELDS +}; + +typedef zend_result (*zend_async_iterator_handler_t)( + zend_async_iterator_t *iterator, zval *current, zval *key); + +typedef zend_async_iterator_t *(*zend_async_new_iterator_t)(zval *array, + zend_object_iterator *zend_iterator, zend_fcall_t *fcall, + zend_async_iterator_handler_t handler, zend_async_scope_t *scope, unsigned int concurrency, + int32_t priority, size_t iterator_size); + +/////////////////////////////////////////////////////////////////// +/// Event Structures +/////////////////////////////////////////////////////////////////// + +struct _zend_async_event_callback_s { + uint32_t ref_count; + zend_async_event_callback_fn callback; + zend_async_event_callback_dispose_fn dispose; +}; + +#define ZEND_ASYNC_EVENT_CALLBACK_ADD_REF(callback) \ + if (callback != NULL) { \ + callback->ref_count++; \ + } + +// +// For a callback, +// it’s crucial that the reference count is always greater than zero, +// because a value of zero is a special case triggered from a destructor. +// If you need to “retain” ownership of the object, +// you **MUST** use either this macro or ZEND_ASYNC_EVENT_CALLBACK_RELEASE. +// +#define ZEND_ASYNC_EVENT_CALLBACK_DEC_REF(callback) \ + if (callback != NULL && callback->ref_count > 1) { \ + callback->ref_count--; \ + } + +#define ZEND_ASYNC_EVENT_CALLBACK_RELEASE(callback) \ + if ((callback) != NULL && (callback)->ref_count > 1) { \ + (callback)->ref_count--; \ + } else if ((callback)->dispose != NULL) { \ + (callback)->dispose((callback), NULL); \ + } else { \ + coroutine_event_callback_dispose((callback), NULL); \ + } + +struct _zend_coroutine_event_callback_s { + zend_async_event_callback_t base; + zend_coroutine_t *coroutine; +}; + +struct _zend_async_waker_trigger_s { + uint32_t length; /* current number of callbacks */ + uint32_t capacity; /* allocated slots in the array */ + zend_async_event_t *event; + zend_async_event_callback_t *data[1]; /* flexible array member */ +}; + +/* Dynamic array of async event callbacks with single iterator protection */ +typedef struct _zend_async_callbacks_vector_s { + uint32_t length; /* current number of callbacks */ + uint32_t capacity; /* allocated slots in the array */ + zend_async_event_callback_t **data; /* dynamically allocated callback array */ + + /* Single iterator tracking - NULL means no active iteration */ + uint32_t *current_iterator; /* pointer to active iterator index */ +} zend_async_callbacks_vector_t; + +/** + * Basic structure for representing events. + * An event can be either an internal C object or a Zend object implementing the Awaitable + * interface. In that case, the ZEND_ASYNC_EVENT_F_ZEND_OBJ flag is set to TRUE, and the + * zend_object_offset field points to the offset of the zend_object structure. + * + * To manage the reference counter, use the macros: + * ZEND_ASYNC_EVENT_ADD_REF, ZEND_ASYNC_EVENT_DEL_REF, ZEND_ASYNC_EVENT_RELEASE. + * + */ +struct _zend_async_event_s { + /* If event is closed, it cannot be started or stopped. */ + uint32_t flags; + /* Offset to the beginning of additional data associated with the event (used for extensions) */ + uint32_t extra_offset; + union { + /* The refcount of the event. */ + uint32_t ref_count; + /* The offset of Zend object structure. */ + uint32_t zend_object_offset; + }; + /* The Event loop reference count. */ + uint32_t loop_ref_count; + /* Events callbacks */ + zend_async_callbacks_vector_t callbacks; + /* Methods */ + zend_async_event_add_callback_t add_callback; + zend_async_event_del_callback_t del_callback; + zend_async_event_start_t start; + zend_async_event_stop_t stop; + /* + * Replay method. Nullable. + * This method is implemented only by those events that can provide a result again, even after + * they have completed. For example, this method is relevant for coroutines and futures, which + * can provide the result again and again. + */ + zend_async_event_replay_t replay; + zend_async_event_dispose_t dispose; + /* Event info: can be NULL */ + zend_async_event_info_t info; + /* + * Handler that is invoked before all event listeners are notified. + * May be NULL. + */ + zend_async_event_callbacks_notify_t notify_handler; +}; + +/** + * Event reference. A special data structure that allows representing an object with the Awaitable + * interface, but which does not store the event directly—instead, it holds only a reference to it. + * This is necessary for events that are destroyed asynchronously and therefore cannot be used as + * Zend objects. + * + * For example, events like Timer, Poll, and Signal cannot be Zend objects + * because their destruction cycle does not align. + * + * * flags should always be equal to ZEND_ASYNC_EVENT_REFERENCE_PREFIX. + * * zend_object_offset is the offset of the Zend object structure. + * * event is a pointer to the zend_async_event_t structure. + */ +#define ZEND_ASYNC_EVENT_REF_PROLOG \ + uint32_t flags; \ + uint32_t zend_object_offset; + +#define ZEND_ASYNC_EVENT_REF_FIELDS \ + uint32_t flags; \ + uint32_t zend_object_offset; \ + zend_async_event_t *event; + +typedef struct { + ZEND_ASYNC_EVENT_REF_FIELDS +} zend_async_event_ref_t; + +#define ZEND_ASYNC_EVENT_F_CLOSED (1u << 0) /* event was closed */ +#define ZEND_ASYNC_EVENT_F_RESULT_USED (1u << 1) /* result will be used in exception handler */ +#define ZEND_ASYNC_EVENT_F_EXC_CAUGHT (1u << 2) /* error was caught in exception handler */ +/* Indicates that the event produces a ZVAL pointer during the callback. */ +#define ZEND_ASYNC_EVENT_F_ZVAL_RESULT (1u << 3) +#define ZEND_ASYNC_EVENT_F_ZEND_OBJ (1u << 4) /* event is a zend object */ +#define ZEND_ASYNC_EVENT_F_NO_FREE_MEMORY \ + (1u << 5) /* event will not free memory in dispose handler */ +#define ZEND_ASYNC_EVENT_F_EXCEPTION_HANDLED (1u << 6) /* exception has been caught and processed \ + */ + +#define ZEND_ASYNC_EVENT_F_REFERENCE (1u << 7) /* event is a reference structure */ + +// Flag indicating that the event has a zend_object reference by extra_offset. +#define ZEND_ASYNC_EVENT_F_OBJ_REF (1u << 8) /* has zend_object ref */ + +#define ZEND_ASYNC_EVENT_REFERENCE_PREFIX ((uint32_t) 0x80) /* prefix for reference structures */ + +// Create a reference to an event with the given offset and event pointer. +#define ZEND_ASYNC_EVENT_REF_SET(ptr, offset, ev) \ + do { \ + (ptr)->flags = ZEND_ASYNC_EVENT_REFERENCE_PREFIX; \ + (ptr)->zend_object_offset = (offset); \ + (ptr)->event = (ev); \ + } while (0) + +#define ZEND_ASYNC_EVENT_IS_CLOSED(ev) (((ev)->flags & ZEND_ASYNC_EVENT_F_CLOSED) != 0) +#define ZEND_ASYNC_EVENT_WILL_RESULT_USED(ev) (((ev)->flags & ZEND_ASYNC_EVENT_F_RESULT_USED) != 0) +#define ZEND_ASYNC_EVENT_WILL_EXC_CAUGHT(ev) (((ev)->flags & ZEND_ASYNC_EVENT_F_EXC_CAUGHT) != 0) +#define ZEND_ASYNC_EVENT_WILL_ZVAL_RESULT(ev) (((ev)->flags & ZEND_ASYNC_EVENT_F_ZVAL_RESULT) != 0) +#define ZEND_ASYNC_EVENT_IS_ZEND_OBJ(ev) (((ev)->flags & ZEND_ASYNC_EVENT_F_ZEND_OBJ) != 0) +#define ZEND_ASYNC_EVENT_IS_NO_FREE_MEMORY(ev) \ + (((ev)->flags & ZEND_ASYNC_EVENT_F_NO_FREE_MEMORY) != 0) +#define ZEND_ASYNC_EVENT_IS_EXCEPTION_HANDLED(ev) \ + (((ev)->flags & ZEND_ASYNC_EVENT_F_EXCEPTION_HANDLED) != 0) + +#define ZEND_ASYNC_EVENT_SET_CLOSED(ev) ((ev)->flags |= ZEND_ASYNC_EVENT_F_CLOSED) +#define ZEND_ASYNC_EVENT_CLR_CLOSED(ev) ((ev)->flags &= ~ZEND_ASYNC_EVENT_F_CLOSED) + +#define ZEND_ASYNC_EVENT_SET_RESULT_USED(ev) ((ev)->flags |= ZEND_ASYNC_EVENT_F_RESULT_USED) +#define ZEND_ASYNC_EVENT_CLR_RESULT_USED(ev) ((ev)->flags &= ~ZEND_ASYNC_EVENT_F_RESULT_USED) + +#define ZEND_ASYNC_EVENT_SET_EXC_CAUGHT(ev) ((ev)->flags |= ZEND_ASYNC_EVENT_F_EXC_CAUGHT) +#define ZEND_ASYNC_EVENT_CLR_EXC_CAUGHT(ev) ((ev)->flags &= ~ZEND_ASYNC_EVENT_F_EXC_CAUGHT) + +#define ZEND_ASYNC_EVENT_SET_ZVAL_RESULT(ev) ((ev)->flags |= ZEND_ASYNC_EVENT_F_ZVAL_RESULT) +#define ZEND_ASYNC_EVENT_CLR_ZVAL_RESULT(ev) ((ev)->flags &= ~ZEND_ASYNC_EVENT_F_ZVAL_RESULT) + +#define ZEND_ASYNC_EVENT_SET_ZEND_OBJ(ev) ((ev)->flags |= ZEND_ASYNC_EVENT_F_ZEND_OBJ) +#define ZEND_ASYNC_EVENT_SET_ZEND_OBJ_OFFSET(ev, offset) \ + ((ev)->zend_object_offset = (unsigned int) (offset)) + +#define ZEND_ASYNC_EVENT_SET_NO_FREE_MEMORY(ev) ((ev)->flags |= ZEND_ASYNC_EVENT_F_NO_FREE_MEMORY) + +#define ZEND_ASYNC_EVENT_SET_EXCEPTION_HANDLED(ev) \ + ((ev)->flags |= ZEND_ASYNC_EVENT_F_EXCEPTION_HANDLED) +#define ZEND_ASYNC_EVENT_CLR_EXCEPTION_HANDLED(ev) \ + ((ev)->flags &= ~ZEND_ASYNC_EVENT_F_EXCEPTION_HANDLED) + +#define ZEND_ASYNC_EVENT_WITH_OBJECT_REF(ev) ((ev)->flags |= ZEND_ASYNC_EVENT_F_OBJ_REF) + +// Convert awaitable Zend object to zend_async_event_t pointer +#define ZEND_ASYNC_EVENT_IS_REFERENCE(ptr) \ + (*((const uint32_t *) (ptr)) == ZEND_ASYNC_EVENT_REFERENCE_PREFIX) +#define ZEND_ASYNC_OBJECT_TO_EVENT(obj) \ + (ZEND_ASYNC_EVENT_IS_REFERENCE((void *) ((char *) (obj) - (obj)->handlers->offset)) \ + ? ((zend_async_event_ref_t *) ((char *) (obj) - (obj)->handlers->offset)) \ + ->event \ + : (zend_async_event_t *) ((char *) (obj) - (obj)->handlers->offset)) + +// Convert zend_async_event_t to zend_object pointer +#define ZEND_ASYNC_EVENT_TO_OBJECT(ev) \ + (((ev)->flags & ZEND_ASYNC_EVENT_F_OBJ_REF) \ + ? *(zend_object **) ((char *) (ev) + (ev)->extra_offset) \ + : (zend_object *) ((char *) (ev) + (ev)->zend_object_offset)) + +// Get refcount of the event object +#define ZEND_ASYNC_EVENT_REF(ev) \ + (ZEND_ASYNC_EVENT_IS_ZEND_OBJ(ev) ? GC_REFCOUNT(ZEND_ASYNC_EVENT_TO_OBJECT(ev)) \ + : (ev)->ref_count) + +// Proper increment of the event object's reference count. +#define ZEND_ASYNC_EVENT_ADD_REF(ev) \ + (ZEND_ASYNC_EVENT_IS_ZEND_OBJ(ev) ? GC_ADDREF(ZEND_ASYNC_EVENT_TO_OBJECT(ev)) \ + : ++(ev)->ref_count) + +// Proper decrement of the event object's reference count. +#define ZEND_ASYNC_EVENT_DEL_REF(ev) \ + (ZEND_ASYNC_EVENT_IS_ZEND_OBJ(ev) ? GC_DELREF(ZEND_ASYNC_EVENT_TO_OBJECT(ev)) \ + : --(ev)->ref_count) + +/* Properly release the event object */ +#define ZEND_ASYNC_EVENT_RELEASE(ev) \ + do { \ + if (ZEND_ASYNC_EVENT_IS_ZEND_OBJ(ev)) { \ + if (GC_REFCOUNT(ZEND_ASYNC_EVENT_TO_OBJECT(ev)) == 1) { \ + OBJ_RELEASE(ZEND_ASYNC_EVENT_TO_OBJECT(ev)); \ + } else { \ + GC_DELREF(ZEND_ASYNC_EVENT_TO_OBJECT(ev)); \ + } \ + } else { \ + if ((ev)->ref_count == 1) { \ + (ev)->ref_count = 0; \ + (ev)->dispose(ev); \ + } else { \ + (ev)->ref_count--; \ + } \ + } \ + } while (0) + +#define ZEND_ASYNC_EVENT_REPLAY(ev, callback) \ + (ev->replay != NULL ? ev->replay(ev, callback, NULL, NULL) : false) +#define ZEND_ASYNC_EVENT_EXTRACT_RESULT(ev, result) \ + (ev->replay != NULL ? ev->replay(ev, NULL, result, NULL) : false) +#define ZEND_ASYNC_EVENT_EXTRACT_RESULT_OR_ERROR(ev, result, exception) \ + (ev->replay != NULL ? ev->replay(ev, NULL, result, exception) : false) + +/* Public callback vector functions - implementations in zend_async_API.c */ +ZEND_API void zend_async_callbacks_notify( + zend_async_event_t *event, void *result, zend_object *exception, bool from_handler); +ZEND_API void zend_async_callbacks_remove( + zend_async_event_t *event, zend_async_event_callback_t *callback); +ZEND_API void zend_async_callbacks_free(zend_async_event_t *event); +ZEND_API void zend_async_callbacks_notify_and_close( + zend_async_event_t *event, void *result, zend_object *exception); + +#define ZEND_ASYNC_CALLBACKS_NOTIFY(event, result, exception) \ + zend_async_callbacks_notify((event), (result), (exception), false) + +#define ZEND_ASYNC_CALLBACKS_NOTIFY_AND_CLOSE(event, result, exception) \ + zend_async_callbacks_notify_and_close((event), (result), (exception)) + +#define ZEND_ASYNC_CALLBACKS_NOTIFY_FROM_HANDLER(event, result, exception) \ + zend_async_callbacks_notify((event), (result), (exception), true) + +/* Append a callback; grows the buffer when needed */ +static zend_always_inline void zend_async_callbacks_push( + zend_async_event_t *event, zend_async_event_callback_t *callback) +{ + if (event->callbacks.data == NULL) { + event->callbacks.data = (zend_async_event_callback_t **) safe_emalloc( + 4, sizeof(zend_async_event_callback_t *), 0); + event->callbacks.capacity = 4; + } + + zend_async_callbacks_vector_t *vector = &event->callbacks; + + if (vector->length == vector->capacity) { + vector->capacity = vector->capacity ? vector->capacity * 2 : 4; + vector->data = (zend_async_event_callback_t **) safe_erealloc( + vector->data, vector->capacity, sizeof(zend_async_event_callback_t *), 0); + } + + vector->data[vector->length++] = callback; +} + +struct _zend_async_poll_event_s { + zend_async_event_t base; + bool is_socket; + union { + zend_file_descriptor_t file; + zend_socket_t socket; + }; + async_poll_event events; + async_poll_event triggered_events; +}; + +struct _zend_async_timer_event_s { + zend_async_event_t base; + /* The timeout in milliseconds. */ + unsigned int timeout; + /* The timer is periodic. */ + bool is_periodic; +}; + +struct _zend_async_signal_event_s { + zend_async_event_t base; + int signal; +}; + +struct _zend_async_process_event_s { + zend_async_event_t base; + zend_process_t process; + zend_long exit_code; +}; + +struct _zend_async_thread_event_s { + zend_async_event_t base; +}; + +struct _zend_async_filesystem_event_s { + zend_async_event_t base; + zend_string *path; + unsigned int flags; + unsigned int triggered_events; + zend_string *triggered_filename; +}; + +struct _zend_async_dns_nameinfo_s { + zend_async_event_t base; + /* These structure fields store the RESULT of the operation. + * It will be automatically freed when the structure is destroyed. */ + zend_string *hostname; + zend_string *service; +}; + +struct _zend_async_dns_addrinfo_s { + zend_async_event_t base; + const char *node; + const char *service; + /* The DNS resolution result must be explicitly and mandatorily freed using the + * ZEND_ASYNC_FREEADDRINFO method! */ + struct addrinfo *result; +}; + +struct _zend_async_exec_event_s { + zend_async_event_t base; + zend_async_exec_mode exec_mode; + bool terminated; + char *cmd; + zval *return_value; + zval *result_buffer; + size_t output_len; + char *output_buffer; + zend_long exit_code; + int term_signal; + zval *std_error; +}; + +struct _zend_async_listen_event_s { + zend_async_event_t base; + const char *host; + int port; + int backlog; + zend_socket_t socket_fd; + zend_async_listen_get_local_address_t get_local_address; +}; + +struct _zend_async_task_s { + zend_async_event_t base; + zend_async_task_run_t run; +}; + +struct _zend_async_trigger_event_s { + zend_async_event_t base; + zend_async_trigger_event_trigger_fn trigger; +}; + +/////////////////////////////////////////////////////////////////// +/// Scope Structures +/////////////////////////////////////////////////////////////////// + +typedef void (*zend_async_before_coroutine_enqueue_t)( + zend_coroutine_t *coroutine, zend_async_scope_t *scope, zval *result); +typedef void (*zend_async_after_coroutine_enqueue_t)( + zend_coroutine_t *coroutine, zend_async_scope_t *scope); + +/* Dynamic array of async event callbacks */ +typedef struct _zend_async_scopes_vector_s { + uint32_t length; /* current number of items */ + uint32_t capacity; /* allocated slots in the array */ + zend_async_scope_t **data; /* dynamically allocated array */ +} zend_async_scopes_vector_t; + +/** + * The internal Scope structure and the Zend object Scope are different data structures. + * This separation is intentional to manage their lifetimes independently. + * The internal Scope structure can outlive the Zend object. + * When the Zend object triggers the dtor_obj method, + * it initiates the disposal process of the Scope. + * + * However, the internal Scope structure remains in memory until the last coroutine has completed. + */ +struct _zend_async_scope_s { + /* Event object for reacting to events. */ + zend_async_event_t event; + /* The link to the zend_object structure */ + zend_object *scope_object; + + zend_async_scopes_vector_t scopes; + zend_async_scope_t *parent_scope; + /* Scope context object */ + zend_async_context_t *context; + + zend_async_before_coroutine_enqueue_t before_coroutine_enqueue; + zend_async_after_coroutine_enqueue_t after_coroutine_enqueue; + + /** + * The method determines the moment when the Scope can be destructed. + * It checks the conditions and, if necessary, calls the dispose method. + */ + bool (*try_to_dispose)(zend_async_scope_t *scope); + + /** + * The method handles an exception delivered to the Scope. + * Its result may either be the cancellation of the Scope or the suppression of the exception. + * If the is_cancellation parameter is FALSE, it indicates an attempt to handle an exception + * from a coroutine. Otherwise, it's an attempt by the user to stop the execution of the Scope. + * + * The method should return true if the exception was handled and the Scope can continue + * execution. + * + * This method is the central point of responsibility where the behavior in case of an error is + * determined. + */ + bool (*catch_or_cancel)(zend_async_scope_t *scope, zend_coroutine_t *coroutine, + zend_async_scope_t *from_scope, zend_object *exception, bool transfer_error, + const bool is_safely, const bool is_cancellation); +}; + +#define ZEND_ASYNC_SCOPE_CLOSE(scope, is_safely) \ + ((scope)->catch_or_cancel((scope), NULL, NULL, NULL, false, (is_safely), true)) + +#define ZEND_ASYNC_SCOPE_CANCEL(scope, exception, transfer_error, is_safely) \ + ((scope)->catch_or_cancel( \ + (scope), NULL, NULL, (exception), (transfer_error), (is_safely), true)) + +#define ZEND_ASYNC_SCOPE_CATCH(scope, coroutine, from_scope, exception, transfer_error, is_safely) \ + ((scope)->catch_or_cancel((scope), (coroutine), (from_scope), (exception), (transfer_error), \ + (is_safely), false)) + +#define ZEND_ASYNC_SCOPE_F_CLOSED ZEND_ASYNC_EVENT_F_CLOSED /* scope was closed */ +#define ZEND_ASYNC_SCOPE_F_NO_FREE_MEMORY \ + ZEND_ASYNC_EVENT_F_NO_FREE_MEMORY /* scope will not free memory in dispose handler */ +#define ZEND_ASYNC_SCOPE_F_DISPOSE_SAFELY (1u << 14) /* scope will be disposed safely */ +#define ZEND_ASYNC_SCOPE_F_CANCELLED (1u << 15) /* scope was cancelled */ +#define ZEND_ASYNC_SCOPE_F_DISPOSING (1u << 16) /* scope disposing */ + +#define ZEND_ASYNC_SCOPE_IS_CLOSED(scope) (((scope)->event.flags & ZEND_ASYNC_SCOPE_F_CLOSED) != 0) +#define ZEND_ASYNC_SCOPE_IS_NO_FREE_MEMORY(scope) \ + (((scope)->event.flags & ZEND_ASYNC_SCOPE_F_NO_FREE_MEMORY) != 0) +#define ZEND_ASYNC_SCOPE_IS_DISPOSE_SAFELY(scope) \ + (((scope)->event.flags & ZEND_ASYNC_SCOPE_F_DISPOSE_SAFELY) != 0) +#define ZEND_ASYNC_SCOPE_IS_CANCELLED(scope) \ + (((scope)->event.flags & ZEND_ASYNC_SCOPE_F_CANCELLED) != 0) +#define ZEND_ASYNC_SCOPE_IS_DISPOSING(scope) \ + (((scope)->event.flags & ZEND_ASYNC_SCOPE_F_DISPOSING) != 0) + +#define ZEND_ASYNC_SCOPE_SET_CLOSED(scope) ((scope)->event.flags |= ZEND_ASYNC_SCOPE_F_CLOSED) +#define ZEND_ASYNC_SCOPE_CLR_CLOSED(scope) ((scope)->event.flags &= ~ZEND_ASYNC_SCOPE_F_CLOSED) + +#define ZEND_ASYNC_SCOPE_SET_NO_FREE_MEMORY(scope) \ + ((scope)->event.flags |= ZEND_ASYNC_SCOPE_F_NO_FREE_MEMORY) +#define ZEND_ASYNC_SCOPE_CLR_NO_FREE_MEMORY(scope) \ + ((scope)->event.flags &= ~ZEND_ASYNC_SCOPE_F_NO_FREE_MEMORY) + +#define ZEND_ASYNC_SCOPE_SET_DISPOSE_SAFELY(scope) \ + ((scope)->event.flags |= ZEND_ASYNC_SCOPE_F_DISPOSE_SAFELY) +#define ZEND_ASYNC_SCOPE_CLR_DISPOSE_SAFELY(scope) \ + ((scope)->event.flags &= ~ZEND_ASYNC_SCOPE_F_DISPOSE_SAFELY) + +#define ZEND_ASYNC_SCOPE_SET_CANCELLED(scope) ((scope)->event.flags |= ZEND_ASYNC_SCOPE_F_CANCELLED) + +#define ZEND_ASYNC_SCOPE_SET_DISPOSING(scope) ((scope)->event.flags |= ZEND_ASYNC_SCOPE_F_DISPOSING) +#define ZEND_ASYNC_SCOPE_CLR_DISPOSING(scope) \ + ((scope)->event.flags &= ~ZEND_ASYNC_SCOPE_F_DISPOSING) + +static zend_always_inline void zend_async_scope_add_child( + zend_async_scope_t *parent_scope, zend_async_scope_t *child_scope) +{ + zend_async_scopes_vector_t *vector = &parent_scope->scopes; + + child_scope->parent_scope = parent_scope; + + if (vector->data == NULL) { + vector->data = (zend_async_scope_t **) safe_emalloc(4, sizeof(zend_async_scope_t *), 0); + vector->capacity = 4; + } + + if (vector->length == vector->capacity) { + vector->capacity *= 2; + vector->data = (zend_async_scope_t **) safe_erealloc( + vector->data, vector->capacity, sizeof(zend_async_scope_t *), 0); + } + + vector->data[vector->length++] = child_scope; +} + +static zend_always_inline void zend_async_scope_remove_child( + zend_async_scope_t *parent_scope, zend_async_scope_t *child_scope) +{ + zend_async_scopes_vector_t *vector = &parent_scope->scopes; + for (uint32_t i = 0; i < vector->length; ++i) { + if (vector->data[i] == child_scope) { + vector->data[i] = vector->data[--vector->length]; + child_scope->parent_scope = NULL; + + // Try to dispose the parent scope if it is empty + if (parent_scope->scopes.length == 0) { + parent_scope->try_to_dispose(parent_scope); + } + + return; + } + } +} + +static zend_always_inline void zend_async_scope_free_children(zend_async_scope_t *parent_scope) +{ + zend_async_scopes_vector_t *vector = &parent_scope->scopes; + + if (vector->data != NULL) { + efree(vector->data); + } + + vector->data = NULL; + vector->length = 0; + vector->capacity = 0; +} + +/////////////////////////////////////////////////////////////////// +/// Waker Structures +/////////////////////////////////////////////////////////////////// + +typedef void (*zend_async_waker_dtor)(zend_coroutine_t *coroutine); + +typedef enum { + ZEND_ASYNC_WAKER_NO_STATUS, + ZEND_ASYNC_WAKER_WAITING, + ZEND_ASYNC_WAKER_QUEUED, + ZEND_ASYNC_WAKER_IGNORED, + ZEND_ASYNC_WAKER_RESULT +} ZEND_ASYNC_WAKER_STATUS; + +/** + * Condition that is TRUE if the coroutine is in the queue + */ +#define ZEND_ASYNC_WAKER_IN_QUEUE(waker) \ + (waker != NULL \ + && ((waker)->status == ZEND_ASYNC_WAKER_QUEUED \ + || waker->status == ZEND_ASYNC_WAKER_IGNORED)) + +struct _zend_async_waker_s { + /* The waker status. */ + ZEND_ASYNC_WAKER_STATUS status; + /* The array of zend_async_trigger_callback_t. */ + HashTable events; + /* A list of events objects (zend_async_event_t) that occurred during the last iteration of the + * event loop. */ + HashTable *triggered_events; + /* Result of the waker. */ + zval result; + /* Error object. */ + zend_object *error; + /* Filename of the waker object creation point. */ + zend_string *filename; + /* Line number of the waker object creation point. */ + uint32_t lineno; + /* The waker destructor. */ + zend_async_waker_dtor dtor; +}; + +#define ZEND_ASYNC_WAKER_WAITING(waker) ((waker)->status < ZEND_ASYNC_WAKER_RESULT) + +/** + * Coroutine destructor. Called when the coroutine needs to clean up all its data. + */ +typedef void (*zend_async_coroutine_dispose)(zend_coroutine_t *coroutine); + +struct _zend_coroutine_s { + zend_async_event_t event; + /* + * Callback and info / cache to be used when coroutine is started. + * If NULL, the coroutine is not a userland coroutine and internal_entry is used. + */ + zend_fcall_t *fcall; + + /* + * The internal entry point of the coroutine. + * If NULL, the coroutine is a userland coroutine and fcall is used. + */ + zend_coroutine_entry_t internal_entry; + + /* The custom data for the coroutine. Can be NULL */ + void *extended_data; + + /* Coroutine waker */ + zend_async_waker_t *waker; + /* Coroutine scope */ + zend_async_scope_t *scope; + + /* Storage for return value. */ + zval result; + + /* Exception object, if any, nullable */ + zend_object *exception; + + /* Coroutine context object */ + zend_async_context_t *context; + + /* Internal context (for C extensions with numeric keys) */ + HashTable *internal_context; + + /* Spawned file and line number */ + zend_string *filename; + uint32_t lineno; + + /* Extended dispose handler */ + zend_async_coroutine_dispose extended_dispose; + + /* Switch handlers for context switching */ + zend_coroutine_switch_handlers_vector_t *switch_handlers; +}; + +/** + * The macro evaluates to TRUE if the coroutine is in a waiting state — + * either waiting for events or waiting in the execution queue. + */ +#define ZEND_COROUTINE_SUSPENDED(coroutine) \ + ((coroutine)->waker != NULL && ZEND_ASYNC_WAKER_WAITING((coroutine)->waker)) + +/* Coroutine flags */ +#define ZEND_COROUTINE_F_STARTED (1u << 10) /* coroutine is started */ +#define ZEND_COROUTINE_F_CANCELLED (1u << 11) /* coroutine is cancelled */ +#define ZEND_COROUTINE_F_ZOMBIE (1u << 12) /* coroutine is a zombie */ +#define ZEND_COROUTINE_F_PROTECTED (1u << 13) /* coroutine is protected */ +#define ZEND_COROUTINE_F_MAIN (1u << 14) /* coroutine is a main coroutine */ + +#define ZEND_COROUTINE_IS_ZOMBIE(coroutine) \ + (((coroutine)->event.flags & ZEND_COROUTINE_F_ZOMBIE) != 0) +#define ZEND_COROUTINE_SET_ZOMBIE(coroutine) ((coroutine)->event.flags |= ZEND_COROUTINE_F_ZOMBIE) +#define ZEND_COROUTINE_IS_STARTED(coroutine) \ + (((coroutine)->event.flags & ZEND_COROUTINE_F_STARTED) != 0) +#define ZEND_COROUTINE_IS_CANCELLED(coroutine) \ + (((coroutine)->event.flags & ZEND_COROUTINE_F_CANCELLED) != 0) +#define ZEND_COROUTINE_IS_FINISHED(coroutine) \ + (((coroutine)->event.flags & ZEND_ASYNC_EVENT_F_CLOSED) != 0) +#define ZEND_COROUTINE_IS_PROTECTED(coroutine) \ + (((coroutine)->event.flags & ZEND_COROUTINE_F_PROTECTED) != 0) +#define ZEND_COROUTINE_IS_EXCEPTION_HANDLED(coroutine) \ + ZEND_ASYNC_EVENT_IS_EXCEPTION_HANDLED(&(coroutine)->event) +#define ZEND_COROUTINE_IS_MAIN(coroutine) (((coroutine)->event.flags & ZEND_COROUTINE_F_MAIN) != 0) +#define ZEND_COROUTINE_SET_STARTED(coroutine) ((coroutine)->event.flags |= ZEND_COROUTINE_F_STARTED) +#define ZEND_COROUTINE_SET_CANCELLED(coroutine) \ + ((coroutine)->event.flags |= ZEND_COROUTINE_F_CANCELLED) +#define ZEND_COROUTINE_SET_FINISHED(coroutine) \ + ((coroutine)->event.flags |= ZEND_ASYNC_EVENT_F_CLOSED) +#define ZEND_COROUTINE_SET_PROTECTED(coroutine) \ + ((coroutine)->event.flags |= ZEND_COROUTINE_F_PROTECTED) +#define ZEND_COROUTINE_SET_MAIN(coroutine) ((coroutine)->event.flags |= ZEND_COROUTINE_F_MAIN) +#define ZEND_COROUTINE_CLR_PROTECTED(coroutine) \ + ((coroutine)->event.flags &= ~ZEND_COROUTINE_F_PROTECTED) +#define ZEND_COROUTINE_SET_EXCEPTION_HANDLED(coroutine) \ + ZEND_ASYNC_EVENT_SET_EXCEPTION_HANDLED(&(coroutine)->event) +#define ZEND_COROUTINE_CLR_EXCEPTION_HANDLED(coroutine) \ + ZEND_ASYNC_EVENT_CLR_EXCEPTION_HANDLED(&(coroutine)->event) + +static zend_always_inline zend_string *zend_coroutine_callable_name( + const zend_coroutine_t *coroutine) +{ + if (ZEND_COROUTINE_IS_MAIN(coroutine)) { + return zend_string_init("main", sizeof("main") - 1, 0); + } + + if (coroutine->fcall) { + return zend_get_callable_name_ex(&coroutine->fcall->fci.function_name, NULL); + } + + return zend_string_init("internal function", sizeof("internal function") - 1, 0); +} + +/** + * Macro for constructing an FCALL structure from PHP function parameters. + * Z_PARAM_FUNC(fci, fcc); + * Z_PARAM_VARIADIC_WITH_NAMED(args, args_count, named_args); + */ +#define ZEND_ASYNC_FCALL_DEFINE(fcall, fci, fcc, args, args_count, named_args) \ + zend_fcall_t *fcall = ecalloc(1, sizeof(zend_fcall_t)); \ + fcall->fci = fci; \ + fcall->fci_cache = fcc; \ + if (args_count) { \ + fcall->fci.param_count = args_count; \ + fcall->fci.params = safe_emalloc(args_count, sizeof(zval), 0); \ + for (uint32_t i = 0; i < args_count; i++) { \ + ZVAL_COPY(&fcall->fci.params[i], &args[i]); \ + } \ + } \ + if (named_args) { \ + fcall->fci.named_params = named_args; \ + GC_ADDREF(named_args); \ + } \ + Z_TRY_ADDREF(fcall->fci.function_name); + +/////////////////////////////////////////////////////////////// +/// Async Context Structures +/////////////////////////////////////////////////////////////// + +typedef zend_async_context_t *(*zend_async_new_context_t)(void); +typedef bool (*zend_async_context_find_t)( + zend_async_context_t *context, zval *key, zval *result, bool include_parent); +typedef void (*zend_async_context_set_t)(zend_async_context_t *context, zval *key, zval *value); +typedef bool (*zend_async_context_unset_t)(zend_async_context_t *context, zval *key); +typedef void (*zend_async_context_dispose_t)(zend_async_context_t *context); + +struct _zend_async_context_s { + /* flags for the context: reserved */ + uint32_t flags; + /* offset of the context zend object */ + uint32_t offset; + zend_async_context_find_t find; + zend_async_context_set_t set; + zend_async_context_unset_t unset; + zend_async_context_dispose_t dispose; +}; + +/////////////////////////////////////////////////////////////// +/// Future +/////////////////////////////////////////////////////////////// + +/** + * zend_future_t structure represents a future result container. + * It inherits from zend_async_event_t to participate in the event system. + */ +struct _zend_future_s { + zend_async_event_t event; /* Event inheritance (first member) */ + zval result; /* Result value */ + zend_object *exception; /* Exception object (NULL = no error) */ + /* Created file and line number */ + uint32_t lineno; + uint32_t completed_lineno; + /* Completed file and line number */ + zend_string *filename; + zend_string *completed_filename; +}; + +#define ZEND_FUTURE_F_THREAD_SAFE (1u << 10) +#define ZEND_FUTURE_F_IGNORED (1u << 11) + +#define ZEND_FUTURE_IS_COMPLETED(future) (((future)->event.flags & ZEND_ASYNC_EVENT_F_CLOSED) != 0) + +#define ZEND_FUTURE_SET_THREAD_SAFE(future) ((future)->event.flags |= ZEND_FUTURE_F_THREAD_SAFE) +#define ZEND_FUTURE_SET_IGNORED(future) ((future)->event.flags |= ZEND_FUTURE_F_IGNORED) + +#define ZEND_FUTURE_COMPLETE(future, result) \ + do { \ + if (ZEND_ASYNC_EVENT_IS_CLOSED(&(future)->event)) { \ + break; \ + } \ + ZVAL_COPY(&(future)->result, (result)); \ + (future)->event.stop(&(future)->event); \ + } while (0) + +#define ZEND_FUTURE_REJECT(future, error) \ + do { \ + if (ZEND_ASYNC_EVENT_IS_CLOSED(&(future)->event)) { \ + break; \ + } \ + (future)->exception = error; \ + GC_ADDREF(error); \ + (future)->event.stop(&(future)->event); \ + } while (0) + +/////////////////////////////////////////////////////////////// +/// Channel +/////////////////////////////////////////////////////////////// + +/** + * zend_async_channel_t structure represents a communication channel. + * It inherits from zend_async_event_t to participate in the event system. + */ +struct _zend_async_channel_s { + zend_async_event_t event; /* Event inheritance (first member) */ + /* Channel-specific method pointers */ + zend_channel_send_t send; /* Send method */ + zend_channel_receive_t receive; /* Receive method */ +}; + +#define ZEND_ASYNC_CHANNEL_F_THREAD_SAFE (1u << 10) + +/////////////////////////////////////////////////////////////// +/// Global Macros +/////////////////////////////////////////////////////////////// +/* + * Async module state + */ +typedef enum { + // The module is inactive. + ZEND_ASYNC_OFF, + // The module is ready for use but has not been activated yet. + ZEND_ASYNC_READY, + // The module is active and can be used. + ZEND_ASYNC_ACTIVE +} zend_async_state_t; + +typedef struct { + zend_async_state_t state; + /* The flag is TRUE if the Scheduler was able to gain control. */ + zend_atomic_bool heartbeat; + /* Equal TRUE if the scheduler executed now */ + bool in_scheduler_context; + /* Equal TRUE if the reactor is in the process of shutting down */ + bool graceful_shutdown; + /* Number of active coroutines */ + unsigned int active_coroutine_count; + /* Number of active event handles */ + unsigned int active_event_count; + /* The current coroutine context. */ + zend_coroutine_t *coroutine; + /* The main async scope. */ + zend_async_scope_t *main_scope; + /* Scheduler coroutine */ + zend_coroutine_t *scheduler; + /* Exit exception object */ + zend_object *exit_exception; +} zend_async_globals_t; + +BEGIN_EXTERN_C() +#ifdef ZTS +ZEND_API extern int zend_async_globals_id; +ZEND_API extern size_t zend_async_globals_offset; +#define ZEND_ASYNC_G(v) ZEND_TSRMG_FAST(zend_async_globals_offset, zend_async_globals_t *, v) +#else +#define ZEND_ASYNC_G(v) (zend_async_globals_api.v) +ZEND_API extern zend_async_globals_t zend_async_globals_api; +#endif +END_EXTERN_C() + +#define ZEND_ASYNC_ON (ZEND_ASYNC_G(state) > ZEND_ASYNC_OFF) +#define ZEND_ASYNC_IS_ACTIVE (ZEND_ASYNC_G(state) == ZEND_ASYNC_ACTIVE) +#define ZEND_ASYNC_IS_OFF (ZEND_ASYNC_G(state) == ZEND_ASYNC_OFF) +#define ZEND_ASYNC_IS_READY (ZEND_ASYNC_G(state) == ZEND_ASYNC_READY) +#define ZEND_ASYNC_ACTIVATE ZEND_ASYNC_G(state) = ZEND_ASYNC_ACTIVE +#define ZEND_ASYNC_INITIALIZE ZEND_ASYNC_G(state) = ZEND_ASYNC_READY +#define ZEND_ASYNC_DEACTIVATE ZEND_ASYNC_G(state) = ZEND_ASYNC_OFF +#define ZEND_ASYNC_SCHEDULER_ALIVE (zend_atomic_bool_load(&ZEND_ASYNC_G(heartbeat)) == true) +#define ZEND_ASYNC_SCHEDULER_HEARTBEAT zend_atomic_bool_store(&ZEND_ASYNC_G(heartbeat), true) +#define ZEND_ASYNC_SCHEDULER_WAIT zend_atomic_bool_store(&ZEND_ASYNC_G(heartbeat), false) +#define ZEND_ASYNC_SCHEDULER_CONTEXT ZEND_ASYNC_G(in_scheduler_context) +#define ZEND_ASYNC_IS_SCHEDULER_CONTEXT (ZEND_ASYNC_G(in_scheduler_context) == true) +#define ZEND_ASYNC_ACTIVE_COROUTINE_COUNT ZEND_ASYNC_G(active_coroutine_count) +#define ZEND_ASYNC_ACTIVE_EVENT_COUNT ZEND_ASYNC_G(active_event_count) +#define ZEND_ASYNC_GRACEFUL_SHUTDOWN ZEND_ASYNC_G(graceful_shutdown) +#define ZEND_ASYNC_EXIT_EXCEPTION ZEND_ASYNC_G(exit_exception) +#define ZEND_ASYNC_CURRENT_COROUTINE ZEND_ASYNC_G(coroutine) +#define ZEND_ASYNC_CURRENT_SCOPE (ZEND_ASYNC_G(coroutine) ? ZEND_ASYNC_G(coroutine)->scope : NULL) +#define ZEND_ASYNC_MAIN_SCOPE ZEND_ASYNC_G(main_scope) +#define ZEND_ASYNC_SCHEDULER ZEND_ASYNC_G(scheduler) + +#define ZEND_ASYNC_INCREASE_EVENT_COUNT \ + if (ZEND_ASYNC_G(active_event_count) < UINT_MAX) { \ + ZEND_ASYNC_G(active_event_count)++; \ + } else { \ + ZEND_ASSERT("The event count is already max."); \ + } + +#define ZEND_ASYNC_DECREASE_EVENT_COUNT \ + if (ZEND_ASYNC_G(active_event_count) > 0) { \ + ZEND_ASYNC_G(active_event_count)--; \ + } else { \ + ZEND_ASSERT("The event count is already zero."); \ + } + +#define ZEND_ASYNC_INCREASE_COROUTINE_COUNT \ + if (ZEND_ASYNC_G(active_coroutine_count) < UINT_MAX) { \ + ZEND_ASYNC_G(active_coroutine_count)++; \ + } else { \ + ZEND_ASSERT("The coroutine count is already max."); \ + } + +#define ZEND_ASYNC_DECREASE_COROUTINE_COUNT \ + if (ZEND_ASYNC_G(active_coroutine_count) > 0) { \ + ZEND_ASYNC_G(active_coroutine_count)--; \ + } else { \ + ZEND_ASSERT("The coroutine count is already zero."); \ + } + +BEGIN_EXTERN_C() + +ZEND_API bool zend_async_is_enabled(void); +ZEND_API bool zend_scheduler_is_enabled(void); + +void zend_async_api_shutdown(void); +void zend_async_globals_ctor(void); +void zend_async_globals_dtor(void); + +ZEND_API const char *zend_async_get_api_version(void); +ZEND_API int zend_async_get_api_version_number(void); + +ZEND_API ZEND_COLD zend_object *zend_async_new_exception( + zend_async_class type, const char *format, ...); +ZEND_API ZEND_COLD zend_object *zend_async_throw(zend_async_class type, const char *format, ...); +ZEND_API ZEND_COLD zend_object *zend_async_throw_cancellation(const char *format, ...); +ZEND_API ZEND_COLD zend_object *zend_async_throw_timeout( + const char *format, const zend_long timeout); + +/* Scheduler API */ + +ZEND_API extern zend_async_spawn_t zend_async_spawn_fn; +ZEND_API extern zend_async_new_coroutine_t zend_async_new_coroutine_fn; +ZEND_API extern zend_async_new_scope_t zend_async_new_scope_fn; +ZEND_API extern zend_async_suspend_t zend_async_suspend_fn; +ZEND_API extern zend_async_enqueue_coroutine_t zend_async_enqueue_coroutine_fn; +ZEND_API extern zend_async_resume_t zend_async_resume_fn; +ZEND_API extern zend_async_cancel_t zend_async_cancel_fn; +ZEND_API extern zend_async_spawn_and_throw_t zend_async_spawn_and_throw_fn; +ZEND_API extern zend_async_shutdown_t zend_async_shutdown_fn; +ZEND_API extern zend_async_engine_shutdown_t zend_async_engine_shutdown_fn; +ZEND_API extern zend_async_get_coroutines_t zend_async_get_coroutines_fn; +ZEND_API extern zend_async_add_microtask_t zend_async_add_microtask_fn; +ZEND_API extern zend_async_get_awaiting_info_t zend_async_get_awaiting_info_fn; +ZEND_API extern zend_async_get_class_ce_t zend_async_get_class_ce_fn; +ZEND_API extern zend_async_new_future_t zend_async_new_future_fn; +ZEND_API extern zend_async_new_channel_t zend_async_new_channel_fn; +ZEND_API extern zend_async_new_future_obj_t zend_async_new_future_obj_fn; +ZEND_API extern zend_async_new_channel_obj_t zend_async_new_channel_obj_fn; + +/* GROUP API */ +ZEND_API extern zend_async_new_group_t zend_async_new_group_fn; + +/* Iterator API */ +ZEND_API extern zend_async_new_iterator_t zend_async_new_iterator_fn; + +/* Context API */ +ZEND_API extern zend_async_new_context_t zend_async_new_context_fn; + +/* Internal Context API - Direct Functions */ +ZEND_API uint32_t zend_async_internal_context_key_alloc(const char *key_name); +ZEND_API const char *zend_async_internal_context_key_name(uint32_t key); +ZEND_API zval *zend_async_internal_context_find(zend_coroutine_t *coroutine, uint32_t key); +ZEND_API void zend_async_internal_context_set( + zend_coroutine_t *coroutine, uint32_t key, zval *value); +ZEND_API bool zend_async_internal_context_unset(zend_coroutine_t *coroutine, uint32_t key); + +/* Internal Context initialization and cleanup */ +ZEND_API void zend_async_init_internal_context_api(void); +ZEND_API void zend_async_coroutine_internal_context_dispose(zend_coroutine_t *coroutine); +ZEND_API void zend_async_internal_context_api_shutdown(void); +ZEND_API void zend_async_coroutine_internal_context_init(zend_coroutine_t *coroutine); + +/* Reactor API */ + +ZEND_API bool zend_async_reactor_is_enabled(void); +ZEND_API extern zend_async_reactor_startup_t zend_async_reactor_startup_fn; +ZEND_API extern zend_async_reactor_shutdown_t zend_async_reactor_shutdown_fn; +ZEND_API extern zend_async_reactor_execute_t zend_async_reactor_execute_fn; +ZEND_API extern zend_async_reactor_loop_alive_t zend_async_reactor_loop_alive_fn; +ZEND_API extern zend_async_new_socket_event_t zend_async_new_socket_event_fn; +ZEND_API extern zend_async_new_poll_event_t zend_async_new_poll_event_fn; +ZEND_API extern zend_async_new_timer_event_t zend_async_new_timer_event_fn; +ZEND_API extern zend_async_new_signal_event_t zend_async_new_signal_event_fn; +ZEND_API extern zend_async_new_process_event_t zend_async_new_process_event_fn; +ZEND_API extern zend_async_new_thread_event_t zend_async_new_thread_event_fn; +ZEND_API extern zend_async_new_filesystem_event_t zend_async_new_filesystem_event_fn; + +/* Socket Listening API */ + +ZEND_API extern zend_async_socket_listen_t zend_async_socket_listen_fn; + +/* DNS API */ + +ZEND_API extern zend_async_getnameinfo_t zend_async_getnameinfo_fn; +ZEND_API extern zend_async_getaddrinfo_t zend_async_getaddrinfo_fn; +ZEND_API extern zend_async_freeaddrinfo_t zend_async_freeaddrinfo_fn; + +/* Exec API */ +ZEND_API extern zend_async_new_exec_event_t zend_async_new_exec_event_fn; +ZEND_API extern zend_async_exec_t zend_async_exec_fn; + +/* Thread pool API */ +ZEND_API bool zend_async_thread_pool_is_enabled(void); +ZEND_API extern zend_async_queue_task_t zend_async_queue_task_fn; + +/* Trigger Event API */ +ZEND_API extern zend_async_new_trigger_event_t zend_async_new_trigger_event_fn; + +ZEND_API bool zend_async_scheduler_register(char *module, bool allow_override, + zend_async_new_coroutine_t new_coroutine_fn, zend_async_new_scope_t new_scope_fn, + zend_async_new_context_t new_context_fn, zend_async_spawn_t spawn_fn, + zend_async_suspend_t suspend_fn, zend_async_enqueue_coroutine_t enqueue_coroutine_fn, + zend_async_resume_t resume_fn, zend_async_cancel_t cancel_fn, + zend_async_spawn_and_throw_t spawn_and_throw_fn, zend_async_shutdown_t shutdown_fn, + zend_async_get_coroutines_t get_coroutines_fn, zend_async_add_microtask_t add_microtask_fn, + zend_async_get_awaiting_info_t get_awaiting_info_fn, + zend_async_get_class_ce_t get_class_ce_fn, zend_async_new_iterator_t new_iterator_fn, + zend_async_new_future_t new_future_fn, zend_async_new_channel_t new_channel_fn, + zend_async_new_future_obj_t new_future_obj_fn, + zend_async_new_channel_obj_t new_channel_obj_fn, zend_async_new_group_t new_group_fn, + zend_async_engine_shutdown_t engine_shutdown_fn); + +ZEND_API bool zend_async_reactor_register(char *module, bool allow_override, + zend_async_reactor_startup_t reactor_startup_fn, + zend_async_reactor_shutdown_t reactor_shutdown_fn, + zend_async_reactor_execute_t reactor_execute_fn, + zend_async_reactor_loop_alive_t reactor_loop_alive_fn, + zend_async_new_socket_event_t new_socket_event_fn, + zend_async_new_poll_event_t new_poll_event_fn, + zend_async_new_timer_event_t new_timer_event_fn, + zend_async_new_signal_event_t new_signal_event_fn, + zend_async_new_process_event_t new_process_event_fn, + zend_async_new_thread_event_t new_thread_event_fn, + zend_async_new_filesystem_event_t new_filesystem_event_fn, + zend_async_getnameinfo_t getnameinfo_fn, zend_async_getaddrinfo_t getaddrinfo_fn, + zend_async_freeaddrinfo_t freeaddrinfo_fn, zend_async_new_exec_event_t new_exec_event_fn, + zend_async_exec_t exec_fn, zend_async_new_trigger_event_t new_trigger_event_fn); + +ZEND_API void zend_async_thread_pool_register( + zend_string *module, bool allow_override, zend_async_queue_task_t queue_task_fn); + +ZEND_API bool zend_async_socket_listening_register( + char *module, bool allow_override, zend_async_socket_listen_t socket_listen_fn); + +ZEND_API zend_string *zend_coroutine_gen_info( + zend_coroutine_t *coroutine, char *zend_coroutine_name); + +ZEND_API zend_async_event_callback_t *zend_async_event_callback_new( + zend_async_event_callback_fn callback, size_t size); + +#define ZEND_ASYNC_EVENT_CALLBACK(callback) zend_async_event_callback_new(callback, 0) +#define ZEND_ASYNC_EVENT_CALLBACK_EX(callback, size) zend_async_event_callback_new(callback, size) + +ZEND_API zend_coroutine_event_callback_t *zend_async_coroutine_callback_new( + zend_coroutine_t *coroutine, zend_async_event_callback_fn callback, size_t size); +ZEND_API void coroutine_event_callback_dispose( + zend_async_event_callback_t *callback, zend_async_event_t *event); + +/* Waker API */ +ZEND_API zend_async_waker_t *zend_async_waker_new(zend_coroutine_t *coroutine); +ZEND_API zend_async_waker_t *zend_async_waker_new_with_timeout( + zend_coroutine_t *coroutine, const zend_ulong timeout, zend_async_event_t *cancellation); +ZEND_API bool zend_async_waker_apply_error(zend_async_waker_t *waker, zend_object *error, + bool transfer_error, bool override, bool for_cancellation); +ZEND_API void zend_async_waker_destroy(zend_coroutine_t *coroutine); +ZEND_API void zend_async_waker_add_triggered_event( + zend_coroutine_t *coroutine, zend_async_event_t *event); +ZEND_API bool zend_async_waker_is_event_exists( + zend_coroutine_t *coroutine, zend_async_event_t *event); + +#define ZEND_ASYNC_WAKER_APPLY_ERROR(waker, error, transfer) \ + zend_async_waker_apply_error((waker), (error), (transfer), true, false) +#define ZEND_ASYNC_WAKER_APPEND_ERROR(waker, error, transfer) \ + zend_async_waker_apply_error((waker), (error), (transfer), false, false) +#define ZEND_ASYNC_WAKER_APPLY_CANCELLATION(waker, error, transfer) \ + zend_async_waker_apply_error((waker), (error), (transfer), true, true) + +ZEND_API void zend_async_resume_when(zend_coroutine_t *coroutine, zend_async_event_t *event, + const bool trans_event, zend_async_event_callback_fn callback, + zend_coroutine_event_callback_t *event_callback); + +ZEND_API void zend_async_waker_callback_resolve(zend_async_event_t *event, + zend_async_event_callback_t *callback, void *result, zend_object *exception); + +ZEND_API void zend_async_waker_callback_cancel(zend_async_event_t *event, + zend_async_event_callback_t *callback, void *result, zend_object *exception); + +ZEND_API void zend_async_waker_callback_timeout(zend_async_event_t *event, + zend_async_event_callback_t *callback, void *result, zend_object *exception); + +/* Coroutine Switch Handlers API */ +ZEND_API uint32_t zend_coroutine_add_switch_handler( + zend_coroutine_t *coroutine, zend_coroutine_switch_handler_fn handler); + +ZEND_API bool zend_coroutine_remove_switch_handler( + zend_coroutine_t *coroutine, uint32_t handler_index); + +ZEND_API void zend_coroutine_call_switch_handlers( + zend_coroutine_t *coroutine, bool is_enter, bool is_finishing); + +ZEND_API void zend_coroutine_switch_handlers_init(zend_coroutine_t *coroutine); +ZEND_API void zend_coroutine_switch_handlers_destroy(zend_coroutine_t *coroutine); + +/* Global Main Coroutine Switch Handlers API */ +ZEND_API void zend_async_add_main_coroutine_start_handler(zend_coroutine_switch_handler_fn handler); + +ZEND_API void zend_async_call_main_coroutine_start_handlers(zend_coroutine_t *main_coroutine); + +/* Future API Functions */ +#define ZEND_ASYNC_NEW_FUTURE(thread_safe) zend_async_new_future_fn(thread_safe, 0) +#define ZEND_ASYNC_NEW_FUTURE_EX(thread_safe, extra_size) \ + zend_async_new_future_fn(thread_safe, extra_size) +#define ZEND_ASYNC_NEW_FUTURE_OBJ(future) zend_async_new_future_obj_fn(future) + +/* Channel API Functions */ +#define ZEND_ASYNC_NEW_CHANNEL(buffer_size, resizable, thread_safe) \ + zend_async_new_channel_fn(buffer_size, resizable, thread_safe, 0) +#define ZEND_ASYNC_NEW_CHANNEL_EX(buffer_size, resizable, thread_safe, extra_size) \ + zend_async_new_channel_fn(buffer_size, resizable, thread_safe, extra_size) +#define ZEND_ASYNC_NEW_CHANNEL_OBJ(channel) zend_async_new_channel_obj_fn(channel) + +/* GROUP API Functions */ +#define ZEND_ASYNC_NEW_GROUP() zend_async_new_group_fn(0) +#define ZEND_ASYNC_NEW_GROUP_EX(extra_size) zend_async_new_group_fn(extra_size) + +END_EXTERN_C() + +#define ZEND_ASYNC_IS_ENABLED() zend_async_is_enabled() +#define ZEND_ASYNC_SPAWN() zend_async_spawn_fn(NULL, NULL, 0) +#define ZEND_ASYNC_SPAWN_WITH(scope) zend_async_spawn_fn(scope, NULL, 0) +#define ZEND_ASYNC_SPAWN_WITH_PROVIDER(scope_provider) zend_async_spawn_fn(NULL, scope_provider, 0) +#define ZEND_ASYNC_SPAWN_WITH_PRIORITY(priority) zend_async_spawn_fn(NULL, NULL, priority) +#define ZEND_ASYNC_SPAWN_WITH_SCOPE_EX(scope, priority) zend_async_spawn_fn(scope, NULL, priority) +#define ZEND_ASYNC_NEW_COROUTINE(scope) zend_async_new_coroutine_fn(scope) +#define ZEND_ASYNC_NEW_SCOPE(parent) zend_async_new_scope_fn(parent, false) +#define ZEND_ASYNC_NEW_SCOPE_WITH_OBJECT(parent) zend_async_new_scope_fn(parent, true) +#define ZEND_ASYNC_SUSPEND() zend_async_suspend_fn(false) +#define ZEND_ASYNC_RUN_SCHEDULER_AFTER_MAIN() zend_async_suspend_fn(true) +#define ZEND_ASYNC_ENQUEUE_COROUTINE(coroutine) zend_async_enqueue_coroutine_fn(coroutine) +#define ZEND_ASYNC_RESUME(coroutine) zend_async_resume_fn(coroutine, NULL, false) +#define ZEND_ASYNC_RESUME_WITH_ERROR(coroutine, error, transfer_error) \ + zend_async_resume_fn(coroutine, error, transfer_error) +#define ZEND_ASYNC_CANCEL(coroutine, error, transfer_error) \ + zend_async_cancel_fn(coroutine, error, transfer_error, false) +#define ZEND_ASYNC_CANCEL_EX(coroutine, error, transfer_error, is_safely) \ + zend_async_cancel_fn(coroutine, error, transfer_error, is_safely) + +/** + * Spawns a new coroutine and throws the specified exception within it. + * + * This creates a dedicated coroutine for exception handling, ensuring proper + * scope-based error propagation when exceptions occur in microtasks or other + * contexts where direct throwing would bypass scope exception handling. + * + * @param exception The exception object to throw in the new coroutine + * @param scope Target scope for the coroutine (NULL for current scope) + * @param priority Priority level for the exception-throwing coroutine + */ +#define ZEND_ASYNC_SPAWN_AND_THROW(exception, scope, priority) \ + zend_async_spawn_and_throw_fn(exception, scope, priority) +/** + * The API method initiates graceful shutdown mode. + */ +#define ZEND_ASYNC_SHUTDOWN() zend_async_shutdown_fn() +#define ZEND_ASYNC_ENGINE_SHUTDOWN() zend_async_engine_shutdown_fn() +#define ZEND_ASYNC_GET_COROUTINES() zend_async_get_coroutines_fn() +#define ZEND_ASYNC_ADD_MICROTASK(microtask) zend_async_add_microtask_fn(microtask) +#define ZEND_ASYNC_GET_AWAITING_INFO(coroutine) zend_async_get_awaiting_info_fn(coroutine) +#define ZEND_ASYNC_GET_CE(type) zend_async_get_class_ce_fn(type) +#define ZEND_ASYNC_GET_EXCEPTION_CE(type) zend_async_get_class_ce_fn(type) + +#define ZEND_ASYNC_REACTOR_IS_ENABLED() zend_async_reactor_is_enabled() +#define ZEND_ASYNC_REACTOR_STARTUP() zend_async_reactor_startup_fn() +#define ZEND_ASYNC_REACTOR_SHUTDOWN() zend_async_reactor_shutdown_fn() + +#define ZEND_ASYNC_REACTOR_EXECUTE(no_wait) zend_async_reactor_execute_fn(no_wait) +#define ZEND_ASYNC_REACTOR_LOOP_ALIVE() zend_async_reactor_loop_alive_fn() + +#define ZEND_ASYNC_NEW_SOCKET_EVENT(socket, events) \ + zend_async_new_socket_event_fn(socket, events, 0) +#define ZEND_ASYNC_NEW_SOCKET_EVENT_EX(socket, events, extra_size) \ + zend_async_new_socket_event_fn(socket, events, extra_size) +#define ZEND_ASYNC_NEW_POLL_EVENT(fh, socket, events) \ + zend_async_new_poll_event_fn(fh, socket, events, 0) +#define ZEND_ASYNC_NEW_POLL_EVENT_EX(fh, socket, events, extra_size) \ + zend_async_new_poll_event_fn(fh, socket, events, extra_size) +#define ZEND_ASYNC_NEW_TIMER_EVENT(timeout, is_periodic) \ + zend_async_new_timer_event_fn(timeout, 0, is_periodic, 0) +#define ZEND_ASYNC_NEW_TIMER_EVENT_EX(timeout, is_periodic, extra_size) \ + zend_async_new_timer_event_fn(timeout, 0, is_periodic, extra_size) +#define ZEND_ASYNC_NEW_TIMER_EVENT_NS(timeout, nanoseconds, is_periodic) \ + zend_async_new_timer_event_fn(timeout, nanoseconds, is_periodic, 0) +#define ZEND_ASYNC_NEW_TIMER_EVENT_NS_EX(timeout, nanoseconds, is_periodic, extra_size) \ + zend_async_new_timer_event_fn(timeout, nanoseconds, is_periodic, extra_size) +#define ZEND_ASYNC_NEW_SIGNAL_EVENT(signum) zend_async_new_signal_event_fn(signum, 0) +#define ZEND_ASYNC_NEW_SIGNAL_EVENT_EX(signum, extra_size) \ + zend_async_new_signal_event_fn(signum, extra_size) +#define ZEND_ASYNC_NEW_PROCESS_EVENT(process_handle) \ + zend_async_new_process_event_fn(process_handle, 0) +#define ZEND_ASYNC_NEW_PROCESS_EVENT_EX(process_handle, extra_size) \ + zend_async_new_process_event_fn(process_handle, extra_size) +#define ZEND_ASYNC_NEW_THREAD_EVENT(entry, arg) zend_async_new_thread_event_fn(entry, arg, 0) +#define ZEND_ASYNC_NEW_THREAD_EVENT_EX(entry, arg) zend_async_new_thread_event_fn(entry, arg, 0) +#define ZEND_ASYNC_NEW_FILESYSTEM_EVENT(path, flags) \ + zend_async_new_filesystem_event_fn(path, flags, 0) +#define ZEND_ASYNC_NEW_FILESYSTEM_EVENT_EX(path, flags, extra_size) \ + zend_async_new_filesystem_event_fn(path, flags, extra_size) + +#define ZEND_ASYNC_GETNAMEINFO(addr, flags) zend_async_getnameinfo_fn(addr, flags, 0) +#define ZEND_ASYNC_GETNAMEINFO_EX(addr, flags, extra_size) \ + zend_async_getnameinfo_fn(addr, flags, extra_size) +#define ZEND_ASYNC_GETADDRINFO(node, service, hints) \ + zend_async_getaddrinfo_fn(node, service, hints, 0) +#define ZEND_ASYNC_GETADDRINFO_EX(node, service, hints, extra_size) \ + zend_async_getaddrinfo_fn(node, service, hints, extra_size) +#define ZEND_ASYNC_FREEADDRINFO(ai) zend_async_freeaddrinfo_fn(ai) + +#define ZEND_ASYNC_NEW_EXEC_EVENT( \ + exec_mode, cmd, return_buffer, return_value, std_error, cwd, env) \ + zend_async_new_exec_event_fn( \ + exec_mode, cmd, return_buffer, return_value, std_error, cwd, env, 0) +#define ZEND_ASYNC_NEW_EXEC_EVENT_EX( \ + exec_mode, cmd, return_buffer, return_value, std_error, cwd, env, extra_size) \ + zend_async_new_exec_event_fn( \ + exec_mode, cmd, return_buffer, return_value, std_error, cwd, env, extra_size) +#define ZEND_ASYNC_EXEC(exec_mode, cmd, return_buffer, return_value, std_error, cwd, env, timeout) \ + zend_async_exec_fn(exec_mode, cmd, return_buffer, return_value, std_error, cwd, env, timeout) + +#define ZEND_ASYNC_QUEUE_TASK(task) zend_async_queue_task_fn(task) + +/* Trigger Event API Macros */ +#define ZEND_ASYNC_NEW_TRIGGER_EVENT() zend_async_new_trigger_event_fn(0) +#define ZEND_ASYNC_NEW_TRIGGER_EVENT_EX(extra_size) zend_async_new_trigger_event_fn(extra_size) + +/* Socket Listening API Macros */ +#define ZEND_ASYNC_SOCKET_LISTEN(host, port, backlog) \ + zend_async_socket_listen_fn(host, port, backlog, 0) +#define ZEND_ASYNC_SOCKET_LISTEN_EX(host, port, backlog, extra_size) \ + zend_async_socket_listen_fn(host, port, backlog, extra_size) + +/* Iterator API Macros */ +#define ZEND_ASYNC_NEW_ITERATOR_SCOPE( \ + array, zend_iterator, fcall, handler, scope, concurrency, priority) \ + zend_async_new_iterator_fn( \ + array, zend_iterator, fcall, handler, scope, concurrency, priority, 0) +#define ZEND_ASYNC_NEW_ITERATOR(array, zend_iterator, fcall, handler, concurrency, priority) \ + zend_async_new_iterator_fn(array, zend_iterator, fcall, handler, NULL, concurrency, priority, 0) +#define ZEND_ASYNC_NEW_ITERATOR_EX( \ + array, zend_iterator, fcall, handler, concurrency, priority, size) \ + zend_async_new_iterator_fn( \ + array, zend_iterator, fcall, handler, NULL, concurrency, priority, size) + +/* Context API Macros */ +#define ZEND_ASYNC_NEW_CONTEXT(parent) zend_async_new_context_fn(parent) +#define ZEND_ASYNC_CURRENT_CONTEXT \ + (ZEND_ASYNC_G(coroutine) != NULL ? ZEND_ASYNC_G(coroutine)->scope->context : NULL) +#define ZEND_ASYNC_GET_COROUTINE_CONTEXT() \ + ((ZEND_ASYNC_G(coroutine)) \ + ? (ZEND_ASYNC_G(coroutine)->context ? ZEND_ASYNC_G(coroutine)->context \ + : (ZEND_ASYNC_G(coroutine)->context \ + = ZEND_ASYNC_NEW_CONTEXT(NULL))) \ + : NULL) + +/* Internal Context API Macros */ +#define ZEND_ASYNC_INTERNAL_CONTEXT_KEY_ALLOC(key_name) \ + zend_async_internal_context_key_alloc(key_name) +#define ZEND_ASYNC_INTERNAL_CONTEXT_KEY_NAME(key) zend_async_internal_context_key_name(key) +#define ZEND_ASYNC_INTERNAL_CONTEXT_FIND(coro, key) zend_async_internal_context_find(coro, key) +#define ZEND_ASYNC_INTERNAL_CONTEXT_SET(coro, key, value) \ + zend_async_internal_context_set(coro, key, value) +#define ZEND_ASYNC_INTERNAL_CONTEXT_UNSET(coro, key) zend_async_internal_context_unset(coro, key) + +/* Coroutine Switch Handlers API Macros */ +#define ZEND_COROUTINE_ADD_SWITCH_HANDLER(coroutine, handler) \ + zend_coroutine_add_switch_handler(coroutine, handler) + +#define ZEND_COROUTINE_ENTER(coroutine) zend_coroutine_call_switch_handlers(coroutine, true, false); +#define ZEND_COROUTINE_LEAVE(coroutine) zend_coroutine_call_switch_handlers(coroutine, false, false) +#define ZEND_COROUTINE_FINISH(coroutine) zend_coroutine_call_switch_handlers(coroutine, false, true) + +/* Global Main Coroutine Switch Handlers API Macros */ +#define ZEND_ASYNC_ADD_MAIN_COROUTINE_START_HANDLER(handler) \ + zend_async_add_main_coroutine_start_handler(handler) +#define ZEND_ASYNC_ADD_SWITCH_HANDLER(handler) \ + if (ZEND_ASYNC_CURRENT_COROUTINE) { \ + zend_coroutine_add_switch_handler(ZEND_ASYNC_CURRENT_COROUTINE, handler); \ + } else { \ + zend_async_add_main_coroutine_start_handler(handler); \ + } + +#endif // ZEND_ASYNC_API_H \ No newline at end of file diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 6d2a6195cd52c..2e0ec8c464bcf 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -49,6 +49,8 @@ #include #endif +#include "zend_async_API.h" + ZEND_API void (*zend_execute_ex)(zend_execute_data *execute_data); ZEND_API void (*zend_execute_internal)(zend_execute_data *execute_data, zval *return_value); ZEND_API zend_class_entry *(*zend_autoload)(zend_string *name, zend_string *lc_name); @@ -199,6 +201,13 @@ void init_executor(void) /* {{{ */ EG(filename_override) = NULL; EG(lineno_override) = -1; + EG(shutdown_context) = (zend_shutdown_context_t) { + .is_started = false, + .coroutine = NULL, + .num_elements = 0, + .idx = 0 + }; + zend_max_execution_timer_init(); zend_fiber_init(); zend_weakrefs_init(); @@ -207,19 +216,6 @@ void init_executor(void) /* {{{ */ } /* }}} */ -static int zval_call_destructor(zval *zv) /* {{{ */ -{ - if (Z_TYPE_P(zv) == IS_INDIRECT) { - zv = Z_INDIRECT_P(zv); - } - if (Z_TYPE_P(zv) == IS_OBJECT && Z_REFCOUNT_P(zv) == 1) { - return ZEND_HASH_APPLY_REMOVE; - } else { - return ZEND_HASH_APPLY_KEEP; - } -} -/* }}} */ - static void zend_unclean_zval_ptr_dtor(zval *zv) /* {{{ */ { if (Z_TYPE_P(zv) == IS_INDIRECT) { @@ -248,22 +244,125 @@ static ZEND_COLD void zend_throw_or_error(int fetch_type, zend_class_entry *exce } /* }}} */ +static void shutdown_destructors_coroutine_dtor(zend_coroutine_t *coroutine) /* {{{ */ +{ + zend_shutdown_context_t *shutdown_context = &EG(shutdown_context); + + if (shutdown_context->coroutine == coroutine) { + shutdown_context->coroutine = NULL; + shutdown_context->is_started = false; + zend_error(E_CORE_ERROR, "Shutdown destructors coroutine was not finished property"); + EG(symbol_table).pDestructor = zend_unclean_zval_ptr_dtor; + shutdown_destructors(); + } +} + +static bool shutdown_destructors_context_switch_handler( + zend_coroutine_t *coroutine, + bool is_enter, + bool is_finishing +) { + if (is_enter) { + return true; + } + + if (is_finishing) { + return false; + } + + zend_shutdown_context_t *shutdown_context = &EG(shutdown_context); + + if (false == shutdown_context->is_started) { + return false; + } + + zend_coroutine_t *shutdown_coroutine = ZEND_ASYNC_SPAWN_WITH_SCOPE_EX(ZEND_ASYNC_MAIN_SCOPE, 1); + shutdown_coroutine->internal_entry = shutdown_destructors; + shutdown_coroutine->extended_dispose = shutdown_destructors_coroutine_dtor; + + return false; +} + void shutdown_destructors(void) /* {{{ */ { + zend_coroutine_t *coroutine = ZEND_ASYNC_CURRENT_COROUTINE; + bool should_continue = false; + + zend_shutdown_context_t *shutdown_context = &EG(shutdown_context); + + if (coroutine == NULL) { + ZEND_ASYNC_ADD_MAIN_COROUTINE_START_HANDLER(shutdown_destructors_context_switch_handler); + } else { + ZEND_COROUTINE_ADD_SWITCH_HANDLER(coroutine, shutdown_destructors_context_switch_handler); + } + + HashTable *symbol_table = &EG(symbol_table); + + if (false == shutdown_context->is_started) { + shutdown_context->is_started = true; + shutdown_context->coroutine = coroutine; + shutdown_context->num_elements = zend_hash_num_elements(symbol_table); + shutdown_context->idx = symbol_table->nNumUsed; + } + if (CG(unclean_shutdown)) { EG(symbol_table).pDestructor = zend_unclean_zval_ptr_dtor; } + zend_try { - uint32_t symbols; do { - symbols = zend_hash_num_elements(&EG(symbol_table)); - zend_hash_reverse_apply(&EG(symbol_table), (apply_func_t) zval_call_destructor); - } while (symbols != zend_hash_num_elements(&EG(symbol_table))); - zend_objects_store_call_destructors(&EG(objects_store)); + if (should_continue) { + shutdown_context->num_elements = zend_hash_num_elements(symbol_table); + shutdown_context->idx = symbol_table->nNumUsed; + } else { + should_continue = true; + } + + while (shutdown_context->idx > 0) { + + shutdown_context->idx--; + + Bucket *p = symbol_table->arData + shutdown_context->idx; + + if (UNEXPECTED(Z_TYPE(p->val) == IS_UNDEF)) { + continue; + } + + zval *zv = &p->val; + if (Z_TYPE_P(zv) == IS_INDIRECT) { + zv = Z_INDIRECT_P(zv); + } + + if (Z_TYPE_P(zv) == IS_OBJECT && Z_REFCOUNT_P(zv) == 1) { + zend_hash_del_bucket(symbol_table, p); + } + + // If the coroutine has changed + if (coroutine != ZEND_ASYNC_CURRENT_COROUTINE) { + should_continue = false; + break; + } + } + + if (false == should_continue) { + break; + } + + } while (shutdown_context->num_elements != zend_hash_num_elements(symbol_table)); + + if (should_continue) { + shutdown_context->is_started = false; + shutdown_context->coroutine = NULL; + zend_objects_store_call_destructors_async(&EG(objects_store)); + } } zend_catch { - /* if we couldn't destruct cleanly, mark all objects as destructed anyway */ - zend_objects_store_mark_destructed(&EG(objects_store)); + EG(symbol_table).pDestructor = zend_unclean_zval_ptr_dtor; + shutdown_context->is_started = false; + shutdown_destructors(); + zend_bailout(); } zend_end_try(); + + shutdown_context->is_started = false; } /* }}} */ diff --git a/Zend/zend_fibers.c b/Zend/zend_fibers.c index 97b7cdcc911b7..2748aa119b13a 100644 --- a/Zend/zend_fibers.c +++ b/Zend/zend_fibers.c @@ -32,6 +32,8 @@ #include "zend_generators.h" #include "zend_fibers.h" + +#include "zend_async_API.h" #include "zend_fibers_arginfo.h" #ifdef HAVE_VALGRIND @@ -461,11 +463,14 @@ ZEND_API void zend_fiber_destroy_context(zend_fiber_context *context) { zend_observer_fiber_destroy_notify(context); + // This code allows freeing the memory of the context independently of the stack memory. + zend_fiber_stack *stack = context->stack; + if (context->cleanup) { context->cleanup(context); } - zend_fiber_stack_free(context->stack); + zend_fiber_stack_free(stack); } ZEND_API void zend_fiber_switch_context(zend_fiber_transfer *transfer) @@ -559,6 +564,17 @@ static void zend_fiber_cleanup(zend_fiber_context *context) fiber->caller = NULL; } +static zend_always_inline bool can_use_fiber(void) +{ + if (UNEXPECTED(ZEND_ASYNC_IS_ACTIVE)) { + zend_throw_error(zend_ce_fiber_error, "Cannot create a fiber while an True Async is active"); + return false; + } + + ZEND_ASYNC_DEACTIVATE; + return true; +} + static ZEND_STACK_ALIGNED void zend_fiber_execute(zend_fiber_transfer *transfer) { ZEND_ASSERT(Z_TYPE(transfer->value) == IS_NULL && "Initial transfer value to fiber context must be NULL"); @@ -708,6 +724,10 @@ ZEND_API zend_result zend_fiber_start(zend_fiber *fiber, zval *return_value) { ZEND_ASSERT(fiber->context.status == ZEND_FIBER_STATUS_INIT); + if (UNEXPECTED(false == can_use_fiber())) { + return FAILURE; + } + if (zend_fiber_init_context(&fiber->context, zend_ce_fiber, zend_fiber_execute, EG(fiber_stack_size)) == FAILURE) { return FAILURE; } @@ -723,6 +743,10 @@ ZEND_API zend_result zend_fiber_start(zend_fiber *fiber, zval *return_value) ZEND_API void zend_fiber_resume(zend_fiber *fiber, zval *value, zval *return_value) { + if (UNEXPECTED(false == can_use_fiber())) { + return; + } + ZEND_ASSERT(fiber->context.status == ZEND_FIBER_STATUS_SUSPENDED && fiber->caller == NULL); fiber->stack_bottom->prev_execute_data = EG(current_execute_data); @@ -734,6 +758,10 @@ ZEND_API void zend_fiber_resume(zend_fiber *fiber, zval *value, zval *return_val ZEND_API void zend_fiber_resume_exception(zend_fiber *fiber, zval *exception, zval *return_value) { + if (UNEXPECTED(false == can_use_fiber())) { + return; + } + ZEND_ASSERT(fiber->context.status == ZEND_FIBER_STATUS_SUSPENDED && fiber->caller == NULL); fiber->stack_bottom->prev_execute_data = EG(current_execute_data); @@ -745,6 +773,10 @@ ZEND_API void zend_fiber_resume_exception(zend_fiber *fiber, zval *exception, zv ZEND_API void zend_fiber_suspend(zend_fiber *fiber, zval *value, zval *return_value) { + if (UNEXPECTED(false == can_use_fiber())) { + return; + } + fiber->stack_bottom->prev_execute_data = NULL; zend_fiber_transfer transfer = zend_fiber_suspend_internal(fiber, value); @@ -873,6 +905,10 @@ ZEND_METHOD(Fiber, __construct) Z_PARAM_FUNC(fci, fcc) ZEND_PARSE_PARAMETERS_END(); + if (UNEXPECTED(false == can_use_fiber())) { + RETURN_THROWS(); + } + zend_fiber *fiber = (zend_fiber *) Z_OBJ_P(ZEND_THIS); if (UNEXPECTED(fiber->context.status != ZEND_FIBER_STATUS_INIT || Z_TYPE(fiber->fci.function_name) != IS_UNDEF)) { @@ -895,6 +931,10 @@ ZEND_METHOD(Fiber, start) Z_PARAM_VARIADIC_WITH_NAMED(fiber->fci.params, fiber->fci.param_count, fiber->fci.named_params); ZEND_PARSE_PARAMETERS_END(); + if (UNEXPECTED(false == can_use_fiber())) { + RETURN_THROWS(); + } + if (UNEXPECTED(zend_fiber_switch_blocked())) { zend_throw_error(zend_ce_fiber_error, "Cannot switch fibers in current execution context"); RETURN_THROWS(); diff --git a/Zend/zend_gc.c b/Zend/zend_gc.c index 8da55fcd48f66..5fcd9a6685de7 100644 --- a/Zend/zend_gc.c +++ b/Zend/zend_gc.c @@ -68,6 +68,7 @@ */ #include "zend.h" #include "zend_API.h" +#include "zend_async_API.h" #include "zend_compile.h" #include "zend_errors.h" #include "zend_fibers.h" @@ -248,6 +249,36 @@ typedef struct _gc_root_buffer { zend_refcounted *ref; } gc_root_buffer; +typedef struct _gc_stack gc_stack; + +#define GC_STACK_SEGMENT_SIZE (((4096 - ZEND_MM_OVERHEAD) / sizeof(void*)) - 2) + +struct _gc_stack { + gc_stack *prev; + gc_stack *next; + zend_refcounted *data[GC_STACK_SEGMENT_SIZE]; +}; + +typedef enum { + GC_ASYNC_STATE_NONE = 0, + GC_ASYNC_STATE_INIT, // initial state + GC_ASYNC_STATE_RUNNING, // GC is running + GC_ASYNC_STATE_CONTINUE // GC is called from GC coroutine +} gc_async_state_t; + +typedef struct { + gc_async_state_t state; // state + int total_count; // total freed objects in this GC run + int count; // freed objects in this GC run + + uint32_t gc_flags; // GC_HAS_DESTRUCTORS, etc. + bool should_rerun_gc; // run GC again after destructors + bool did_rerun_gc; // guard against endless reruns + + zend_hrtime_t start_time; // start of full GC pass + zend_hrtime_t dtor_start_time; // start of dtor phase +} gc_async_context_t; + typedef struct _zend_gc_globals { gc_root_buffer *buf; /* preallocated arrays of buffers */ @@ -274,6 +305,11 @@ typedef struct _zend_gc_globals { uint32_t dtor_end; zend_fiber *dtor_fiber; bool dtor_fiber_running; + gc_async_context_t async_context; /* async context for gc */ + gc_stack *gc_stack; /* local mark/scan stack */ + zend_coroutine_t *dtor_coroutine; + zend_async_scope_t *dtor_scope; + zend_async_microtask_t *microtask; #if GC_BENCH uint32_t root_buf_length; @@ -308,17 +344,6 @@ static zend_gc_globals gc_globals; # define GC_BENCH_PEAK(peak, counter) #endif - -#define GC_STACK_SEGMENT_SIZE (((4096 - ZEND_MM_OVERHEAD) / sizeof(void*)) - 2) - -typedef struct _gc_stack gc_stack; - -struct _gc_stack { - gc_stack *prev; - gc_stack *next; - zend_refcounted *data[GC_STACK_SEGMENT_SIZE]; -}; - #define GC_STACK_DCL(init) \ gc_stack *_stack = init; \ size_t _top = 0; @@ -504,6 +529,12 @@ static void gc_globals_ctor_ex(zend_gc_globals *gc_globals) gc_globals->dtor_fiber = NULL; gc_globals->dtor_fiber_running = false; + gc_globals->dtor_coroutine = NULL; + gc_globals->dtor_scope = NULL; + gc_globals->microtask = NULL; + gc_globals->async_context.state = GC_ASYNC_STATE_NONE; + gc_globals->gc_stack = NULL; + #if GC_BENCH gc_globals->root_buf_length = 0; gc_globals->root_buf_peak = 0; @@ -528,6 +559,10 @@ void gc_globals_dtor(void) #ifndef ZTS root_buffer_dtor(&gc_globals); #endif + + if (GC_G(dtor_scope)) { + GC_G(dtor_scope) = NULL; + } } void gc_reset(void) @@ -1813,6 +1848,8 @@ static zend_always_inline zend_result gc_call_destructors(uint32_t idx, uint32_t gc_root_buffer *current; zend_refcounted *p; + const bool in_coroutine = GC_G(dtor_coroutine) != NULL; + /* The root buffer might be reallocated during destructors calls, * make sure to reload pointers as necessary. */ while (idx != end) { @@ -1825,17 +1862,25 @@ static zend_always_inline zend_result gc_call_destructors(uint32_t idx, uint32_t * could have already been invoked indirectly by some other * destructor. */ if (!(OBJ_FLAGS(p) & IS_OBJ_DESTRUCTOR_CALLED)) { - if (fiber != NULL) { + if (fiber != NULL || in_coroutine) { GC_G(dtor_idx) = idx; } zend_object *obj = (zend_object*)p; GC_TRACE_REF(obj, "calling destructor"); GC_ADD_FLAGS(obj, IS_OBJ_DESTRUCTOR_CALLED); GC_ADDREF(obj); + if (in_coroutine) { + ZEND_ASYNC_CURRENT_COROUTINE->extended_data = obj; + } + obj->handlers->dtor_obj(obj); + if (in_coroutine) { + ZEND_ASYNC_CURRENT_COROUTINE->extended_data = NULL; + } GC_TRACE_REF(obj, "returned from destructor"); GC_DELREF(obj); - if (UNEXPECTED(fiber != NULL && GC_G(dtor_fiber) != fiber)) { + if (UNEXPECTED((fiber != NULL && GC_G(dtor_fiber) != fiber) + || (in_coroutine && GC_G(dtor_coroutine) != ZEND_ASYNC_CURRENT_COROUTINE))) { /* We resumed after suspension */ gc_check_possible_root((zend_refcounted*)&obj->gc); return FAILURE; @@ -1910,31 +1955,255 @@ static zend_never_inline void gc_call_destructors_in_fiber(uint32_t end) } } +static void zend_gc_collect_cycles_microtask(zend_async_microtask_t *task); + +static void zend_gc_collect_cycles_microtask_dtor(zend_async_microtask_t *task) +{ + if (task->ref_count > 1) { + task->ref_count--; + return; + } + + if (GC_G(microtask) == task) { + GC_G(microtask) = NULL; + } + + efree(task); +} + +static void zend_gc_collect_cycles_coroutine(void) +{ + GC_TRACE("GC coroutine started"); + + if (GC_G(microtask) == NULL) { + zend_async_microtask_t *task = ecalloc(1, sizeof(zend_async_microtask_t)); + task->handler = zend_gc_collect_cycles_microtask; + task->dtor = zend_gc_collect_cycles_microtask_dtor; + task->ref_count = 1; + GC_G(microtask) = task; + } + + if (GC_ASYNC_STATE_NONE == GC_G(async_context).state) { + gc_async_context_t *context = &GC_G(async_context); + context->state = GC_ASYNC_STATE_INIT; + context->total_count = 0; + context->count = 0; + context->start_time = zend_hrtime(); + context->should_rerun_gc = 0; + context->did_rerun_gc = 0; + context->gc_flags = 0; + } else if (GC_G(async_context).state == GC_ASYNC_STATE_RUNNING) { + GC_G(async_context).state = GC_ASYNC_STATE_CONTINUE; + } + + if (GC_G(gc_stack) == NULL) { + gc_stack *stack = ecalloc(1, sizeof(gc_stack)); + stack->prev = NULL; + stack->next = NULL; + GC_G(gc_stack) = stack; + } + + ZEND_ASYNC_ADD_MICROTASK(GC_G(microtask)); + zend_gc_collect_cycles(); + + // Coroutines were separated. + if (GC_G(dtor_coroutine) != ZEND_ASYNC_CURRENT_COROUTINE) { + return; + } + + if (GC_G(microtask) != NULL) { + zend_async_microtask_t *microtask = GC_G(microtask); + GC_G(microtask) = NULL; + microtask->is_cancelled = true; + ZEND_ASYNC_MICROTASK_RELEASE(microtask); + } +} + +static zend_string* coroutine_info(zend_async_event_t *event) +{ + zend_coroutine_t *coroutine = (zend_coroutine_t *) event; + + if (coroutine->extended_data == NULL) { + return zend_coroutine_gen_info(coroutine, "zend_gc_collect_cycles"); + } + + zend_object *obj = coroutine->extended_data; + const char *class_name = ZSTR_VAL(obj->ce->name); + const char *method_name = "__destructor"; + + zend_string *tmp = zend_strpprintf(0, "%s::%s", class_name, method_name); + zend_string *result = zend_coroutine_gen_info(coroutine, ZSTR_VAL(tmp)); + zend_string_release(tmp); + + return result; +} + +static void coroutine_dispose(zend_coroutine_t *coroutine) +{ + if (coroutine == GC_G(dtor_coroutine)) { + GC_TRACE("GC coroutine finished"); + GC_G(dtor_coroutine) = NULL; + GC_G(dtor_scope) = NULL; + + if (GC_G(microtask) != NULL) { + zend_async_microtask_t *microtask = GC_G(microtask); + GC_G(microtask) = NULL; + microtask->is_cancelled = true; + ZEND_ASYNC_MICROTASK_RELEASE(microtask); + } + + if (GC_G(gc_stack) != NULL) { + gc_stack *stack = GC_G(gc_stack); + GC_G(gc_stack) = NULL; + gc_stack_free(stack); + efree(stack); + } + + if (GC_G(async_context.state)) { + gc_async_context_t *context = &GC_G(async_context); + context->state = GC_ASYNC_STATE_NONE; + context->total_count = 0; + context->count = 0; + context->start_time = 0; + context->should_rerun_gc = 0; + context->did_rerun_gc = 0; + context->gc_flags = 0; + } + } +} + +static zend_always_inline zend_coroutine_t* new_gc_coroutine(void) +{ + zend_coroutine_t * coroutine = ZEND_ASYNC_SPAWN_WITH(GC_G(dtor_scope)); + + if (UNEXPECTED(coroutine == NULL)) { + return NULL; + } + + GC_G(dtor_coroutine) = coroutine; + + coroutine->internal_entry = zend_gc_collect_cycles_coroutine; + coroutine->extended_data = NULL; + coroutine->extended_dispose = coroutine_dispose; + coroutine->event.info = coroutine_info; + + return coroutine; +} + +static void zend_gc_collect_cycles_microtask(zend_async_microtask_t *task) +{ + if (UNEXPECTED(new_gc_coroutine() == NULL)) { + task->is_cancelled = true; + return; + } +} + +static zend_always_inline void start_gc_in_coroutine(void) +{ + GC_TRACE("Try to start GC in coroutine"); + + if (GC_G(dtor_scope) == NULL) { + GC_G(dtor_scope) = ZEND_ASYNC_NEW_SCOPE(NULL); + + if (UNEXPECTED(GC_G(dtor_scope) == NULL)) { + zend_error_noreturn(E_ERROR, "Unable to create destructor scope"); + } + } + + if (UNEXPECTED(new_gc_coroutine() == NULL)) { + zend_error_noreturn(E_ERROR, "Unable to spawn destructor coroutine"); + } +} + ZEND_API int zend_gc_collect_cycles(void) { - int total_count = 0; - bool should_rerun_gc = 0; - bool did_rerun_gc = 0; + if (UNEXPECTED(ZEND_ASYNC_IS_ACTIVE && ZEND_ASYNC_CURRENT_COROUTINE != GC_G(dtor_coroutine))) { + + if (GC_G(dtor_coroutine)) { + return 0; + } + + start_gc_in_coroutine(); + return 0; + } + + zend_hrtime_t dtor_start_time = 0; + + // Someone is trying to invoke GC from within a destructor? + // We don’t know what that is, but we have nothing to do here. + if (UNEXPECTED(GC_G(async_context).state == GC_ASYNC_STATE_RUNNING)) { + return 0; + } + + const bool in_fiber = EG(active_fiber) != NULL; + + // + // We might enter this context from different coroutines, so we don’t initialize anything here. + // + gc_async_context_t *context = NULL; + gc_stack *stack = NULL; + + if (in_fiber) { + context = ecalloc(1, sizeof(gc_async_context_t)); + stack = emalloc(sizeof(gc_stack)); + } else { + context = &GC_G(async_context); + stack = GC_G(gc_stack); + } + + if (UNEXPECTED(context->state == GC_ASYNC_STATE_CONTINUE)) { + // If we reach this point, it means the destructor call was interrupted by a suspend() operation, + // so we continue the process from the next element. + GC_G(async_context).state = GC_ASYNC_STATE_RUNNING; + GC_G(dtor_idx)++; + dtor_start_time = zend_hrtime(); + goto continue_calling_destructors; + } else { + context->state = GC_ASYNC_STATE_INIT; + context->total_count = 0; + context->count = 0; + context->start_time = zend_hrtime(); + context->should_rerun_gc = 0; + context->did_rerun_gc = 0; + context->gc_flags = 0; + // reset the destructor index + GC_G(dtor_idx) = GC_FIRST_ROOT; + + if (false == in_fiber) { + if (GC_G(gc_stack) == NULL) { + stack = emalloc(sizeof(gc_stack)); + stack->prev = NULL; + stack->next = NULL; + GC_G(gc_stack) = stack; + } else { + stack = GC_G(gc_stack); + } + } else { + stack->prev = NULL; + stack->next = NULL; + } + } + + context->state = GC_ASYNC_STATE_RUNNING; + - zend_hrtime_t start_time = zend_hrtime(); if (GC_G(num_roots) && !GC_G(gc_active)) { zend_gc_remove_root_tmpvars(); } rerun_gc: if (GC_G(num_roots)) { - int count; gc_root_buffer *current, *last; zend_refcounted *p; uint32_t gc_flags = 0; - uint32_t idx, end; - gc_stack stack; + uint32_t idx, end = 0; - stack.prev = NULL; - stack.next = NULL; + stack->next = NULL; + stack->prev = NULL; if (GC_G(gc_active)) { - GC_G(collector_time) += zend_hrtime() - start_time; + GC_G(collector_time) += zend_hrtime() - context->start_time; + GC_G(async_context).state = GC_ASYNC_STATE_NONE; return 0; } @@ -1943,17 +2212,17 @@ ZEND_API int zend_gc_collect_cycles(void) GC_G(gc_active) = 1; GC_TRACE("Marking roots"); - gc_mark_roots(&stack); + gc_mark_roots(stack); GC_TRACE("Scanning roots"); - gc_scan_roots(&stack); + gc_scan_roots(stack); GC_TRACE("Collecting roots"); - count = gc_collect_roots(&gc_flags, &stack); + context->count = gc_collect_roots(&gc_flags, stack); if (!GC_G(num_roots)) { /* nothing to free */ GC_TRACE("Nothing to free"); - gc_stack_free(&stack); + gc_stack_free(stack); GC_G(gc_active) = 0; goto finish; } @@ -1968,7 +2237,7 @@ ZEND_API int zend_gc_collect_cycles(void) * modify any refcounts, so we have no real way to detect this situation * short of rerunning full GC tracing. What we do instead is to only run * destructors at this point and automatically re-run GC afterwards. */ - should_rerun_gc = 1; + context->should_rerun_gc = 1; /* Mark all roots for which a dtor will be invoked as DTOR_GARBAGE. Additionally * color them purple. This serves a double purpose: First, they should be @@ -2002,16 +2271,27 @@ ZEND_API int zend_gc_collect_cycles(void) while (idx != end) { if (GC_IS_DTOR_GARBAGE(current->ref)) { p = GC_GET_PTR(current->ref); - count -= gc_remove_nested_data_from_buffer(p, current, &stack); + context->count -= gc_remove_nested_data_from_buffer(p, current, stack); } current++; idx++; } /* Actually call destructors. */ - zend_hrtime_t dtor_start_time = zend_hrtime(); + dtor_start_time = zend_hrtime(); if (EXPECTED(!EG(active_fiber))) { - gc_call_destructors(GC_FIRST_ROOT, end, NULL); +continue_calling_destructors: + end = GC_G(first_unused); + if (UNEXPECTED(FAILURE == gc_call_destructors(GC_G(dtor_idx), GC_G(first_unused), NULL))) { + // + // gc_call_destructors returns FAILURE when a destructor interrupts execution, + // i.e. calls suspend(). + // At this point, we are inside a foreign coroutine from which we must immediately exit, + // because the process continues elsewhere. + // + GC_G(dtor_time) += zend_hrtime() - dtor_start_time; + return context->total_count; + } } else { gc_call_destructors_in_fiber(end); } @@ -2020,12 +2300,17 @@ ZEND_API int zend_gc_collect_cycles(void) if (GC_G(gc_protected)) { /* something went wrong */ zend_get_gc_buffer_release(); - GC_G(collector_time) += zend_hrtime() - start_time; + GC_G(collector_time) += zend_hrtime() - context->start_time; + GC_G(async_context).state = GC_ASYNC_STATE_NONE; return 0; } } - gc_stack_free(&stack); + gc_stack_free(stack); + + if (false == in_fiber) { + end = GC_G(first_unused); + } /* Destroy zvals. The root buffer may be reallocated. */ GC_TRACE("Destroying zvals"); @@ -2083,8 +2368,8 @@ ZEND_API int zend_gc_collect_cycles(void) GC_G(free_time) += zend_hrtime() - free_start_time; GC_TRACE("Collection finished"); - GC_G(collected) += count; - total_count += count; + GC_G(collected) += context->count; + context->total_count += context->count; GC_G(gc_active) = 0; } @@ -2093,8 +2378,8 @@ ZEND_API int zend_gc_collect_cycles(void) /* Objects with destructors were removed from this GC run. Rerun GC right away to clean them * up. We do this only once: If we encounter more destructors on the second run, we'll not * run GC another time. */ - if (should_rerun_gc && !did_rerun_gc) { - did_rerun_gc = 1; + if (context->should_rerun_gc && !context->did_rerun_gc) { + context->did_rerun_gc = 1; goto rerun_gc; } @@ -2107,8 +2392,22 @@ ZEND_API int zend_gc_collect_cycles(void) zend_gc_check_root_tmpvars(); GC_G(gc_active) = 0; - GC_G(collector_time) += zend_hrtime() - start_time; - return total_count; + GC_G(collector_time) += zend_hrtime() - context->start_time; + + if (in_fiber) { + const int total_count = context->total_count; + efree(context); + efree(stack); + return total_count; + } else { + GC_G(async_context).state = GC_ASYNC_STATE_NONE; + if (GC_G(gc_stack) != NULL) { + GC_G(gc_stack) = NULL; + efree(stack); + } + } + + return context->total_count; } ZEND_API void zend_gc_get_status(zend_gc_status *status) diff --git a/Zend/zend_globals.h b/Zend/zend_globals.h index b4e94ec1f9892..038a13e85fcf2 100644 --- a/Zend/zend_globals.h +++ b/Zend/zend_globals.h @@ -163,6 +163,12 @@ struct _zend_compiler_globals { #endif }; +typedef struct { + bool is_started; + void *coroutine; + uint32_t num_elements; + uint32_t idx; +} zend_shutdown_context_t; struct _zend_executor_globals { zval uninitialized_zval; @@ -259,6 +265,9 @@ struct _zend_executor_globals { const zend_op *opline_before_exception; zend_op exception_op[3]; + // Used to track the state of shutdown destructors in coroutines + zend_shutdown_context_t shutdown_context; + struct _zend_module_entry *current_module; bool active; diff --git a/Zend/zend_objects_API.c b/Zend/zend_objects_API.c index c19873cf3be30..9342453533f24 100644 --- a/Zend/zend_objects_API.c +++ b/Zend/zend_objects_API.c @@ -23,6 +23,8 @@ #include "zend_variables.h" #include "zend_API.h" #include "zend_objects_API.h" + +#include "zend_async_API.h" #include "zend_fibers.h" ZEND_API void ZEND_FASTCALL zend_objects_store_init(zend_objects_store *objects, uint32_t init_size) @@ -63,6 +65,98 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_call_destructors(zend_objects_sto } } +static void store_call_destructors_coroutine_dtor(zend_coroutine_t *coroutine) +{ + zend_shutdown_context_t *shutdown_context = &EG(shutdown_context); + + if (shutdown_context->coroutine == coroutine) { + shutdown_context->coroutine = NULL; + shutdown_context->is_started = false; + zend_error(E_CORE_ERROR, "Shutdown destructors coroutine was not finished property"); + shutdown_destructors(); + } +} + +static void store_call_destructors_entry(void) +{ + zend_objects_store_call_destructors_async(&EG(objects_store)); +} + +static bool store_call_destructors_context_switch_handler( + zend_coroutine_t *coroutine, + bool is_enter, + bool is_finishing +) { + if (is_enter) { + return true; + } + + if (is_finishing) { + return false; + } + + zend_shutdown_context_t *shutdown_context = &EG(shutdown_context); + + if (false == shutdown_context->is_started) { + return false; + } + + zend_coroutine_t *shutdown_coroutine = ZEND_ASYNC_SPAWN_WITH_SCOPE_EX(ZEND_ASYNC_MAIN_SCOPE, 1); + shutdown_coroutine->internal_entry = store_call_destructors_entry; + shutdown_coroutine->extended_dispose = store_call_destructors_coroutine_dtor; + + return false; +} + +ZEND_API void ZEND_FASTCALL zend_objects_store_call_destructors_async(zend_objects_store *objects) +{ + if (objects->top <= 1) { + return; + } + + EG(flags) |= EG_FLAGS_OBJECT_STORE_NO_REUSE; + + zend_class_entry *coroutine_ce = ZEND_ASYNC_GET_CE(ZEND_ASYNC_CLASS_COROUTINE); + + zend_coroutine_t *coroutine = ZEND_ASYNC_CURRENT_COROUTINE; + + zend_shutdown_context_t *shutdown_context = &EG(shutdown_context); + + if (coroutine == NULL) { + ZEND_ASYNC_ADD_MAIN_COROUTINE_START_HANDLER(store_call_destructors_context_switch_handler); + } else { + ZEND_COROUTINE_ADD_SWITCH_HANDLER(coroutine, store_call_destructors_context_switch_handler); + } + + if (false == shutdown_context->is_started) { + shutdown_context->is_started = true; + shutdown_context->coroutine = coroutine; + shutdown_context->idx = 1; + } + + for (uint32_t i = shutdown_context->idx; i < objects->top; i++) { + zend_object *obj = objects->object_buckets[i]; + if (IS_OBJ_VALID(obj) && obj->ce != coroutine_ce) { + if (!(OBJ_FLAGS(obj) & IS_OBJ_DESTRUCTOR_CALLED)) { + GC_ADD_FLAGS(obj, IS_OBJ_DESTRUCTOR_CALLED); + + if (obj->handlers->dtor_obj != zend_objects_destroy_object || obj->ce->destructor) { + shutdown_context->idx = i; + GC_ADDREF(obj); + obj->handlers->dtor_obj(obj); + GC_DELREF(obj); + + if (coroutine != ZEND_ASYNC_CURRENT_COROUTINE) { + return; + } + } + } + } + } + + shutdown_context->is_started = false; +} + ZEND_API void ZEND_FASTCALL zend_objects_store_mark_destructed(zend_objects_store *objects) { if (objects->object_buckets && objects->top > 1) { diff --git a/Zend/zend_objects_API.h b/Zend/zend_objects_API.h index 86c3a49f8c8c5..8bd7392de9804 100644 --- a/Zend/zend_objects_API.h +++ b/Zend/zend_objects_API.h @@ -55,6 +55,7 @@ typedef struct _zend_objects_store { BEGIN_EXTERN_C() ZEND_API void ZEND_FASTCALL zend_objects_store_init(zend_objects_store *objects, uint32_t init_size); ZEND_API void ZEND_FASTCALL zend_objects_store_call_destructors(zend_objects_store *objects); +ZEND_API void ZEND_FASTCALL zend_objects_store_call_destructors_async(zend_objects_store *objects); ZEND_API void ZEND_FASTCALL zend_objects_store_mark_destructed(zend_objects_store *objects); ZEND_API void ZEND_FASTCALL zend_objects_store_free_object_storage(zend_objects_store *objects, bool fast_shutdown); ZEND_API void ZEND_FASTCALL zend_objects_store_destroy(zend_objects_store *objects); diff --git a/configure.ac b/configure.ac index 2bd6ae26ce625..ec934f13bdb37 100644 --- a/configure.ac +++ b/configure.ac @@ -1786,6 +1786,9 @@ PHP_ADD_SOURCES([Zend], m4_normalize([ ]), [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 $fiber_asm_cflag]) + PHP_ADD_SOURCES([Zend], m4_normalize([zend_async_API.c]), + [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 $fiber_asm_cflag]) + PHP_ADD_MAKEFILE_FRAGMENT([$abs_srcdir/scripts/Makefile.frag], [$abs_srcdir/scripts], [scripts]) diff --git a/docs/source/index.rst b/docs/source/index.rst index 21e2526f47f64..de0cd477c469d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,6 +15,12 @@ core/data-structures/index +.. toctree:: + :caption: TrueAsync API + :hidden: + + true_async_api/index + .. toctree:: :caption: Miscellaneous :hidden: diff --git a/docs/source/true_async_api/api-reference.rst b/docs/source/true_async_api/api-reference.rst new file mode 100644 index 0000000000000..5deecf10dbd7c --- /dev/null +++ b/docs/source/true_async_api/api-reference.rst @@ -0,0 +1,424 @@ +############### + API Reference +############### + +This section provides comprehensive documentation for the TrueAsync API functions, macros, and data +structures based on the actual implementation in ``Zend/zend_async_API.h``. + +************* + API Version +************* + +.. code:: c + + #define ZEND_ASYNC_API "TrueAsync API v0.4.0" + #define ZEND_ASYNC_API_VERSION_MAJOR 0 + #define ZEND_ASYNC_API_VERSION_MINOR 4 + #define ZEND_ASYNC_API_VERSION_PATCH 0 + +***************** + Core API Macros +***************** + +Enablement Check +================ + +.. code:: c + + ZEND_ASYNC_IS_ENABLED() + +Returns ``true`` if async functionality is available, ``false`` otherwise. Always check this before +using any async APIs. + +.. code:: c + + ZEND_ASYNC_REACTOR_IS_ENABLED() + +Returns ``true`` if reactor (event loop) is available and functional. + +********************** + Coroutine Operations +********************** + +Coroutine Creation +================== + +.. code:: c + + ZEND_ASYNC_SPAWN() + ZEND_ASYNC_SPAWN_WITH(scope) + ZEND_ASYNC_SPAWN_WITH_PROVIDER(scope_provider) + ZEND_ASYNC_SPAWN_WITH_PRIORITY(priority) + ZEND_ASYNC_SPAWN_WITH_SCOPE_EX(scope, priority) + +Creates and spawns new coroutines with various configuration options: + +- ``ZEND_ASYNC_SPAWN()`` - Create coroutine with default settings +- ``ZEND_ASYNC_SPAWN_WITH(scope)`` - Create coroutine within specific scope +- ``ZEND_ASYNC_SPAWN_WITH_PROVIDER(scope_provider)`` - Use scope provider object +- ``ZEND_ASYNC_SPAWN_WITH_PRIORITY(priority)`` - Set coroutine priority +- ``ZEND_ASYNC_SPAWN_WITH_SCOPE_EX(scope, priority)`` - Both scope and priority + +**Priority levels:** + +.. code:: c + + typedef enum { + ZEND_COROUTINE_NORMAL = 0, + ZEND_COROUTINE_HI_PRIORITY = 255 + } zend_coroutine_priority; + +Coroutine Management +==================== + +.. code:: c + + ZEND_ASYNC_NEW_COROUTINE(scope) + +Creates a new coroutine object without starting it. + +.. code:: c + + ZEND_ASYNC_ENQUEUE_COROUTINE(coroutine) + +Adds coroutine to execution queue for scheduling. + +.. code:: c + + ZEND_ASYNC_SUSPEND() + ZEND_ASYNC_RUN_SCHEDULER_AFTER_MAIN() + +Suspends current coroutine execution: + +- ``SUSPEND()`` - Normal suspension +- ``RUN_SCHEDULER_AFTER_MAIN()`` - Suspend from main context + +Coroutine Control +================= + +.. code:: c + + ZEND_ASYNC_RESUME(coroutine) + ZEND_ASYNC_RESUME_WITH_ERROR(coroutine, error, transfer_error) + +Resume coroutine execution: + +- ``RESUME(coroutine)`` - Normal resume +- ``RESUME_WITH_ERROR(coroutine, error, transfer_error)`` - Resume with exception + +.. code:: c + + ZEND_ASYNC_CANCEL(coroutine, error, transfer_error) + ZEND_ASYNC_CANCEL_EX(coroutine, error, transfer_error, is_safely) + +Cancel coroutine execution: + +- ``CANCEL()`` - Cancel with default safety +- ``CANCEL_EX()`` - Cancel with explicit safety flag + +*********** + Scope API +*********** + +Scope Creation +============== + +.. code:: c + + ZEND_ASYNC_NEW_SCOPE(parent) + ZEND_ASYNC_NEW_SCOPE_WITH_OBJECT(parent) + +Create new async scopes for coroutine isolation: + +- ``NEW_SCOPE(parent)`` - Create scope with parent reference +- ``NEW_SCOPE_WITH_OBJECT(parent)`` - Create scope backed by zend_object + +Scope Flags +=========== + +.. code:: c + + #define ZEND_ASYNC_SCOPE_IS_CLOSED(scope) + #define ZEND_ASYNC_SCOPE_IS_CANCELLED(scope) + #define ZEND_ASYNC_SCOPE_IS_DISPOSING(scope) + +Check scope status: + +- ``IS_CLOSED`` - Scope has been closed +- ``IS_CANCELLED`` - Scope was cancelled +- ``IS_DISPOSING`` - Scope is being disposed + +*********** + Event API +*********** + +Event Lifecycle +=============== + +All async events implement these core function pointers: + +.. code:: c + + typedef void (*zend_async_event_start_t)(zend_async_event_t *event); + typedef void (*zend_async_event_stop_t)(zend_async_event_t *event); + typedef void (*zend_async_event_dispose_t)(zend_async_event_t *event); + +Event State Checking +==================== + +.. code:: c + + ZEND_ASYNC_EVENT_IS_CLOSED(event) + +Check if event has been closed. + +Event Reference Counting +======================== + +.. code:: c + + ZEND_ASYNC_EVENT_REF(event) + ZEND_ASYNC_EVENT_ADD_REF(event) + ZEND_ASYNC_EVENT_DEL_REF(event) + +Manage event reference counts for memory safety. + +Event Flags +=========== + +.. code:: c + + #define ZEND_ASYNC_EVENT_F_CLOSED (1u << 0) /* event was closed */ + #define ZEND_ASYNC_EVENT_F_RESULT_USED (1u << 1) /* result will be used */ + #define ZEND_ASYNC_EVENT_F_EXC_CAUGHT (1u << 2) /* error was caught */ + #define ZEND_ASYNC_EVENT_F_ZVAL_RESULT (1u << 3) /* result is zval */ + +.. code:: c + + ZEND_ASYNC_EVENT_SET_CLOSED(event) + ZEND_ASYNC_EVENT_CLR_CLOSED(event) + +Manipulate event flags. + +************* + Reactor API +************* + +Reactor Control +=============== + +.. code:: c + + ZEND_ASYNC_REACTOR_STARTUP() + +Start the async reactor (event loop). + +Poll Events +=========== + +.. code:: c + + typedef enum { + ASYNC_READABLE = 1, + ASYNC_WRITABLE = 2, + ASYNC_DISCONNECT = 4, + ASYNC_PRIORITIZED = 8 + } async_poll_event; + +Event flags for socket polling operations. + +****************** + Signal Constants +****************** + +.. code:: c + + #define ZEND_ASYNC_SIGHUP 1 + #define ZEND_ASYNC_SIGINT 2 + #define ZEND_ASYNC_SIGQUIT 3 + #define ZEND_ASYNC_SIGILL 4 + #define ZEND_ASYNC_SIGTERM 15 + #define ZEND_ASYNC_SIGBREAK 21 + #define ZEND_ASYNC_SIGABRT 22 + #define ZEND_ASYNC_SIGWINCH 28 + +Cross-platform signal constants for async signal handling. + +******************* + Process Execution +******************* + +Execution Modes +=============== + +.. code:: c + + typedef enum { + ZEND_ASYNC_EXEC_MODE_EXEC, /* exec() - last line only */ + ZEND_ASYNC_EXEC_MODE_SYSTEM, /* system() - print + return last */ + ZEND_ASYNC_EXEC_MODE_EXEC_ARRAY, /* exec() with array output */ + ZEND_ASYNC_EXEC_MODE_PASSTHRU, /* passthru() - binary output */ + ZEND_ASYNC_EXEC_MODE_SHELL_EXEC /* shell_exec() - buffer output */ + } zend_async_exec_mode; + +************************ + Future and Channel API +************************ + +Futures +======= + +.. code:: c + + ZEND_ASYNC_NEW_FUTURE(thread_safe) + ZEND_ASYNC_NEW_FUTURE_EX(thread_safe, extra_size) + +Create future objects for async result handling. + +Channels +======== + +.. code:: c + + ZEND_ASYNC_NEW_CHANNEL(buffer_size, resizable, thread_safe) + ZEND_ASYNC_NEW_CHANNEL_EX(buffer_size, resizable, thread_safe, extra_size) + +Create channels for coroutine communication. + +***************************** + API Context Switch Handlers +***************************** + +Registration +============ + +.. code:: c + + uint32_t zend_coroutine_add_switch_handler( + zend_coroutine_t *coroutine, + zend_coroutine_switch_handler_fn handler + ); + + bool zend_coroutine_remove_switch_handler( + zend_coroutine_t *coroutine, + uint32_t handler_index + ); + +Add/remove context switch handlers for individual coroutines. + +Global Handlers +=============== + +.. code:: c + + void zend_async_add_main_coroutine_start_handler( + zend_coroutine_switch_handler_fn handler + ); + +Add global handlers that are called when main coroutines start. + +Handler Function Type +===================== + +.. code:: c + + typedef bool (*zend_coroutine_switch_handler_fn)( + zend_coroutine_t *coroutine, + bool is_enter, /* true = entering, false = leaving */ + bool is_finishing /* true = coroutine finishing */ + ); + +Returns ``true`` to keep handler, ``false`` to remove it after execution. + +******************* + Utility Functions +******************* + +System Management +================= + +.. code:: c + + ZEND_ASYNC_SHUTDOWN() + ZEND_ASYNC_ENGINE_SHUTDOWN() + +Initiate graceful shutdown of async subsystem. + +.. code:: c + + ZEND_ASYNC_GET_COROUTINES() + ZEND_ASYNC_GET_AWAITING_INFO(coroutine) + +Get information about active coroutines and their await status. + +Microtasks +========== + +.. code:: c + + ZEND_ASYNC_ADD_MICROTASK(microtask) + +Add microtask to execution queue. + +Exception Handling +================== + +.. code:: c + + ZEND_ASYNC_SPAWN_AND_THROW(exception, scope, priority) + +Spawn coroutine specifically for throwing an exception within a scope. + +********************** + API Error Management +********************** + +API Exception Classes +===================== + +.. code:: c + + typedef enum { + ZEND_ASYNC_CLASS_NO = 0, + ZEND_ASYNC_CLASS_AWAITABLE = 1, + ZEND_ASYNC_CLASS_COROUTINE = 2 + } zend_async_class; + + ZEND_ASYNC_GET_CE(type) + ZEND_ASYNC_GET_EXCEPTION_CE(type) + +Get class entries for async-related classes and exceptions. + +************ + Data Types +************ + +Platform Types +============== + +.. code:: c + + #ifdef PHP_WIN32 + typedef HANDLE zend_file_descriptor_t; + typedef DWORD zend_process_id_t; + typedef HANDLE zend_process_t; + typedef SOCKET zend_socket_t; + #else + typedef int zend_file_descriptor_t; + typedef pid_t zend_process_id_t; + typedef pid_t zend_process_t; + typedef int zend_socket_t; + #endif + +Cross-platform type definitions for file descriptors, processes, and sockets. + +******************** + API Best Practices +******************** + +#. **Always check enablement** with ``ZEND_ASYNC_IS_ENABLED()`` before using APIs +#. **Use appropriate scopes** for coroutine isolation +#. **Handle reference counting** properly with event objects +#. **Register cleanup handlers** using context switch handlers +#. **Check reactor availability** before using event-based operations +#. **Use priority levels** appropriately for coroutine scheduling diff --git a/docs/source/true_async_api/architecture.rst b/docs/source/true_async_api/architecture.rst new file mode 100644 index 0000000000000..8a61ab8ae6b2f --- /dev/null +++ b/docs/source/true_async_api/architecture.rst @@ -0,0 +1,27 @@ +############## + Architecture +############## + +The TrueAsync API follows a clean separation-of-concerns architecture that splits API definition +from implementation. This design enables modularity, testability, and support for multiple backend +implementations. + +The **True Async API** is divided into three main functional blocks: + +- **Scheduler** Manages coroutines and the main event loop. +- **Reactor** Handles everything related to input/output (I/O), including sockets, timers, and file + descriptors. +- **Thread Pool** Allows execution of blocking or heavy tasks in a thread pool. + +--- + +## Implementation Flexibility + +The True Async API provides clearly separated interfaces for each block, allowing them to be +implemented independently—even across different PHP extensions. For example: + +- The Scheduler can be implemented as part of the PHP core. +- The Reactor can be implemented in a separate extension using `libuv`. +- The Thread Pool can be based on native threads or a library like `pthreads`. + +This approach gives developers maximum flexibility and control over asynchronous execution. diff --git a/docs/source/true_async_api/coroutines.rst b/docs/source/true_async_api/coroutines.rst new file mode 100644 index 0000000000000..8e94cc71b9250 --- /dev/null +++ b/docs/source/true_async_api/coroutines.rst @@ -0,0 +1,469 @@ +############ + Coroutines +############ + +Coroutines are the core building blocks of the TrueAsync API. They represent lightweight execution +contexts that can be suspended and resumed, enabling cooperative multitasking without traditional +thread overhead. Every coroutine is implemented as an event (``zend_coroutine_t`` extends +``zend_async_event_t``) and participates in the standard event lifecycle. + +****************** + Coroutine Basics +****************** + +A coroutine represents a function or code block that can be paused during execution and resumed +later. Unlike traditional threads, coroutines are cooperatively scheduled - they yield control +voluntarily rather than being preemptively interrupted. + +Key characteristics: + +- **Lightweight** - Minimal memory overhead compared to threads +- **Cooperative** - Explicit yield points, no race conditions +- **Event-based** - Coroutines are events that can await other events +- **Scoped** - Each coroutine runs within a scope for context isolation + +********************* + Creating Coroutines +********************* + +The TrueAsync API provides several ways to create coroutines: + +.. code:: c + + // Create a new coroutine within default scope + zend_coroutine_t *coro = ZEND_ASYNC_SPAWN(); + + // Create with specific scope + zend_async_scope_t *scope = ZEND_ASYNC_NEW_SCOPE(parent_scope); + zend_coroutine_t *coro = ZEND_ASYNC_SPAWN_WITH(scope); + + // Create with scope provider object + zend_coroutine_t *coro = ZEND_ASYNC_SPAWN_WITH_PROVIDER(scope_provider); + +Coroutines can be either: + +**Userland coroutines** + Execute PHP code through ``zend_fcall_t`` callback mechanism + +**Internal coroutines** + Execute C code through ``zend_coroutine_entry_t`` function pointer + +********************* + Coroutine Lifecycle +********************* + +Coroutines follow a well-defined lifecycle: + +#. **Creation** - Coroutine is spawned but not yet started +#. **Enqueue** - Added to execution queue via ``ZEND_ASYNC_ENQUEUE_COROUTINE()`` +#. **Execution** - Runs until suspension point or completion +#. **Suspension** - Yields control while waiting for events +#. **Resume** - Continues execution when events complete +#. **Completion** - Finishes with result or exception + +.. code:: c + + zend_coroutine_t *coro = ZEND_ASYNC_SPAWN(); + + // Set up coroutine execution + coro->fcall = &my_function_call; + coro->scope = current_scope; + + // Start execution + ZEND_ASYNC_ENQUEUE_COROUTINE(coro); + + // Later: resume with result + ZEND_ASYNC_RESUME(coro); + + // Or resume with error + ZEND_ASYNC_RESUME_WITH_ERROR(coro, exception, transfer_error); + +******************* + Coroutine Context +******************* + +Each coroutine maintains its own context information: + +**Scope Context** (``zend_async_scope_t *scope``) + Links to parent scope for hierarchical context management + +**Async Context** (``zend_async_context_t *context``) + User-defined context variables accessible from PHP code + +**Internal Context** (``HashTable *internal_context``) + Extension-private data storage with numeric keys + +**Switch Handlers** (``zend_coroutine_switch_handlers_vector_t *switch_handlers``) + Functions called when entering/leaving the coroutine + +************************* + Context Switch Handlers +************************* + +Context switch handlers are one of the most powerful features of the coroutine system. They allow +extensions to hook into coroutine context switches and perform necessary cleanup, initialization, or +state management. + +Handler Function Signature +========================== + +.. code:: c + + typedef bool (*zend_coroutine_switch_handler_fn)( + zend_coroutine_t *coroutine, + bool is_enter, /* true = entering coroutine, false = leaving */ + bool is_finishing /* true = coroutine is finishing */ + /* returns: true = keep handler, false = remove handler after execution */ + ); + +The handler receives three parameters: + +- **coroutine** - The coroutine being switched to/from +- **is_enter** - ``true`` when entering coroutine, ``false`` when leaving +- **is_finishing** - ``true`` when coroutine is finishing execution + +The return value determines handler lifetime: + +- **true** - Keep handler for future context switches +- **false** - Remove handler immediately after execution + +This allows handlers to perform one-time initialization and then remove themselves automatically. + +Registering Handlers +==================== + +There are two types of switch handlers: + +**Per-Coroutine Handlers** + +.. code:: c + + // Add handler to specific coroutine + uint32_t handler_id = ZEND_COROUTINE_ADD_SWITCH_HANDLER(coroutine, my_handler); + + // Remove specific handler + zend_coroutine_remove_switch_handler(coroutine, handler_id); + +**Global Main Coroutine Handlers** + +.. code:: c + + // Register global handler for all main coroutines + ZEND_ASYNC_ADD_MAIN_COROUTINE_START_HANDLER(my_main_handler); + +Global handlers are automatically copied to each main coroutine when it starts and can remove +themselves by returning ``false``. + +Practical Example: Output Buffering +=================================== + +The ``main/output.c`` module provides an excellent example of context switch handlers in action. The +output system needs to isolate output buffers between the main execution context and coroutines to +prevent buffer conflicts. + +.. code:: c + + static bool php_output_main_coroutine_start_handler( + zend_coroutine_t *coroutine, bool is_enter, bool is_finishing) + { + if (false == is_enter || OG(handlers).elements == 0) { + return false; /* Remove handler - no work to do */ + } + + /* Create coroutine-specific output context */ + php_output_context_t *ctx = ecalloc(1, sizeof(php_output_context_t)); + php_output_init_async_context(ctx); + + /* Copy output handlers from global to coroutine context */ + php_output_handler **src_handlers = (php_output_handler **)zend_stack_base(&OG(handlers)); + int handler_count = zend_stack_count(&OG(handlers)); + + for (int i = 0; i < handler_count; i++) { + php_output_handler *handler = src_handlers[i]; + zend_stack_push(&ctx->handlers, &handler); + } + + /* Store context in coroutine internal_context */ + zval ctx_zval; + ZVAL_PTR(&ctx_zval, ctx); + ZEND_ASYNC_INTERNAL_CONTEXT_SET(coroutine, php_output_context_key, &ctx_zval); + + /* Clean global handlers to avoid conflicts */ + zend_stack_destroy(&OG(handlers)); + zend_stack_init(&OG(handlers), sizeof(php_output_handler *)); + OG(active) = NULL; + + /* Add cleanup callback for coroutine completion */ + zend_coroutine_event_callback_t *cleanup_callback = + zend_async_coroutine_callback_new(coroutine, php_output_coroutine_cleanup_callback, 0); + coroutine->event.add_callback(&coroutine->event, &cleanup_callback->base); + + return false; /* Remove handler - initialization is complete */ + } + +This handler: + +#. **Detects main coroutine entry** - Only acts when ``is_enter`` is true +#. **Creates isolated context** - Copies output handlers to coroutine-specific storage +#. **Stores in internal_context** - Uses numeric key for fast access +#. **Cleans global state** - Prevents conflicts between main and coroutine contexts +#. **Registers cleanup** - Ensures proper resource cleanup on coroutine completion +#. **Removes itself** - Returns ``false`` since initialization is one-time only + +When to Use Context Switch Handlers +=================================== + +Context switch handlers are ideal for: + +**Resource Isolation** + Separating global state between main execution and coroutines (like output buffers, error + handlers, etc.) + +**Context Migration** + Moving data from one execution context to another + +**State Initialization** + Setting up coroutine-specific resources or configurations + +**Cleanup Management** + Registering cleanup callbacks for coroutine completion + +**Performance Optimization** + Pre-computing or caching data when entering frequently-used coroutines + +********************** + Internal Context API +********************** + +The internal context system provides a type-safe, efficient way for PHP extensions to store private +data associated with coroutines. Unlike userland context variables, internal context uses numeric +keys for faster access and is completely isolated from PHP code. + +Why Internal Context? +===================== + +Before internal context, extensions had limited options for storing coroutine-specific data: + +- **Global variables** - Not coroutine-safe, cause conflicts +- **Object properties** - Not available for internal coroutines +- **Manual management** - Complex, error-prone cleanup + +Internal context solves these problems by providing: + +- **Automatic cleanup** - Data is freed when coroutine completes +- **Type safety** - Values stored as ``zval`` with proper reference counting +- **Uniqueness** - Each extension gets private numeric keys +- **Performance** - Hash table lookup by integer key + +Key Allocation +============== + +Extensions allocate unique keys during module initialization: + +.. code:: c + + static uint32_t my_extension_context_key = 0; + + PHP_MINIT_FUNCTION(my_extension) + { + // Allocate unique key for this extension + my_extension_context_key = ZEND_ASYNC_INTERNAL_CONTEXT_KEY_ALLOC("my_extension_data"); + return SUCCESS; + } + +The ``ZEND_ASYNC_INTERNAL_CONTEXT_KEY_ALLOC`` macro: + +- Takes a static string identifier (for debugging) +- Returns a unique numeric key +- Validates that the same string address always gets the same key +- Is thread-safe in ZTS builds + +Storing and Retrieving Data +=========================== + +.. code:: c + + void my_coroutine_handler(zend_coroutine_t *coroutine) + { + // Store data in coroutine context + zval my_data; + ZVAL_LONG(&my_data, 42); + ZEND_ASYNC_INTERNAL_CONTEXT_SET(coroutine, my_extension_context_key, &my_data); + + // Later: retrieve data + zval *stored_data = ZEND_ASYNC_INTERNAL_CONTEXT_FIND(coroutine, my_extension_context_key); + if (stored_data && Z_TYPE_P(stored_data) == IS_LONG) { + long value = Z_LVAL_P(stored_data); + // Use value... + } + + // Optional: remove data explicitly + ZEND_ASYNC_INTERNAL_CONTEXT_UNSET(coroutine, my_extension_context_key); + } + +The API automatically handles: + +- **Reference counting** - ``zval`` reference counts are managed properly +- **Memory management** - All context data is freed when coroutine ends +- **Thread safety** - Operations are safe in ZTS builds + +Advanced Usage Patterns +======================= + +**Caching expensive computations:** + +.. code:: c + + zval *cached_result = ZEND_ASYNC_INTERNAL_CONTEXT_FIND(coroutine, cache_key); + if (!cached_result) { + // Expensive operation + zval computed_value; + ZVAL_STRING(&computed_value, expensive_computation()); + ZEND_ASYNC_INTERNAL_CONTEXT_SET(coroutine, cache_key, &computed_value); + cached_result = &computed_value; + } + +**Tracking state across suspensions:** + +.. code:: c + + // Before suspending + zval state; + ZVAL_LONG(&state, OPERATION_IN_PROGRESS); + ZEND_ASYNC_INTERNAL_CONTEXT_SET(coroutine, state_key, &state); + + // After resuming + zval *state_val = ZEND_ASYNC_INTERNAL_CONTEXT_FIND(coroutine, state_key); + if (state_val && Z_LVAL_P(state_val) == OPERATION_IN_PROGRESS) { + // Continue operation... + } + +**Storing complex objects:** + +.. code:: c + + zval object_container; + ZVAL_OBJ(&object_container, my_object); + ZEND_ASYNC_INTERNAL_CONTEXT_SET(coroutine, object_key, &object_container); + // Object reference count is automatically incremented + +Best Practices +============== + +#. **Allocate keys in MINIT** - Ensures keys are available when needed +#. **Use descriptive key names** - Helps with debugging and maintenance +#. **Check return values** - ``FIND`` returns ``NULL`` if key doesn't exist +#. **Validate data types** - Always check ``Z_TYPE_P`` before accessing data +#. **Clean up appropriately** - Use ``UNSET`` for early cleanup if needed + +*************** + Coroutine API +*************** + +Core Functions +============== + +.. code:: c + + // Coroutine creation and management + zend_coroutine_t *ZEND_ASYNC_SPAWN(); + zend_coroutine_t *ZEND_ASYNC_NEW_COROUTINE(scope); + void ZEND_ASYNC_ENQUEUE_COROUTINE(coroutine); + + // Execution control + void ZEND_ASYNC_SUSPEND(); + void ZEND_ASYNC_RESUME(coroutine); + void ZEND_ASYNC_RESUME_WITH_ERROR(coroutine, error, transfer_error); + void ZEND_ASYNC_CANCEL(coroutine, error, transfer_error); + + // Context switch handlers + uint32_t ZEND_COROUTINE_ADD_SWITCH_HANDLER(coroutine, handler); + bool zend_coroutine_remove_switch_handler(coroutine, handler_index); + void ZEND_ASYNC_ADD_MAIN_COROUTINE_START_HANDLER(handler); + +State Checking Macros +===================== + +.. code:: c + + // Coroutine state + ZEND_COROUTINE_IS_STARTED(coroutine) + ZEND_COROUTINE_IS_FINISHED(coroutine) + ZEND_COROUTINE_IS_CANCELLED(coroutine) + ZEND_COROUTINE_SUSPENDED(coroutine) + ZEND_COROUTINE_IS_MAIN(coroutine) + + // Current context + ZEND_ASYNC_CURRENT_COROUTINE + ZEND_ASYNC_CURRENT_SCOPE + +Memory Management +================= + +Coroutines follow standard event reference counting: + +.. code:: c + + ZEND_ASYNC_EVENT_ADD_REF(&coroutine->event); // Increment reference + ZEND_ASYNC_EVENT_RELEASE(&coroutine->event); // Decrement and cleanup if needed + +The coroutine automatically cleans up: + +- Internal context data +- Switch handlers +- Event callbacks +- Scope references + +**************** + Best Practices +**************** + +#. **Always check if async is enabled** before using coroutine APIs +#. **Use appropriate scopes** to maintain context isolation +#. **Handle exceptions properly** in coroutine callbacks +#. **Register cleanup handlers** for long-running operations +#. **Use internal context** instead of global variables for coroutine data +#. **Remove one-time handlers** by returning ``false`` from switch handlers +#. **Validate coroutine state** before performing operations + +Example: Complete Coroutine Usage +================================= + +.. code:: c + + static uint32_t my_context_key = 0; + + PHP_MINIT_FUNCTION(my_extension) + { + my_context_key = ZEND_ASYNC_INTERNAL_CONTEXT_KEY_ALLOC("my_extension"); + ZEND_ASYNC_ADD_MAIN_COROUTINE_START_HANDLER(my_main_handler); + return SUCCESS; + } + + static bool my_main_handler(zend_coroutine_t *coroutine, bool is_enter, bool is_finishing) + { + if (!is_enter) return true; + + // Initialize coroutine-specific data + zval init_data; + ZVAL_STRING(&init_data, "initialized"); + ZEND_ASYNC_INTERNAL_CONTEXT_SET(coroutine, my_context_key, &init_data); + + return false; // Remove handler after initialization + } + + void my_async_operation(void) + { + if (!ZEND_ASYNC_IS_ENABLED()) { + return; // Fallback to synchronous operation + } + + zend_coroutine_t *coro = ZEND_ASYNC_SPAWN(); + coro->internal_entry = my_coroutine_function; + + ZEND_ASYNC_ENQUEUE_COROUTINE(coro); + ZEND_ASYNC_SUSPEND(); // Wait for completion + } + +This comprehensive coroutine system enables powerful asynchronous programming patterns while +maintaining clean separation of concerns and proper resource management. diff --git a/docs/source/true_async_api/events.rst b/docs/source/true_async_api/events.rst new file mode 100644 index 0000000000000..1af86924f76c2 --- /dev/null +++ b/docs/source/true_async_api/events.rst @@ -0,0 +1,301 @@ +######## + Events +######## + +The TrueAsync API revolves around the concept of **events**. An event represents any asynchronous +action that can produce a result or error in the future. All specific event types share a common +base structure ``zend_async_event_t`` which provides memory management, state flags and a standard +lifecycle. + +******************* + Creating an Event +******************* + +To create an event you typically call one of the ``ZEND_ASYNC_NEW_*`` macros which delegate to the +underlying implementation. Each macro returns a structure that contains ``zend_async_event_t`` as +its first member. + +.. code:: c + + zend_async_timer_event_t *timer = ZEND_ASYNC_NEW_TIMER_EVENT(1000, false); + timer->base.start(&timer->base); + +The ``base`` field exposes the common functions ``start``, ``stop`` and ``dispose`` as well as +reference counting helpers. + +************* + Event Flags +************* + +Each event has a ``flags`` field that controls its state and behaviour. The most important flags +are: + +``ZEND_ASYNC_EVENT_F_CLOSED`` + The event has been closed and may no longer be started or stopped. + +``ZEND_ASYNC_EVENT_F_RESULT_USED`` + Indicates that the result will be consumed by an awaiting context. When this flag is set the + awaiting code keeps the result alive until it is processed in the exception handler. + +``ZEND_ASYNC_EVENT_F_EXC_CAUGHT`` + Marks that an error produced by the event was caught in a callback and will not be rethrown + automatically. + +``ZEND_ASYNC_EVENT_F_ZVAL_RESULT`` + Signals that the callback result is a ``zval`` pointer. The TrueAsync core will properly + increment the reference count before inserting the value into userland arrays. + +``ZEND_ASYNC_EVENT_F_ZEND_OBJ`` + Specifies that the structure also acts as a Zend object implementing ``Awaitable``. + +``ZEND_ASYNC_EVENT_F_NO_FREE_MEMORY`` + The dispose handler must not free the memory of the event itself. This is used when the event is + embedded into another structure. + +``ZEND_ASYNC_EVENT_F_EXCEPTION_HANDLED`` + Set by the callback once it has fully processed an exception. If this flag is not set the + exception will be rethrown after callbacks finish. + +``ZEND_ASYNC_EVENT_F_REFERENCE`` + Indicates that this structure is only an event reference which stores a pointer to the real + event. + +``ZEND_ASYNC_EVENT_F_OBJ_REF`` + Marks that the event keeps a pointer to the Zend object in its ``extra_offset`` region so + reference counting works through the object. + +Convenience macros such as ``ZEND_ASYNC_EVENT_SET_CLOSED`` and +``ZEND_ASYNC_EVENT_IS_EXCEPTION_HANDLED`` are provided to manipulate these flags. + +***************** + Event Callbacks +***************** + +The core maintains a dynamic vector of callbacks for each event. Implementations provide the +``add_callback`` and ``del_callback`` methods which internally use ``zend_async_callbacks_push`` and +``zend_async_callbacks_remove``. The ``zend_async_callbacks_notify`` helper iterates over all +registered callbacks and passes the result or exception. If a ``notify_handler`` is set on the event +it is invoked first and can adjust the result or exception as needed. The handler is expected to +call ``ZEND_ASYNC_CALLBACKS_NOTIFY_FROM_HANDLER`` to forward the values to all listeners. The +``php-async`` extension provides convenience macros ``ZEND_ASYNC_CALLBACKS_NOTIFY`` for regular use, +``ZEND_ASYNC_CALLBACKS_NOTIFY_FROM_HANDLER`` when called from inside a ``notify_handler``, and +``ZEND_ASYNC_CALLBACKS_NOTIFY_AND_CLOSE`` to close the event before notifying listeners. + +The following example shows a libuv poll event that dispatches its callbacks once the underlying +handle becomes readable: + +.. code:: c + + static void on_poll_event(uv_poll_t *handle, int status, int events) { + async_poll_event_t *poll = handle->data; + zend_object *exception = NULL; + + if (status < 0) { + exception = async_new_exception( + async_ce_input_output_exception, + "Input output error: %s", + uv_strerror(status) + ); + } + + poll->event.triggered_events = events; + + ZEND_ASYNC_CALLBACKS_NOTIFY(&poll->event.base, NULL, exception); + + if (exception != NULL) { + zend_object_release(exception); + } + } + +****************** + Event Coroutines +****************** + +Coroutines themselves are implemented as events using the ``zend_coroutine_t`` structure. When a +coroutine yields, its waker waits on multiple events and resumes the coroutine once any of them +triggers. + +.. code:: c + + // Spawn a coroutine that waits for a timer + zend_coroutine_t *co = ZEND_ASYNC_SPAWN(NULL); + zend_async_resume_when(co, &timer->base, false, zend_async_waker_callback_resolve, NULL); + zend_async_enqueue_coroutine(co); + +When the coroutine finishes execution the event triggers again to deliver the result or exception. +The coroutine implementation marks the callback result as a ``zval`` value using +``ZEND_ASYNC_EVENT_SET_ZVAL_RESULT``. Callback handlers may also set +``ZEND_ASYNC_EVENT_SET_EXCEPTION_HANDLED`` to indicate that the thrown exception has been processed +and should not be rethrown by the runtime. + +****************** + Extending Events +****************** + +Custom event types embed ``zend_async_event_t`` at the beginning of their structure and may allocate +additional memory beyond the end of the struct. The ``extra_size`` argument in +``ZEND_ASYNC_NEW_*_EX`` controls how much extra space is reserved, and ``extra_offset`` records +where that region begins. + +.. code:: c + + // Allocate extra space for user data + zend_async_poll_event_t *poll = ZEND_ASYNC_NEW_POLL_EVENT_EX(fd, false, sizeof(my_data_t)); + my_data_t *data = (my_data_t *)((char*)poll + poll->base.extra_offset); + +The libuv backend defines event wrappers that embed libuv handles. A timer event, for example, +extends ``zend_async_timer_event_t`` as follows: + +.. code:: c + + typedef struct { + zend_async_timer_event_t event; + uv_timer_t uv_handle; + } async_timer_event_t; + + // Initialize callbacks for the event + event->event.base.add_callback = libuv_add_callback; + event->event.base.del_callback = libuv_remove_callback; + event->event.base.start = libuv_timer_start; + event->event.base.stop = libuv_timer_stop; + event->event.base.dispose = libuv_timer_dispose; + +Every extended event defines its own ``start``, ``stop`` and ``dispose`` functions. The dispose +handler must release all resources associated with the event and is called when the reference count +reaches ``1``. It is common to stop the event first and then close the underlying libuv handle so +that memory gets freed in the ``uv_close`` callback. + +.. code:: c + + static void libuv_timer_dispose(zend_async_event_t *event) + { + if (ZEND_ASYNC_EVENT_REF(event) > 1) { + ZEND_ASYNC_EVENT_DEL_REF(event); + return; + } + + if (event->loop_ref_count > 0) { + event->loop_ref_count = 1; + event->stop(event); + } + + zend_async_callbacks_free(event); + + async_timer_event_t *timer = (async_timer_event_t *)event; + uv_close((uv_handle_t *)&timer->uv_handle, libuv_close_handle_cb); + } + +If ``ZEND_ASYNC_EVENT_F_NO_FREE_MEMORY`` is set the dispose handler must not free the event memory +itself because the structure is embedded in another object (e.g. ``async_coroutine_t``). The libuv +close callback will only free the libuv handle in this case. + +************************ + Custom Event Callbacks +************************ + +Callbacks can also be extended to store additional state. The await logic in ``php-async`` defines a +callback that inherits from ``zend_coroutine_event_callback_t`` and keeps a reference to the +awaiting context: + +.. code:: c + + typedef struct { + zend_coroutine_event_callback_t callback; + async_await_context_t *await_context; + zval key; + zend_async_event_callback_dispose_fn prev_dispose; + } async_await_callback_t; + + async_await_callback_t *cb = ecalloc(1, sizeof(async_await_callback_t)); + cb->callback.base.callback = async_waiting_callback; + cb->await_context = ctx; + zend_async_resume_when(co, awaitable, false, NULL, &cb->callback); + +************************ + Events as Zend Objects +************************ + +If ``ZEND_ASYNC_EVENT_F_ZEND_OBJ`` is set, the event also acts as a Zend object implementing +``Awaitable``. The ``zend_object_offset`` field stores the location of the ``zend_object`` within +the structure. Reference counting macros automatically use either the internal counter or +``GC_REFCOUNT`` depending on this flag. + +This allows events to be exposed to userland seamlessly while keeping the internal lifecycle +consistent. + +For events that are destroyed asynchronously (e.g. libuv timers) the actual event structure cannot +be a Zend object. Instead a lightweight reference structure is used. ``ZEND_ASYNC_EVENT_REF_PROLOG`` +reserves the required fields in the Zend object and ``ZEND_ASYNC_EVENT_REF_SET`` stores the pointer +to the real event together with the ``zend_object`` offset. The event must then be flagged with +``ZEND_ASYNC_EVENT_WITH_OBJECT_REF`` so that reference counting delegates to the object. + +When accessing the event from userland objects use ``ZEND_ASYNC_OBJECT_TO_EVENT`` and +``ZEND_ASYNC_EVENT_TO_OBJECT`` which handle both direct and reference-based layouts transparently. + +The ``php-async`` extension provides ``Async\\Timeout`` objects that embed a timer event. Recent +updates introduce a helper API for linking an event with a Zend object. A helper macro +``ZEND_ASYNC_EVENT_REF_PROLOG`` reserves fields at the beginning of the object to hold an event +reference. ``ZEND_ASYNC_EVENT_REF_SET`` stores the pointer to the newly created event and the offset +of the ``zend_object`` inside the structure. ``ZEND_ASYNC_EVENT_WITH_OBJECT_REF`` then marks the +event so reference counting will use the Zend object rather than the internal counter. + +The object factory now uses these helpers when creating the timer: + +.. code:: + + static zend_object *async_timeout_create(const zend_ulong ms, const bool is_periodic) + { + async_timeout_object_t *object = + ecalloc(1, sizeof(async_timeout_object_t) + + zend_object_properties_size(async_ce_timeout)); + + zend_async_event_t *event = (zend_async_event_t *) ZEND_ASYNC_NEW_TIMER_EVENT_EX( + ms, is_periodic, sizeof(async_timeout_ext_t) + ); + + if (UNEXPECTED(event == NULL)) { + efree(object); + return NULL; + } + + ZEND_ASYNC_EVENT_REF_SET(object, XtOffsetOf(async_timeout_object_t, std), (zend_async_timer_event_t *) event); + ZEND_ASYNC_EVENT_WITH_OBJECT_REF(event); + + async_timeout_ext_t *timeout = ASYNC_TIMEOUT_FROM_EVENT(event); + timeout->std = &object->std; + timeout->prev_dispose = event->dispose; + event->notify_handler = timeout_before_notify_handler; + event->dispose = async_timeout_event_dispose; + + zend_object_std_init(&object->std, async_ce_timeout); + object_properties_init(&object->std, async_ce_timeout); + object->std.handlers = &async_timeout_handlers; + + return &object->std; + } + + static void timeout_before_notify_handler(zend_async_event_t *event, + void *result, + zend_object *exception) + { + if (UNEXPECTED(exception != NULL)) { + ZEND_ASYNC_CALLBACKS_NOTIFY_FROM_HANDLER(event, result, exception); + return; + } + + zend_object *timeout_exception = async_new_exception( + async_ce_timeout_exception, + "Timeout occurred after %lu milliseconds", + ((zend_async_timer_event_t *) event)->timeout + ); + + ZEND_ASYNC_CALLBACKS_NOTIFY_FROM_HANDLER(event, result, timeout_exception); + zend_object_release(timeout_exception); + } + +.. note:: + + Events must not be exposed as Zend objects if their memory is released asynchronously. Zend + assumes that object destruction happens entirely during the ``zend_object_release`` call and + cannot wait for callbacks such as ``uv_close`` to free the underlying event. The + ``Async\\Timeout`` class will be redesigned to avoid this pattern. diff --git a/docs/source/true_async_api/examples.rst b/docs/source/true_async_api/examples.rst new file mode 100644 index 0000000000000..aae751a5725bd --- /dev/null +++ b/docs/source/true_async_api/examples.rst @@ -0,0 +1,452 @@ +########## + Examples +########## + +This section provides practical C code examples for using the TrueAsync API in PHP extensions. + +**************** + Basic Examples +**************** + +Checking API Availability +========================= + +Always check if async is enabled before using the API: + +.. code:: c + + #include "Zend/zend_async_API.h" + + void my_async_function(void) + { + if (!ZEND_ASYNC_IS_ENABLED()) { + // Fallback to synchronous operation + my_sync_function(); + return; + } + + // Use async API + my_actual_async_function(); + } + +Simple Coroutine Creation +========================= + +Creating and spawning a basic coroutine: + +.. code:: c + + static void my_coroutine_entry(void *arg) + { + php_printf("Hello from coroutine!\n"); + } + + void spawn_example(void) + { + if (!ZEND_ASYNC_IS_ENABLED()) return; + + zend_coroutine_t *coro = ZEND_ASYNC_SPAWN(); + if (!coro) { + php_error(E_WARNING, "Failed to create coroutine"); + return; + } + + coro->internal_entry = my_coroutine_entry; + coro->internal_arg = NULL; + + ZEND_ASYNC_ENQUEUE_COROUTINE(coro); + } + +Using Scopes +============ + +Creating coroutines with custom scopes: + +.. code:: c + + void scope_example(void) + { + if (!ZEND_ASYNC_IS_ENABLED()) return; + + // Create new scope + zend_async_scope_t *scope = ZEND_ASYNC_NEW_SCOPE(NULL); + if (!scope) return; + + // Spawn coroutine within scope + zend_coroutine_t *coro = ZEND_ASYNC_SPAWN_WITH(scope); + if (coro) { + coro->internal_entry = my_coroutine_entry; + ZEND_ASYNC_ENQUEUE_COROUTINE(coro); + } + } + +********************************* + Context Switch Handler Examples +********************************* + +Per-Coroutine Handler +===================== + +Adding context switch handlers to specific coroutines: + +.. code:: c + + static bool my_switch_handler(zend_coroutine_t *coroutine, bool is_enter, bool is_finishing) + { + if (is_finishing) { + php_printf("Coroutine finishing\n"); + return false; // Remove handler + } + + if (is_enter) { + php_printf("Entering coroutine\n"); + } else { + php_printf("Leaving coroutine\n"); + } + + return true; // Keep handler + } + + void add_handler_example(void) + { + zend_coroutine_t *coro = ZEND_ASYNC_SPAWN(); + if (!coro) return; + + uint32_t handler_id = zend_coroutine_add_switch_handler(coro, my_switch_handler); + + coro->internal_entry = my_coroutine_entry; + ZEND_ASYNC_ENQUEUE_COROUTINE(coro); + + // Later: remove handler if needed + // zend_coroutine_remove_switch_handler(coro, handler_id); + } + +Global Main Coroutine Handler +============================= + +Handler that applies to all main coroutines: + +.. code:: c + + static uint32_t my_context_key = 0; + + static bool main_coroutine_handler(zend_coroutine_t *coroutine, bool is_enter, bool is_finishing) + { + if (!is_enter) return true; + + // Initialize extension data for this coroutine + my_extension_data_t *data = ecalloc(1, sizeof(my_extension_data_t)); + data->initialized = true; + + // Store in coroutine internal context + zval data_zval; + ZVAL_PTR(&data_zval, data); + ZEND_ASYNC_INTERNAL_CONTEXT_SET(coroutine, my_context_key, &data_zval); + + return false; // Remove handler after initialization + } + + PHP_MINIT_FUNCTION(my_extension) + { + my_context_key = ZEND_ASYNC_INTERNAL_CONTEXT_KEY_ALLOC("my_extension"); + zend_async_add_main_coroutine_start_handler(main_coroutine_handler); + return SUCCESS; + } + +****************** + Event Operations +****************** + +Basic Event Usage +================= + +Working with async events (using timer as example): + +.. code:: c + + static void timer_callback(zend_async_event_t *event, zend_async_event_callback_t *callback, + void *result, zend_object *exception) + { + if (exception) { + php_printf("Timer error occurred\n"); + } else { + php_printf("Timer fired!\n"); + } + + // Clean up + ZEND_ASYNC_EVENT_DEL_REF(event); + } + + void timer_example(void) + { + if (!ZEND_ASYNC_REACTOR_IS_ENABLED()) return; + + // This would need actual reactor implementation + // zend_async_timer_event_t *timer = zend_async_new_timer_event_fn(1000, 0, false, 0); + // if (!timer) return; + + // zend_async_event_callback_t *callback = ...; + // timer->base.add_callback(&timer->base, callback); + // timer->base.start(&timer->base); + } + +*************************** + Internal Context Examples +*************************** + +Storing Extension Data +====================== + +Using internal context to store per-coroutine data: + +.. code:: c + + static uint32_t cache_context_key = 0; + + typedef struct { + HashTable *cached_results; + time_t last_access; + } cache_data_t; + + PHP_MINIT_FUNCTION(cache_extension) + { + cache_context_key = ZEND_ASYNC_INTERNAL_CONTEXT_KEY_ALLOC("cache_data"); + return SUCCESS; + } + + cache_data_t* get_cache_data(zend_coroutine_t *coroutine) + { + zval *cached = ZEND_ASYNC_INTERNAL_CONTEXT_FIND(coroutine, cache_context_key); + if (cached && Z_TYPE_P(cached) == IS_PTR) { + return (cache_data_t *)Z_PTR_P(cached); + } + + // Create new cache data + cache_data_t *data = ecalloc(1, sizeof(cache_data_t)); + ALLOC_HASHTABLE(data->cached_results); + zend_hash_init(data->cached_results, 16, NULL, ZVAL_PTR_DTOR, 0); + data->last_access = time(NULL); + + // Store in context + zval data_zval; + ZVAL_PTR(&data_zval, data); + ZEND_ASYNC_INTERNAL_CONTEXT_SET(coroutine, cache_context_key, &data_zval); + + return data; + } + +************************* + Error Handling Examples +************************* + +Exception Creation +================== + +Creating and handling exceptions in async context: + +.. code:: c + + void async_operation_with_error(void) + { + if (!ZEND_ASYNC_IS_ENABLED()) return; + + zend_coroutine_t *coro = ZEND_ASYNC_SPAWN(); + if (!coro) { + zend_throw_error(NULL, "Failed to create coroutine"); + return; + } + + // Set up coroutine that might throw + coro->internal_entry = error_prone_operation; + ZEND_ASYNC_ENQUEUE_COROUTINE(coro); + } + + static void error_prone_operation(void *arg) + { + // Simulate error condition + if (some_error_condition()) { + zend_object *exception = zend_throw_exception( + zend_ce_exception, "Async operation failed", 0 + ); + + // Resume current coroutine with error + zend_coroutine_t *current = ZEND_ASYNC_CURRENT_COROUTINE; + if (current) { + ZEND_ASYNC_RESUME_WITH_ERROR(current, exception, true); + } + return; + } + + php_printf("Operation completed successfully\n"); + } + +**************************** + Memory Management Examples +**************************** + +Reference Counting Examples +=========================== + +Proper event reference management: + +.. code:: c + + void event_reference_example(void) + { + // Assuming we have an event from somewhere + zend_async_event_t *event = get_some_event(); + + // Add reference when storing event + ZEND_ASYNC_EVENT_ADD_REF(event); + + // Store event somewhere + store_event_reference(event); + + // Later: remove reference when done + ZEND_ASYNC_EVENT_DEL_REF(event); + + // Or use RELEASE which decrements and disposes if needed + // ZEND_ASYNC_EVENT_RELEASE(event); + } + +******************************* + Futures and Channels Examples +******************************* + +Future Creation +=============== + +Creating and using futures for async results: + +.. code:: c + + void future_example(void) + { + if (!ZEND_ASYNC_IS_ENABLED()) return; + + // Create thread-safe future + zend_future_t *future = ZEND_ASYNC_NEW_FUTURE(true); + if (!future) return; + + // Future can be resolved from any thread + // zend_future_resolve(future, &result_zval); + + // Or with exception + // zend_future_reject(future, exception); + } + +Channel Communication +===================== + +Creating channels for coroutine communication: + +.. code:: c + + void channel_example(void) + { + if (!ZEND_ASYNC_IS_ENABLED()) return; + + // Create buffered channel + zend_async_channel_t *channel = ZEND_ASYNC_NEW_CHANNEL(10, true, false); + if (!channel) return; + + // Channels support send/receive operations + // channel->send(channel, &value); + // channel->receive(channel, &result); + } + +*********************** + Extension Integration +*********************** + +Complete Extension Example +========================== + +Full example of async-aware extension: + +.. code:: c + + static uint32_t my_ext_context_key = 0; + + typedef struct { + bool async_enabled; + HashTable *pending_operations; + } my_extension_context_t; + + static bool my_ext_main_handler(zend_coroutine_t *coroutine, bool is_enter, bool is_finishing) + { + if (is_finishing) { + // Cleanup on coroutine completion + zval *ctx_zval = ZEND_ASYNC_INTERNAL_CONTEXT_FIND(coroutine, my_ext_context_key); + if (ctx_zval && Z_TYPE_P(ctx_zval) == IS_PTR) { + my_extension_context_t *ctx = (my_extension_context_t *)Z_PTR_P(ctx_zval); + if (ctx->pending_operations) { + zend_hash_destroy(ctx->pending_operations); + FREE_HASHTABLE(ctx->pending_operations); + } + efree(ctx); + } + return false; + } + + if (!is_enter) return true; + + // Initialize context for new main coroutine + my_extension_context_t *ctx = ecalloc(1, sizeof(my_extension_context_t)); + ctx->async_enabled = true; + + ALLOC_HASHTABLE(ctx->pending_operations); + zend_hash_init(ctx->pending_operations, 8, NULL, ZVAL_PTR_DTOR, 0); + + zval ctx_zval; + ZVAL_PTR(&ctx_zval, ctx); + ZEND_ASYNC_INTERNAL_CONTEXT_SET(coroutine, my_ext_context_key, &ctx_zval); + + return false; // Remove handler after init + } + + PHP_MINIT_FUNCTION(my_extension) + { + my_ext_context_key = ZEND_ASYNC_INTERNAL_CONTEXT_KEY_ALLOC("my_extension"); + + if (ZEND_ASYNC_IS_ENABLED()) { + zend_async_add_main_coroutine_start_handler(my_ext_main_handler); + } + + return SUCCESS; + } + + // Extension function that works async-aware + PHP_FUNCTION(my_async_operation) + { + if (!ZEND_ASYNC_IS_ENABLED()) { + // Fallback to sync operation + my_sync_operation(INTERNAL_FUNCTION_PARAM_PASSTHRU); + return; + } + + zend_coroutine_t *current = ZEND_ASYNC_CURRENT_COROUTINE; + if (current) { + my_extension_context_t *ctx = get_my_context(current); + if (ctx && ctx->async_enabled) { + // Perform async operation + perform_async_operation(current); + return; + } + } + + // Default to sync + my_sync_operation(INTERNAL_FUNCTION_PARAM_PASSTHRU); + } + +************************* + Examples Best Practices +************************* + +#. **Always check enablement** before using async APIs +#. **Use internal context** for per-coroutine data storage +#. **Handle reference counting** properly for events +#. **Register cleanup handlers** for resource management +#. **Provide sync fallbacks** when async is not available +#. **Use appropriate scopes** for coroutine isolation +#. **Handle exceptions** properly in async context diff --git a/docs/source/true_async_api/implementation-guide.rst b/docs/source/true_async_api/implementation-guide.rst new file mode 100644 index 0000000000000..4a049e1cea3b3 --- /dev/null +++ b/docs/source/true_async_api/implementation-guide.rst @@ -0,0 +1,396 @@ +###################### + Implementation Guide +###################### + +This guide explains how to implement TrueAsync API backends and integrate them with the PHP core. It +covers the registration process, implementation patterns, and best practices. + +********** + Overview +********** + +Implementing a TrueAsync API backend involves: + +#. **Creating function implementations** that match the API signatures +#. **Registering implementations** with the core during module initialization +#. **Following memory management** and error handling conventions +#. **Implementing event lifecycle** methods (start, stop, dispose) +#. **Handling callbacks** and result delivery + +********************** + Registration Process +********************** + +The registration process connects your implementations to the core API. + +Module Initialization +===================== + +Register your implementations during ``PHP_MINIT_FUNCTION``: + +.. code:: c + + PHP_MINIT_FUNCTION(my_async_backend) { + // Register reactor implementations + bool success = zend_async_reactor_register( + "MyAsyncBackend", // Module name + false, // Allow override + my_reactor_startup, // Startup function + my_reactor_shutdown, // Shutdown function + my_reactor_execute, // Execute function + my_reactor_loop_alive, // Loop alive check + my_new_socket_event, // Socket event factory + my_new_poll_event, // Poll event factory + my_new_timer_event, // Timer event factory + my_new_signal_event, // Signal event factory + my_new_process_event, // Process event factory + my_new_thread_event, // Thread event factory + my_new_filesystem_event, // Filesystem event factory + my_getnameinfo, // DNS nameinfo + my_getaddrinfo, // DNS addrinfo + my_freeaddrinfo, // DNS cleanup + my_new_exec_event, // Exec event factory + my_exec // Exec function + ); + + if (!success) { + php_error_docref(NULL, E_CORE_ERROR, "Failed to register async backend"); + return FAILURE; + } + + return SUCCESS; + } + +Function Pointer Types +====================== + +All implementations must match the exact function pointer signatures defined in +``zend_async_API.h``: + +.. code:: c + + // DNS function signatures + typedef zend_async_dns_addrinfo_t* (*zend_async_getaddrinfo_t)( + const char *node, const char *service, + const struct addrinfo *hints, size_t extra_size + ); + + typedef void (*zend_async_freeaddrinfo_t)(struct addrinfo *ai); + + // Timer function signature + typedef zend_async_timer_event_t* (*zend_async_new_timer_event_t)( + const zend_ulong timeout, const zend_ulong nanoseconds, + const bool is_periodic, size_t extra_size + ); + +************************* + Implementation Patterns +************************* + +Event Structure Pattern +======================= + +All async events follow a common pattern with base event structure plus implementation-specific +data: + +.. code:: c + + // Your implementation structure + typedef struct _my_dns_addrinfo_s { + zend_async_dns_addrinfo_t event; // Base event (MUST be first) + my_event_loop_handle_t handle; // Your event loop handle + // Additional implementation-specific fields... + } my_dns_addrinfo_t; + +Factory Function Pattern +======================== + +Event factory functions create and initialize event structures: + +.. code:: c + + static zend_async_dns_addrinfo_t* my_getaddrinfo( + const char *node, const char *service, + const struct addrinfo *hints, size_t extra_size + ) { + // Allocate memory (include extra_size if specified) + size_t total_size = sizeof(my_dns_addrinfo_t) + extra_size; + my_dns_addrinfo_t *dns = pecalloc(1, total_size, 0); + + // Initialize base event fields + dns->event.base.extra_offset = sizeof(my_dns_addrinfo_t); + dns->event.base.ref_count = 1; + dns->event.base.add_callback = my_add_callback; + dns->event.base.del_callback = my_remove_callback; + dns->event.base.start = my_dns_start; + dns->event.base.stop = my_dns_stop; + dns->event.base.dispose = my_dns_dispose; + + // Store request parameters + dns->event.node = node; + dns->event.service = service; + dns->event.hints = hints; + + // Initialize your backend-specific handle + int result = my_event_loop_init_dns(&dns->handle, node, service, hints); + if (result != 0) { + my_throw_error("Failed to initialize DNS request: %s", my_strerror(result)); + pefree(dns, 0); + return NULL; + } + + // Link handle to event data + dns->handle.data = dns; + + return &dns->event; + } + +Lifecycle Methods +================= + +Implement the three lifecycle methods for each event type: + +.. code:: c + + // Start the async operation + static void my_dns_start(zend_async_event_t *event) { + EVENT_START_PROLOGUE(event); // Standard prologue + + my_dns_addrinfo_t *dns = (my_dns_addrinfo_t *)event; + + int result = my_event_loop_start_dns(&dns->handle, my_dns_callback); + if (result != 0) { + my_throw_error("Failed to start DNS operation: %s", my_strerror(result)); + return; + } + + event->loop_ref_count++; + ZEND_ASYNC_INCREASE_EVENT_COUNT; + } + + // Stop the async operation + static void my_dns_stop(zend_async_event_t *event) { + EVENT_STOP_PROLOGUE(event); // Standard prologue + + my_dns_addrinfo_t *dns = (my_dns_addrinfo_t *)event; + + my_event_loop_stop_dns(&dns->handle); + + event->loop_ref_count = 0; + ZEND_ASYNC_DECREASE_EVENT_COUNT; + } + + // Clean up and dispose + static void my_dns_dispose(zend_async_event_t *event) { + if (ZEND_ASYNC_EVENT_REF(event) > 1) { + ZEND_ASYNC_EVENT_DEL_REF(event); + return; + } + + // Stop if still running + if (event->loop_ref_count > 0) { + event->loop_ref_count = 1; + event->stop(event); + } + + // Free callbacks + zend_async_callbacks_free(event); + + my_dns_addrinfo_t *dns = (my_dns_addrinfo_t *)event; + + // Clean up backend-specific resources + my_event_loop_cleanup_dns(&dns->handle); + + // Free the structure + pefree(dns, 0); + } + +******************* + Callback Handling +******************* + +Event Completion Callbacks +========================== + +When your backend completes an operation, notify the core through callbacks: + +.. code:: c + + // Your backend callback (called by event loop) + static void my_dns_callback(my_event_loop_handle_t *handle, int status, struct addrinfo *result) { + my_dns_addrinfo_t *dns = (my_dns_addrinfo_t *)handle->data; + zend_object *exception = NULL; + + // Close the event after one-time operations + ZEND_ASYNC_EVENT_SET_CLOSED(&dns->event.base); + dns->event.base.stop(&dns->event.base); + + // Handle errors + if (status != 0) { + exception = my_new_exception( + "DNS resolution failed: %s", my_strerror(status) + ); + } + + // Notify all registered callbacks + zend_async_callbacks_notify(&dns->event.base, result, exception); + + // Release exception if created + if (exception) { + zend_object_release(exception); + } + } + +Result Memory Management +======================== + +For operations that return allocated memory (like ``getaddrinfo``): + +.. code:: c + + static void my_freeaddrinfo(struct addrinfo *ai) { + if (ai != NULL) { + my_event_loop_free_addrinfo(ai); // Use your backend's free function + } + } + +**Make sure your ``getaddrinfo`` results can be freed by your ``freeaddrinfo`` implementation.** + +**************** + Error Handling +**************** + +Error Reporting +=============== + +Use the standard PHP error reporting mechanisms: + +.. code:: c + + // For recoverable errors during operation + static void my_throw_error(const char *format, ...) { + va_list args; + va_start(args, format); + zend_string *message = zend_vstrpprintf(0, format, args); + va_end(args); + + zend_throw_error(NULL, ZSTR_VAL(message)); + zend_string_release(message); + } + + // For creating exception objects + static zend_object* my_new_exception(const char *format, ...) { + va_list args; + va_start(args, format); + zend_string *message = zend_vstrpprintf(0, format, args); + va_end(args); + + zend_object *ex = zend_throw_exception(zend_ce_exception, ZSTR_VAL(message), 0); + zend_string_release(message); + return ex; + } + +Exception Classes +================= + +Use appropriate exception classes for different error types: + +.. code:: c + + // DNS errors + zend_object *dns_ex = async_new_exception(async_ce_dns_exception, "DNS error: %s", error_msg); + + // I/O errors + zend_object *io_ex = async_new_exception(async_ce_input_output_exception, "I/O error: %s", error_msg); + + // Timeout errors + zend_object *timeout_ex = async_new_exception(async_ce_timeout_exception, "Operation timed out"); + +******************* + Memory Management +******************* + +Reference Counting +================== + +The TrueAsync API uses reference counting for event objects: + +.. code:: c + + // Add reference (when storing event somewhere) + ZEND_ASYNC_EVENT_ADD_REF(event); + + // Remove reference (when done with event) + ZEND_ASYNC_EVENT_DEL_REF(event); + + // Release event (decrements and disposes if needed) + ZEND_ASYNC_EVENT_RELEASE(event); + +Memory Allocation +================= + +Use persistent allocation for long-lived structures: + +.. code:: c + + // For event structures that may outlive a request + my_event_t *event = pecalloc(1, sizeof(my_event_t), 0); // Persistent + + // For temporary data + char *buffer = emalloc(size); // Request-scoped + +Extra Data Support +================== + +Support the ``extra_size`` parameter for extensions: + +.. code:: c + + static zend_async_timer_event_t* my_new_timer_event( + const zend_ulong timeout, const zend_ulong nanoseconds, + const bool is_periodic, size_t extra_size + ) { + // Allocate base size + extra space + size_t total_size = sizeof(my_timer_event_t) + extra_size; + my_timer_event_t *timer = pecalloc(1, total_size, 0); + + // Set extra offset so extensions can find their data + timer->event.base.extra_offset = sizeof(my_timer_event_t); + + // ... rest of initialization + } + +**************** + Best Practices +**************** + +Performance Considerations +========================== + +- **Minimize allocations** in hot paths (event creation/callback handling) +- **Use object pools** for frequently created/destroyed events +- **Batch operations** when possible in your event loop +- **Profile memory usage** to detect leaks early + +Thread Safety +============= + +- **Document thread safety** guarantees of your implementation +- **Use proper locking** if supporting multi-threaded access +- **Consider thread-local storage** for per-thread state + +Testing +======= + +- **Create unit tests** for each API function implementation +- **Test error conditions** and memory cleanup +- **Verify callback behavior** under various scenarios +- **Load test** with many concurrent operations + +Documentation +============= + +- **Document implementation-specific behavior** and limitations +- **Provide configuration options** for tuning performance +- **Include examples** showing integration with your event loop +- **Document version compatibility** with different backend versions diff --git a/docs/source/true_async_api/index.rst b/docs/source/true_async_api/index.rst new file mode 100644 index 0000000000000..e9c33c023474a --- /dev/null +++ b/docs/source/true_async_api/index.rst @@ -0,0 +1,108 @@ +############### + TrueAsync API +############### + +.. toctree:: + :hidden: + + architecture + events + coroutines + api-reference + implementation-guide + patterns + examples + +The TrueAsync API provides a comprehensive asynchronous programming interface for PHP extensions, +built on top of the Zend Engine. This API enables non-blocking operations for I/O, timers, signals, +and other asynchronous tasks while maintaining clean separation between the API definition and its +implementation. + +.. warning:: + + TrueAsync API is currently in development. The API may change in future versions. + +The TrueAsync API follows a strict architectural pattern where the PHP core (php-src) defines the +API interfaces, while extensions provide concrete implementations. This design ensures modularity, +testability, and allows for multiple backend implementations (libuv, epoll, etc.). + +************** + Key Features +************** + +- **Non-blocking I/O operations** - File, network, and socket operations +- **Timer and scheduling support** - Timeout handling and periodic tasks +- **Signal handling** - Asynchronous signal processing +- **DNS resolution** - Async getaddrinfo/getnameinfo with proper memory management +- **Process management** - Spawn and monitor external processes +- **Filesystem monitoring** - Watch for file and directory changes +- **Clean separation** - API definition vs implementation +- **Memory safety** - Proper reference counting and cleanup + +************** + Architecture +************** + +The TrueAsync API consists of two main components: + +**API Definition Layer** (``Zend/zend_async_API.h``) + Defines function pointer types, structures, and macros that form the public API. Located in the + php-src core and used by all consumers. + +**Implementation Layer** (``ext/async/``) + Provides concrete implementations using libuv or other event loop libraries. Registers + implementations with the core through registration functions. + +This separation allows php-src core to use async functionality without directly depending on any +specific implementation, enabling clean modular design and testing. + +***************** + Getting Started +***************** + +To use the TrueAsync API in your extension: + +#. Include the API header: ``#include "Zend/zend_async_API.h"`` +#. Check if async is enabled: ``ZEND_ASYNC_IS_ENABLED()`` +#. Use API macros like ``ZEND_ASYNC_GETADDRINFO()`` for operations +#. Handle results through callback mechanisms +#. Clean up resources with appropriate free functions + +For implementation details, see the :doc:`implementation-guide` section. + +*************** + Quick Example +*************** + +.. code:: c + + // Async DNS resolution + zend_async_dns_addrinfo_t *event = ZEND_ASYNC_GETADDRINFO("example.com", "80", &hints); + + // Register callback for completion + zend_async_resume_when(coroutine, &event->base, true, my_callback, NULL); + + // Later, in callback: clean up + ZEND_ASYNC_FREEADDRINFO(result); + +************************ + Documentation Sections +************************ + +:doc:`architecture` + Detailed explanation of the two-repository architecture and component separation. + +:doc:`coroutines` + Complete guide to coroutines, context switching, and internal context management. + +:doc:`api-reference` + Complete API reference with function signatures, macros, and data structures. + +:doc:`implementation-guide` + Guide for implementing TrueAsync API backends and registering with the core. + +:doc:`patterns` + Common patterns, best practices, and coding conventions for the API. + +:doc:`examples` + Practical C code examples showing how to use different API components. diff --git a/docs/source/true_async_api/patterns.rst b/docs/source/true_async_api/patterns.rst new file mode 100644 index 0000000000000..a064be37e7271 --- /dev/null +++ b/docs/source/true_async_api/patterns.rst @@ -0,0 +1,568 @@ +########## + Patterns +########## + +This section covers common patterns and best practices for implementing TrueAsync API in PHP +extensions. + +********************** + Fundamental Patterns +********************** + +Async-Aware Function Pattern +============================ + +The standard pattern for making extension functions async-aware: + +.. code:: c + + PHP_FUNCTION(my_extension_function) + { + // 1. Check if async is enabled + if (!ZEND_ASYNC_IS_ENABLED()) { + // Fallback to synchronous implementation + my_sync_implementation(INTERNAL_FUNCTION_PARAM_PASSTHRU); + return; + } + + // 2. Check if we're in coroutine context + zend_coroutine_t *current = ZEND_ASYNC_CURRENT_COROUTINE; + if (!current) { + // Not in coroutine, use sync + my_sync_implementation(INTERNAL_FUNCTION_PARAM_PASSTHRU); + return; + } + + // 3. Perform async operation + my_async_implementation(current, INTERNAL_FUNCTION_PARAM_PASSTHRU); + } + +API Enablement Check Pattern +============================ + +Always check API availability before using async features: + +.. code:: c + + void my_extension_init(void) + { + if (ZEND_ASYNC_IS_ENABLED()) { + // Initialize async-specific resources + init_async_resources(); + + if (ZEND_ASYNC_REACTOR_IS_ENABLED()) { + // Reactor is available, can use events + init_event_handlers(); + } + } + } + +***************************** + Context Management Patterns +***************************** + +Extension Context Initialization +================================ + +Pattern for setting up per-coroutine extension data: + +.. code:: c + + static uint32_t my_context_key = 0; + + typedef struct { + HashTable *cache; + zend_async_timer_event_t *cleanup_timer; + bool initialized; + } my_context_t; + + static bool my_context_initializer(zend_coroutine_t *coroutine, bool is_enter, bool is_finishing) + { + if (is_finishing) { + // Cleanup context + zval *ctx_zval = ZEND_ASYNC_INTERNAL_CONTEXT_FIND(coroutine, my_context_key); + if (ctx_zval && Z_TYPE_P(ctx_zval) == IS_PTR) { + my_context_t *ctx = (my_context_t *)Z_PTR_P(ctx_zval); + cleanup_my_context(ctx); + } + return false; // Remove handler + } + + if (!is_enter) return true; + + // Initialize context + my_context_t *ctx = ecalloc(1, sizeof(my_context_t)); + ALLOC_HASHTABLE(ctx->cache); + zend_hash_init(ctx->cache, 16, NULL, ZVAL_PTR_DTOR, 0); + ctx->initialized = true; + + zval ctx_zval; + ZVAL_PTR(&ctx_zval, ctx); + ZEND_ASYNC_INTERNAL_CONTEXT_SET(coroutine, my_context_key, &ctx_zval); + + return false; // One-time initialization + } + + PHP_MINIT_FUNCTION(my_extension) + { + my_context_key = ZEND_ASYNC_INTERNAL_CONTEXT_KEY_ALLOC("my_extension"); + + if (ZEND_ASYNC_IS_ENABLED()) { + zend_async_add_main_coroutine_start_handler(my_context_initializer); + } + + return SUCCESS; + } + +Context Access Pattern +====================== + +Safe pattern for accessing extension context: + +.. code:: c + + my_context_t* get_my_context(zend_coroutine_t *coroutine) + { + if (!coroutine) { + coroutine = ZEND_ASYNC_CURRENT_COROUTINE; + if (!coroutine) return NULL; + } + + zval *ctx_zval = ZEND_ASYNC_INTERNAL_CONTEXT_FIND(coroutine, my_context_key); + if (!ctx_zval || Z_TYPE_P(ctx_zval) != IS_PTR) { + return NULL; + } + + my_context_t *ctx = (my_context_t *)Z_PTR_P(ctx_zval); + return ctx->initialized ? ctx : NULL; + } + +****************************** + Resource Management Patterns +****************************** + +RAII Pattern for Events +======================= + +Resource Acquisition Is Initialization pattern for async events: + +.. code:: c + + typedef struct { + zend_async_event_t *event; + bool owns_reference; + } event_guard_t; + + static event_guard_t* event_guard_create(zend_async_event_t *event, bool add_ref) + { + event_guard_t *guard = emalloc(sizeof(event_guard_t)); + guard->event = event; + guard->owns_reference = add_ref; + + if (add_ref) { + ZEND_ASYNC_EVENT_ADD_REF(event); + } + + return guard; + } + + static void event_guard_destroy(event_guard_t *guard) + { + if (guard->owns_reference && guard->event) { + ZEND_ASYNC_EVENT_DEL_REF(guard->event); + } + efree(guard); + } + + void my_operation_with_event(void) + { + zend_async_event_t *event = create_some_event(); + event_guard_t *guard = event_guard_create(event, true); + + // Use event... + + // Automatic cleanup + event_guard_destroy(guard); + } + +Cleanup Callback Pattern +======================== + +Registering cleanup callbacks for async operations: + +.. code:: c + + typedef struct { + void *user_data; + void (*cleanup_fn)(void *data); + } cleanup_context_t; + + static void cleanup_callback(zend_async_event_t *event, zend_async_event_callback_t *callback, + void *result, zend_object *exception) + { + cleanup_context_t *ctx = (cleanup_context_t *)callback->arg; + + if (ctx->cleanup_fn && ctx->user_data) { + ctx->cleanup_fn(ctx->user_data); + } + + efree(ctx); + } + + void register_cleanup(zend_async_event_t *event, void *data, void (*cleanup_fn)(void*)) + { + cleanup_context_t *ctx = emalloc(sizeof(cleanup_context_t)); + ctx->user_data = data; + ctx->cleanup_fn = cleanup_fn; + + zend_async_event_callback_t *callback = zend_async_event_callback_new( + event, cleanup_callback, 0 + ); + callback->arg = ctx; + + event->add_callback(event, callback); + } + +************************* + Error Handling Patterns +************************* + +Exception Propagation Pattern +============================= + +Proper exception handling in async context: + +.. code:: c + + static void async_operation_callback(zend_async_event_t *event, zend_async_event_callback_t *callback, + void *result, zend_object *exception) + { + zend_coroutine_t *coroutine = (zend_coroutine_t *)callback->arg; + + if (exception) { + // Propagate exception to coroutine + ZEND_ASYNC_RESUME_WITH_ERROR(coroutine, exception, true); + } else { + // Process result and resume normally + process_async_result(result); + ZEND_ASYNC_RESUME(coroutine); + } + + // Cleanup + ZEND_ASYNC_EVENT_DEL_REF(event); + } + +Error Recovery Pattern +====================== + +Handling recoverable errors in async operations: + +.. code:: c + + typedef struct { + int retry_count; + int max_retries; + void *operation_data; + } retry_context_t; + + static void retry_operation_callback(zend_async_event_t *event, zend_async_event_callback_t *callback, + void *result, zend_object *exception) + { + retry_context_t *ctx = (retry_context_t *)callback->arg; + + if (exception && ctx->retry_count < ctx->max_retries) { + // Retry the operation + ctx->retry_count++; + php_error(E_NOTICE, "Retrying operation (attempt %d/%d)", + ctx->retry_count, ctx->max_retries); + + retry_async_operation(ctx); + return; + } + + // Either success or max retries reached + if (exception) { + handle_final_error(exception, ctx); + } else { + handle_success(result, ctx); + } + + efree(ctx); + } + +********************** + Performance Patterns +********************** + +Object Pooling Pattern +====================== + +Reusing objects to reduce allocation overhead: + +.. code:: c + + static zend_async_event_callback_t *callback_pool[MAX_POOL_SIZE]; + static int pool_size = 0; + static zend_mutex_t pool_mutex; + + static zend_async_event_callback_t* get_pooled_callback(void) + { + zend_mutex_lock(&pool_mutex); + + zend_async_event_callback_t *callback = NULL; + if (pool_size > 0) { + callback = callback_pool[--pool_size]; + } + + zend_mutex_unlock(&pool_mutex); + + if (!callback) { + callback = emalloc(sizeof(zend_async_event_callback_t)); + memset(callback, 0, sizeof(zend_async_event_callback_t)); + } + + return callback; + } + + static void return_pooled_callback(zend_async_event_callback_t *callback) + { + // Reset callback state + memset(callback, 0, sizeof(zend_async_event_callback_t)); + + zend_mutex_lock(&pool_mutex); + + if (pool_size < MAX_POOL_SIZE) { + callback_pool[pool_size++] = callback; + } else { + efree(callback); + } + + zend_mutex_unlock(&pool_mutex); + } + +Batch Operations Pattern +======================== + +Grouping operations for better performance: + +.. code:: c + + typedef struct { + zend_async_event_t **events; + int event_count; + int completed_count; + void (*completion_callback)(void *batch_data); + void *batch_data; + } batch_operation_t; + + static void batch_item_callback(zend_async_event_t *event, zend_async_event_callback_t *callback, + void *result, zend_object *exception) + { + batch_operation_t *batch = (batch_operation_t *)callback->arg; + + __sync_fetch_and_add(&batch->completed_count, 1); + + if (batch->completed_count == batch->event_count) { + // All operations completed + if (batch->completion_callback) { + batch->completion_callback(batch->batch_data); + } + cleanup_batch(batch); + } + } + +********************** + Integration Patterns +********************** + +Reactor Integration Pattern +=========================== + +Integrating with external event loops: + +.. code:: c + + typedef struct { + my_event_loop_t *external_loop; + HashTable *pending_events; + bool integrated; + } reactor_integration_t; + + static void bridge_event_callback(my_external_event_t *ext_event, void *data) + { + zend_async_event_t *async_event = (zend_async_event_t *)data; + + // Convert external event result to async event result + void *result = convert_external_result(ext_event); + zend_object *exception = check_external_error(ext_event); + + // Notify async event callbacks + async_event->callbacks_notify(async_event, result, exception); + } + + bool integrate_with_reactor(my_event_loop_t *loop) + { + if (!ZEND_ASYNC_REACTOR_IS_ENABLED()) { + return false; + } + + reactor_integration_t *integration = emalloc(sizeof(reactor_integration_t)); + integration->external_loop = loop; + + ALLOC_HASHTABLE(integration->pending_events); + zend_hash_init(integration->pending_events, 32, NULL, NULL, 0); + + // Set up bridge between external loop and async events + loop->set_callback(loop, bridge_event_callback); + + return true; + } + +****************** + Testing Patterns +****************** + +Mock Implementation Pattern +=========================== + +Creating testable async code: + +.. code:: c + + #ifdef TESTING_MODE + + typedef struct { + zend_async_event_t base; + void *expected_result; + zend_object *expected_exception; + bool should_fail; + } mock_event_t; + + static void mock_event_start(zend_async_event_t *event) + { + mock_event_t *mock = (mock_event_t *)event; + + // Simulate immediate completion for testing + if (mock->should_fail) { + event->callbacks_notify(event, NULL, mock->expected_exception); + } else { + event->callbacks_notify(event, mock->expected_result, NULL); + } + } + + mock_event_t* create_mock_event(void *result, zend_object *exception) + { + mock_event_t *mock = emalloc(sizeof(mock_event_t)); + memset(mock, 0, sizeof(mock_event_t)); + + mock->base.start = mock_event_start; + mock->expected_result = result; + mock->expected_exception = exception; + mock->should_fail = (exception != NULL); + + return mock; + } + + #endif + +******************* + Advanced Patterns +******************* + +State Machine Pattern +===================== + +Managing complex async state transitions: + +.. code:: c + + typedef enum { + STATE_IDLE, + STATE_CONNECTING, + STATE_CONNECTED, + STATE_SENDING, + STATE_RECEIVING, + STATE_ERROR, + STATE_CLOSED + } connection_state_t; + + typedef struct { + connection_state_t state; + zend_async_event_t *current_event; + void (*state_handlers[STATE_CLOSED + 1])(void *connection); + void *connection_data; + } async_state_machine_t; + + static void transition_state(async_state_machine_t *sm, connection_state_t new_state) + { + if (sm->state != new_state) { + sm->state = new_state; + + if (sm->state_handlers[new_state]) { + sm->state_handlers[new_state](sm->connection_data); + } + } + } + +Producer-Consumer Pattern +========================= + +Implementing async producer-consumer with channels: + +.. code:: c + + typedef struct { + zend_async_channel_t *channel; + zend_coroutine_t *producer_coro; + zend_coroutine_t *consumer_coro; + bool shutdown_requested; + } producer_consumer_t; + + static void producer_coroutine(void *arg) + { + producer_consumer_t *pc = (producer_consumer_t *)arg; + + while (!pc->shutdown_requested) { + // Produce data + zval data; + produce_data(&data); + + // Send to channel (may suspend if channel is full) + if (!pc->channel->send(pc->channel, &data)) { + break; // Channel closed + } + } + } + + static void consumer_coroutine(void *arg) + { + producer_consumer_t *pc = (producer_consumer_t *)arg; + + while (!pc->shutdown_requested) { + zval data; + + // Receive from channel (may suspend if channel is empty) + if (!pc->channel->receive(pc->channel, &data)) { + break; // Channel closed + } + + // Process data + process_data(&data); + zval_dtor(&data); + } + } + +************************ + Best Practices Summary +************************ + +#. **Always check enablement** before using async APIs +#. **Provide sync fallbacks** for non-async environments +#. **Use internal context** for per-coroutine data +#. **Handle reference counting** properly +#. **Register cleanup handlers** for resource management +#. **Use appropriate error handling** patterns +#. **Consider performance** with pooling and batching +#. **Test thoroughly** with mock implementations +#. **Document async behavior** clearly for users +#. **Follow reactor integration** patterns for external loops diff --git a/main/main.c b/main/main.c index 3518e4137ecef..6322d0e24b0e6 100644 --- a/main/main.c +++ b/main/main.c @@ -85,6 +85,7 @@ #include "rfc1867.h" #include "main_arginfo.h" +#include "zend_async_API.h" /* }}} */ PHPAPI int (*php_register_internal_extensions_func)(void) = php_register_internal_extensions; @@ -142,6 +143,7 @@ PHPAPI char *php_get_version(sapi_module_struct *sapi_module) #ifdef HAVE_GCOV " GCOV" #endif + " " ZEND_ASYNC_API ); smart_string_appends(&version_info, "Copyright (c) The PHP Group\n"); if (php_build_provider()) { @@ -149,7 +151,6 @@ PHPAPI char *php_get_version(sapi_module_struct *sapi_module) } smart_string_appends(&version_info, get_zend_version()); smart_string_0(&version_info); - return version_info.c; } @@ -1929,6 +1930,8 @@ void php_request_shutdown(void *dummy) zend_call_destructors(); } zend_end_try(); + ZEND_ASYNC_RUN_SCHEDULER_AFTER_MAIN(); + /* 3. Flush all output buffers */ zend_try { php_output_end_all(); @@ -2155,6 +2158,7 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi module_shutdown = false; module_startup = true; + zend_async_init_internal_context_api(); sapi_initialize_empty_request(); sapi_activate(); @@ -2176,6 +2180,7 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi php_startup_ticks(); #endif gc_globals_ctor(); + zend_async_globals_ctor(); zend_observer_startup(); #if ZEND_DEBUG @@ -2501,6 +2506,8 @@ void php_module_shutdown(void) module_initialized = false; + zend_async_api_shutdown(); + #ifndef ZTS core_globals_dtor(&core_globals); gc_globals_dtor(); @@ -2596,6 +2603,9 @@ PHPAPI bool php_execute_script_ex(zend_file_handle *primary_file, zval *retval) if (append_file_p && result) { result = zend_execute_script(ZEND_REQUIRE, NULL, append_file_p) == SUCCESS; } + + ZEND_ASYNC_RUN_SCHEDULER_AFTER_MAIN(); + ZEND_ASYNC_INITIALIZE; } zend_catch { result = false; } zend_end_try(); @@ -2767,7 +2777,8 @@ PHPAPI void php_reserve_tsrm_memory(void) TSRM_ALIGNED_SIZE(zend_mm_globals_size()) + TSRM_ALIGNED_SIZE(zend_gc_globals_size()) + TSRM_ALIGNED_SIZE(sizeof(php_core_globals)) + - TSRM_ALIGNED_SIZE(sizeof(sapi_globals_struct)) + TSRM_ALIGNED_SIZE(sizeof(sapi_globals_struct)) + + TSRM_ALIGNED_SIZE(sizeof(zend_async_globals_t)) ); } /* }}} */ diff --git a/win32/build/config.w32 b/win32/build/config.w32 index 403f0aa6efbfe..bd68db44c7b9a 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -407,3 +407,7 @@ toolset_setup_intrinsic_cflags(); ARG_ENABLE('snapshot-build', 'Build a snapshot: turns on everything it can and ignores build errors', 'no'); ARG_ENABLE('vs-link-compat', 'Allow linking of libraries built with compatible versions of VS toolset', 'yes'); + +ARG_ENABLE("async-api", "Disable async API support", "yes"); + +ADD_SOURCES("Zend", "zend_async_API.c"); \ No newline at end of file diff --git a/win32/build/confutils.js b/win32/build/confutils.js index 244defdf68f72..128826f68b79f 100644 --- a/win32/build/confutils.js +++ b/win32/build/confutils.js @@ -1998,6 +1998,8 @@ function write_summary() ar[k++] = ['Static analyzer', 'disabled']; } + ar[k++] = ['True Async API', 'Yes']; + output_as_table(["",""], ar); STDOUT.WriteBlankLines(2); }