diff --git a/build/php.m4 b/build/php.m4 index db4265c66fc67..d6c70337baab2 100644 --- a/build/php.m4 +++ b/build/php.m4 @@ -1371,6 +1371,48 @@ int main(void) { ]) ]) +AC_DEFUN([PHP_POLL_MECHANISMS], +[ + AC_MSG_CHECKING([for polling mechanisms]) + poll_mechanisms="" + + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + #include + ], [ + int fd = epoll_create(1); + return fd; + ])], [ + AC_DEFINE([HAVE_EPOLL], [1], [Define if epoll is available]) + poll_mechanisms="$poll_mechanisms epoll" + ]) + + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + #include + #include + ], [ + int kq = kqueue(); + return kq; + ])], [ + AC_DEFINE([HAVE_KQUEUE], [1], [Define if kqueue is available]) + poll_mechanisms="$poll_mechanisms kqueue" + ]) + + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + #include + ], [ + int port = port_create(); + return port; + ])], [ + AC_DEFINE([HAVE_EVENT_PORTS], [1], [Define if event ports are available]) + poll_mechanisms="$poll_mechanisms eventport" + ]) + + dnl Set poll mechanisms including poll that is always available + poll_mechanisms="$poll_mechanisms poll" + + AC_MSG_RESULT([$poll_mechanisms]) +]) + dnl ---------------------------------------------------------------------------- dnl Library/function existence and build sanity checks. dnl ---------------------------------------------------------------------------- diff --git a/configure.ac b/configure.ac index 2bd6ae26ce625..619e85e839a80 100644 --- a/configure.ac +++ b/configure.ac @@ -426,6 +426,7 @@ AC_CHECK_HEADERS(m4_normalize([ ]) PHP_FOPENCOOKIE +PHP_POLL_MECHANISMS PHP_BROKEN_GETCWD AS_VAR_IF([GCC], [yes], [PHP_BROKEN_GCC_STRLEN_OPT]) @@ -1677,6 +1678,16 @@ PHP_ADD_SOURCES_X([main], [PHP_FASTCGI_OBJS], [no]) +PHP_ADD_SOURCES([main/poll], m4_normalize([ + poll_backend_epoll.c + poll_backend_eventport.c + poll_backend_kqueue.c + poll_backend_poll.c + poll_core.c + poll_fd_table.c + ]), + [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) + PHP_ADD_SOURCES([main/streams], m4_normalize([ cast.c filter.c diff --git a/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt b/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt index 47813255381e4..3db78ba78062d 100644 --- a/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt +++ b/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt @@ -16,5 +16,8 @@ AssertionError Directory RoundingMode StreamBucket +StreamPollContext +StreamPollEvent +StreamPollException __PHP_Incomplete_Class php_user_filter diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index ee148ff6c9226..dd8498480b28e 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -303,6 +303,7 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */ BASIC_MINIT_SUBMODULE(browscap) BASIC_MINIT_SUBMODULE(standard_filters) BASIC_MINIT_SUBMODULE(user_filters) + BASIC_MINIT_SUBMODULE(stream_poll) BASIC_MINIT_SUBMODULE(password) BASIC_MINIT_SUBMODULE(image) BASIC_MINIT_SUBMODULE(url) diff --git a/ext/standard/basic_functions.h b/ext/standard/basic_functions.h index bad6fcf4e645e..9a12b4a2a3fc6 100644 --- a/ext/standard/basic_functions.h +++ b/ext/standard/basic_functions.h @@ -42,6 +42,7 @@ PHP_MINFO_FUNCTION(basic); ZEND_API void php_get_highlight_struct(zend_syntax_highlighter_ini *syntax_highlighter_ini); +PHP_MINIT_FUNCTION(stream_poll); PHP_MINIT_FUNCTION(user_filters); PHP_RSHUTDOWN_FUNCTION(user_filters); PHP_RSHUTDOWN_FUNCTION(browscap); diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 541415a5ab918..a0ff89de99e9f 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -3386,6 +3386,21 @@ function soundex(string $string): string {} /* streamsfuncs.c */ +function stream_poll_create(int|string $backend = STREAM_POLL_BACKEND_AUTO): StreamPollContext {} + +/** @param resource $stream */ +function stream_poll_add(StreamPollContext $poll_ctx, $stream, int $events, mixed $data = null): void {} + +/** @param resource $stream */ +function stream_poll_modify(StreamPollContext $poll_ctx, $stream, int $events, mixed $data = null): void {} + +/** @param resource $stream */ +function stream_poll_remove(StreamPollContext $poll_ctx, $stream): void {} + +function stream_poll_wait(StreamPollContext $poll_ctx, int $timeout = -1, int $max_events = -1): array {} + +function stream_poll_backend_name(StreamPollContext $poll_ctx): string {} + function stream_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, ?int $microseconds = null): int|false {} /** diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index ba9d1710137cc..d9ca0512615ae 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: deb4ea96dd130d8a0174678095c30a61e118bd60 */ + * Stub hash: fcf29622f3ae629b790b19d4d753750974e332b3 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -1810,6 +1810,34 @@ ZEND_END_ARG_INFO() #define arginfo_soundex arginfo_base64_encode +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_stream_poll_create, 0, 0, StreamPollContext, 0) + ZEND_ARG_TYPE_MASK(0, backend, MAY_BE_LONG|MAY_BE_STRING, "STREAM_POLL_BACKEND_AUTO") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_add, 0, 3, IS_VOID, 0) + ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) + ZEND_ARG_INFO(0, stream) + ZEND_ARG_TYPE_INFO(0, events, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_MIXED, 0, "null") +ZEND_END_ARG_INFO() + +#define arginfo_stream_poll_modify arginfo_stream_poll_add + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_remove, 0, 2, IS_VOID, 0) + ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) + ZEND_ARG_INFO(0, stream) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_wait, 0, 1, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout, IS_LONG, 0, "-1") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, max_events, IS_LONG, 0, "-1") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_backend_name, 0, 1, IS_STRING, 0) + ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_stream_select, 0, 4, MAY_BE_LONG|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(1, read, IS_ARRAY, 1) ZEND_ARG_TYPE_INFO(1, write, IS_ARRAY, 1) @@ -2780,6 +2808,12 @@ ZEND_FUNCTION(proc_get_status); ZEND_FUNCTION(quoted_printable_decode); ZEND_FUNCTION(quoted_printable_encode); ZEND_FUNCTION(soundex); +ZEND_FUNCTION(stream_poll_create); +ZEND_FUNCTION(stream_poll_add); +ZEND_FUNCTION(stream_poll_modify); +ZEND_FUNCTION(stream_poll_remove); +ZEND_FUNCTION(stream_poll_wait); +ZEND_FUNCTION(stream_poll_backend_name); ZEND_FUNCTION(stream_select); ZEND_FUNCTION(stream_context_create); ZEND_FUNCTION(stream_context_set_params); @@ -3384,6 +3418,12 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY("quoted_printable_decode", zif_quoted_printable_decode, arginfo_quoted_printable_decode, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("quoted_printable_encode", zif_quoted_printable_encode, arginfo_quoted_printable_encode, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_FE(soundex, arginfo_soundex) + ZEND_FE(stream_poll_create, arginfo_stream_poll_create) + ZEND_FE(stream_poll_add, arginfo_stream_poll_add) + ZEND_FE(stream_poll_modify, arginfo_stream_poll_modify) + ZEND_FE(stream_poll_remove, arginfo_stream_poll_remove) + ZEND_FE(stream_poll_wait, arginfo_stream_poll_wait) + ZEND_FE(stream_poll_backend_name, arginfo_stream_poll_backend_name) ZEND_FE(stream_select, arginfo_stream_select) ZEND_FE(stream_context_create, arginfo_stream_context_create) ZEND_FE(stream_context_set_params, arginfo_stream_context_set_params) diff --git a/ext/standard/config.m4 b/ext/standard/config.m4 index ef6b3c5a01018..78f57ebbea149 100644 --- a/ext/standard/config.m4 +++ b/ext/standard/config.m4 @@ -437,6 +437,7 @@ PHP_NEW_EXTENSION([standard], m4_normalize([ scanf.c sha1.c soundex.c + stream_poll.c streamsfuncs.c string.c strnatcmp.c diff --git a/ext/standard/config.w32 b/ext/standard/config.w32 index c7c14b8705ca2..e20a2b105dfdc 100644 --- a/ext/standard/config.w32 +++ b/ext/standard/config.w32 @@ -35,7 +35,8 @@ EXTENSION("standard", "array.c base64.c basic_functions.c browscap.c \ url_scanner_ex.c ftp_fopen_wrapper.c http_fopen_wrapper.c \ php_fopen_wrapper.c credits.c css.c var_unserializer.c ftok.c sha1.c \ user_filters.c uuencode.c filters.c proc_open.c password.c \ - streamsfuncs.c http.c flock_compat.c hrtime.c", false /* never shared */, + stream_poll.c streamsfuncs.c http.c flock_compat.c hrtime.c", + false /* never shared */, '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1'); ADD_SOURCES("ext/standard/libavifinfo", "avifinfo.c", "standard"); PHP_STANDARD = "yes"; diff --git a/ext/standard/stream_poll.c b/ext/standard/stream_poll.c new file mode 100644 index 0000000000000..1f7337c2ef70e --- /dev/null +++ b/ext/standard/stream_poll.c @@ -0,0 +1,423 @@ +/* + +----------------------------------------------------------------------+ + | 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. | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "php_network.h" +#include "php_poll.h" +#include "stream_poll_arginfo.h" +#include "zend_exceptions.h" + +static zend_class_entry *stream_poll_context_class_entry; +static zend_class_entry *stream_poll_event_class_entry; +static zend_class_entry *stream_poll_exception_class_entry; +static zend_object_handlers php_stream_poll_context_object_handlers; + +/* Internal structure to hold stream data */ +typedef struct { + php_stream *stream; + zval data; +} php_stream_poll_entry; + +/* Object wrapper for userspace */ +typedef struct { + php_poll_ctx *ctx; + HashTable *stream_map; /* Maps fd -> php_stream_poll_entry */ + zend_object std; +} php_stream_poll_context_object; + +#define PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZOBJ php_stream_poll_context_object_from_zend_object +#define PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(_zv) \ + php_stream_poll_context_object_from_zend_object(Z_OBJ_P(_zv)) + +static inline php_stream_poll_context_object *php_stream_poll_context_object_from_zend_object( + zend_object *obj) +{ + return (php_stream_poll_context_object *) ((char *) (obj) -XtOffsetOf( + php_stream_poll_context_object, std)); +} + +static void stream_map_entry_dtor(zval *zv) +{ + php_stream_poll_entry *entry = Z_PTR_P(zv); + if (entry) { + zval_ptr_dtor(&entry->data); + efree(entry); + } +} + +static void php_stream_poll_context_free_object_storage(zend_object *obj) +{ + php_stream_poll_context_object *intern = PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZOBJ(obj); + + if (intern->ctx) { + php_poll_destroy(intern->ctx); + } + if (intern->stream_map) { + zend_hash_destroy(intern->stream_map); + efree(intern->stream_map); + } + zend_object_std_dtor(&intern->std); +} + +static inline zend_object *php_stream_poll_context_create_object_ex( + zend_class_entry *ce, php_poll_ctx **ctx) +{ + php_stream_poll_context_object *intern + = zend_object_alloc(sizeof(php_stream_poll_context_object), ce); + + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + + intern->ctx = NULL; + intern->stream_map = NULL; + *ctx = NULL; + + return &intern->std; +} + +static zend_object *php_stream_poll_context_create_object(zend_class_entry *ce) +{ + php_poll_ctx *ctx; + return php_stream_poll_context_create_object_ex(ce, &ctx); +} + +/* Create a new stream polling context */ +PHP_FUNCTION(stream_poll_create) +{ + zend_long backend_long = PHP_POLL_BACKEND_AUTO; + zend_string *backend_str = NULL; + php_poll_ctx *poll_ctx; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_STR_OR_LONG(backend_str, backend_long) + ZEND_PARSE_PARAMETERS_END(); + + if (backend_str == NULL) { + poll_ctx = php_poll_create((php_poll_backend_type) backend_long, false); + } else { + poll_ctx = php_poll_create_by_name(ZSTR_VAL(backend_str), false); + } + if (!poll_ctx) { + zend_throw_exception( + stream_poll_exception_class_entry, "Failed to create polling context", 0); + RETURN_THROWS(); + } + if (php_poll_init(poll_ctx) != SUCCESS) { + php_poll_destroy(poll_ctx); + zend_throw_exception( + stream_poll_exception_class_entry, "Failed to initialize polling context", 0); + RETURN_THROWS(); + } + + /* Create object */ + object_init_ex(return_value, stream_poll_context_class_entry); + php_stream_poll_context_object *intern = PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(return_value); + + intern->ctx = poll_ctx; + intern->stream_map = emalloc(sizeof(HashTable)); + zend_hash_init(intern->stream_map, 8, NULL, stream_map_entry_dtor, 0); +} + +static php_stream_poll_context_object *get_stream_poll_context_object(zval *obj) +{ + if (Z_TYPE_P(obj) != IS_OBJECT + || !instanceof_function(Z_OBJCE_P(obj), stream_poll_context_class_entry)) { + return NULL; + } + return PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(obj); +} + +/* Add a stream to the polling context */ +PHP_FUNCTION(stream_poll_add) +{ + zval *zpoll_ctx, *zdata = NULL; + zend_long events; + php_stream *stream; + php_stream_poll_context_object *context; + php_stream_poll_entry *entry; + php_socket_t fd; + + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + PHP_Z_PARAM_STREAM(stream) + Z_PARAM_LONG(events) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(zdata) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + /* Get file descriptor from stream */ + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + (void *) &fd, + 1) != SUCCESS + || fd == -1) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); + RETURN_THROWS(); + } + + /* Check if already exists */ + if (zend_hash_index_exists(context->stream_map, (zend_ulong) fd)) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream already added", 0); + RETURN_THROWS(); + } + + /* Create entry */ + entry = emalloc(sizeof(php_stream_poll_entry)); + entry->stream = stream; + if (zdata) { + ZVAL_COPY(&entry->data, zdata); + } else { + ZVAL_NULL(&entry->data); + } + + /* Add to internal poll context */ + if (php_poll_add(context->ctx, (int) fd, (uint32_t) events, entry) != SUCCESS) { + zval_ptr_dtor(&entry->data); + efree(entry); + zend_throw_exception(stream_poll_exception_class_entry, "Failed to add stream to poll", 0); + RETURN_THROWS(); + } + + /* Add to our mapping */ + zend_hash_index_add_ptr(context->stream_map, (zend_ulong) fd, entry); +} + +/* Modify events for a stream in the polling context */ +PHP_FUNCTION(stream_poll_modify) +{ + zval *zpoll_ctx, *zdata = NULL; + zend_long events; + php_stream *stream; + php_stream_poll_context_object *context; + php_stream_poll_entry *entry; + php_socket_t fd; + + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + PHP_Z_PARAM_STREAM(stream) + Z_PARAM_LONG(events) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(zdata) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + /* Get file descriptor from stream */ + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + (void *) &fd, + 1) != SUCCESS + || fd == -1) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); + RETURN_THROWS(); + } + + /* Find existing entry */ + entry = zend_hash_index_find_ptr(context->stream_map, (zend_ulong) fd); + if (!entry) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream not found", 0); + RETURN_THROWS(); + } + + /* Update data if provided */ + if (zdata) { + zval_ptr_dtor(&entry->data); + ZVAL_COPY(&entry->data, zdata); + } + + /* Modify in poll context */ + if (php_poll_modify(context->ctx, (int) fd, (uint32_t) events, entry) != SUCCESS) { + zend_throw_exception( + stream_poll_exception_class_entry, "Failed to modify stream in poll", 0); + RETURN_THROWS(); + } +} + +/* Remove a stream from the polling context */ +PHP_FUNCTION(stream_poll_remove) +{ + zval *zpoll_ctx; + php_stream *stream; + php_stream_poll_context_object *context; + php_socket_t fd; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + PHP_Z_PARAM_STREAM(stream) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + /* Get file descriptor from stream */ + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + (void *) &fd, + 1) != SUCCESS + || fd == -1) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); + RETURN_THROWS(); + } + + if (!zend_hash_index_exists(context->stream_map, (zend_ulong) fd)) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream not found", 0); + RETURN_THROWS(); + } + + /* Remove from poll context */ + if (php_poll_remove(context->ctx, (int) fd) != SUCCESS) { + zend_throw_exception( + stream_poll_exception_class_entry, "Failed to remove stream from poll", 0); + RETURN_THROWS(); + } + + /* Remove from our mapping */ + zend_hash_index_del(context->stream_map, (zend_ulong) fd); +} + +/* Wait for events on streams in the polling context */ +PHP_FUNCTION(stream_poll_wait) +{ + zval *zpoll_ctx; + zend_long timeout = -1; + zend_long max_events = -1; + php_stream_poll_context_object *context; + php_poll_event *events; + int num_events, i; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(timeout) + Z_PARAM_LONG(max_events) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + if (max_events <= 0) { + /* Get suitable value from the polling backend */ + max_events = php_poll_get_suitable_max_events(context->ctx); + if (max_events <= 0) { + /* This should not happen but use fallback just in case */ + max_events = 64; + } + } + events = emalloc(sizeof(php_poll_event) * max_events); + + num_events = php_poll_wait(context->ctx, events, max_events, (int) timeout); + + if (num_events < 0) { + efree(events); + zend_throw_exception(stream_poll_exception_class_entry, "Poll wait failed", 0); + RETURN_THROWS(); + } + + array_init(return_value); + + for (i = 0; i < num_events; i++) { + php_stream_poll_entry *entry = (php_stream_poll_entry *) events[i].data; + zval event_obj; + + object_init_ex(&event_obj, stream_poll_event_class_entry); + + /* Set stream property */ + zval stream_zval; + php_stream_to_zval(entry->stream, &stream_zval); + zend_update_property( + stream_poll_event_class_entry, Z_OBJ(event_obj), ZEND_STRL("stream"), &stream_zval); + + /* Set events property */ + zend_update_property_long(stream_poll_event_class_entry, Z_OBJ(event_obj), + ZEND_STRL("events"), events[i].revents); + + /* Set data property */ + zend_update_property( + stream_poll_event_class_entry, Z_OBJ(event_obj), ZEND_STRL("data"), &entry->data); + + add_next_index_zval(return_value, &event_obj); + } + + efree(events); +} + +/* Get the backend name for the polling context */ +PHP_FUNCTION(stream_poll_backend_name) +{ + zval *zpoll_ctx; + php_stream_poll_context_object *context; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + const char *backend_name = php_poll_backend_name(context->ctx); + RETURN_STRING(backend_name); +} + +PHP_METHOD(StreamPollContext, __construct) +{ + zend_throw_error( + NULL, "Cannot directly construct StreamPollContext, use stream_poll_create() instead"); +} + +/* Initialize the stream poll classes - add to PHP_MINIT_FUNCTION */ +PHP_MINIT_FUNCTION(stream_poll) +{ + /* Register symbols */ + register_stream_poll_symbols(module_number); + + /* Register classes */ + stream_poll_context_class_entry = register_class_StreamPollContext(); + stream_poll_context_class_entry->create_object = php_stream_poll_context_create_object; + stream_poll_context_class_entry->default_object_handlers + = &php_stream_poll_context_object_handlers; + + stream_poll_event_class_entry = register_class_StreamPollEvent(); + stream_poll_exception_class_entry = register_class_StreamPollException(zend_ce_exception); + + /* Set up object handlers */ + memcpy(&php_stream_poll_context_object_handlers, &std_object_handlers, + sizeof(zend_object_handlers)); + php_stream_poll_context_object_handlers.offset + = XtOffsetOf(php_stream_poll_context_object, std); + php_stream_poll_context_object_handlers.free_obj = php_stream_poll_context_free_object_storage; + + /* Register poll backends */ + php_poll_register_backends(); + + return SUCCESS; +} diff --git a/ext/standard/stream_poll.stub.php b/ext/standard/stream_poll.stub.php new file mode 100644 index 0000000000000..4331c2c425e84 --- /dev/null +++ b/ext/standard/stream_poll.stub.php @@ -0,0 +1,94 @@ + $event) { + if (!$event instanceof StreamPollEvent) { + die('Invalid event type'); + } + echo "Event[$i]: " . $event->events . ", user data: " . $event->data; + if ($read_data && $event->events & STREAM_POLL_READ) { + $data = fread($event->stream, 1024); + echo ", read data: '$data'"; + } + echo "\n"; + } +} + +function pt_expect_events($events, $expected, $poll_ctx = null): void { + if (!is_array($events)) { + die("Events must be an array\n"); + } + + if (!is_array($expected)) { + die("Expected events must be an array\n"); + } + + $event_count = count($events); + $expected_count = count($expected); + + // Get current backend name for backend-specific expectations + $backend_name = $poll_ctx ? stream_poll_backend_name($poll_ctx) : 'unknown'; + + if ($event_count !== $expected_count) { + echo "Event count mismatch: got $event_count, expected $expected_count\n"; + pt_print_mismatched_events($events, $expected, [], $backend_name); + return; + } + + // Convert events to comparable format for matching + $actual_events = []; + foreach ($events as $event) { + if (!$event instanceof StreamPollEvent) { + die('Invalid event type'); + } + + $event_data = [ + 'events' => $event->events, + 'data' => $event->data + ]; + + $actual_events[] = $event_data; + } + + // Resolve backend-specific expectations + $resolved_expected = []; + foreach ($expected as $exp_event) { + $resolved_event = $exp_event; + + if (isset($exp_event['events']) && is_array($exp_event['events'])) { + $resolved_event['events'] = pt_resolve_backend_specific_value($exp_event['events'], $backend_name); + } + + $resolved_expected[] = $resolved_event; + } + + // Try to match each expected event with an actual event + $matched = []; + $unmatched_expected = []; + + foreach ($resolved_expected as $exp_idx => $exp_event) { + $found_match = false; + + foreach ($actual_events as $act_idx => $act_event) { + if (isset($matched[$act_idx])) { + continue; // Already matched + } + + // Check if events and data match + if ($act_event['events'] === $exp_event['events'] && + $act_event['data'] === $exp_event['data']) { + + // If read data is expected, check it + if (isset($exp_event['read'])) { + $read_data = fread($events[$act_idx]->stream, 1024); + if ($read_data !== $exp_event['read']) { + continue; // Read data doesn't match + } + } + + $matched[$act_idx] = $exp_idx; + $found_match = true; + break; + } + } + + if (!$found_match) { + $unmatched_expected[] = $exp_event; + } + } + + // Check if all events matched + if (count($matched) === $event_count && empty($unmatched_expected)) { + echo "Events matched - count: $event_count\n"; + } else { + echo "Events did not match:\n"; + pt_print_mismatched_events($events, $expected, $matched, $backend_name); + } +} + +function pt_resolve_backend_specific_value($backend_map, $current_backend) { + // Direct backend match + if (isset($backend_map[$current_backend])) { + return $backend_map[$current_backend]; + } + + // Check for multi-backend keys (e.g., "poll|epoll") + foreach ($backend_map as $key => $value) { + if (strpos($key, '|') !== false) { + $backends = array_map('trim', explode('|', $key)); + if (in_array($current_backend, $backends)) { + return $value; + } + } + } + + // Fall back to default + if (isset($backend_map['default'])) { + return $backend_map['default']; + } + + // If no match found, this is an error + die("No backend-specific value found for '$current_backend' and no default specified\n"); +} + +function pt_event_flags_to_string($flags): string { + $names = []; + if ($flags & STREAM_POLL_READ) $names[] = 'READ'; + if ($flags & STREAM_POLL_WRITE) $names[] = 'WRITE'; + if ($flags & STREAM_POLL_ERROR) $names[] = 'ERROR'; + if ($flags & STREAM_POLL_HUP) $names[] = 'HUP'; + + return empty($names) ? 'NONE:' . $flags : implode('|', $names); +} + +function pt_print_mismatched_events($actual_events, $expected_events, $matched = [], $backend_name = null): void { + echo "Actual events:\n"; + foreach ($actual_events as $i => $event) { + $match_status = isset($matched[$i]) ? " [MATCHED]" : " [UNMATCHED]"; + $event_names = pt_event_flags_to_string($event->events); + echo " Event[$i]: $event_names, user data: " . $event->data . $match_status . "\n"; + } + + echo "Expected events:\n"; + foreach ($expected_events as $i => $exp_event) { + $was_matched = in_array($i, $matched); + $match_status = $was_matched ? " [MATCHED]" : " [UNMATCHED]"; + + $events_value = $exp_event['events']; + if (is_array($events_value) && $backend_name !== null) { + $events_value = pt_resolve_backend_specific_value($events_value, $backend_name); + } + + $event_names = pt_event_flags_to_string($events_value); + echo " Event[$i]: $event_names, user data: " . $exp_event['data']; + if (isset($exp_event['read'])) { + echo ", read data: '" . $exp_event['read'] . "'"; + } + echo $match_status . "\n"; + } +} diff --git a/ext/standard/tests/streams/stream_poll_add_error_duplicite.phpt b/ext/standard/tests/streams/stream_poll_add_error_duplicite.phpt new file mode 100644 index 0000000000000..c2ff7a4011418 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_add_error_duplicite.phpt @@ -0,0 +1,19 @@ +--TEST-- +Stream polling - add duplicite error +--FILE-- +getMessage() . "\n"; +} +?> +--EXPECT-- +ERROR: Stream already added diff --git a/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt b/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt new file mode 100644 index 0000000000000..401350b63f134 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt @@ -0,0 +1,16 @@ +--TEST-- +Stream polling - backend name +--FILE-- + +--EXPECT-- +string(4) "poll" +string(4) "poll" diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt new file mode 100644 index 0000000000000..a600a7f7f1ed1 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt @@ -0,0 +1,17 @@ +--TEST-- +Stream polling - only add +--FILE-- + +--EXPECT-- +object(StreamPollContext)#1 (0) { +} diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt new file mode 100644 index 0000000000000..5b00be6ee6eb4 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt @@ -0,0 +1,18 @@ +--TEST-- +Stream polling - socket modify write +--FILE-- + STREAM_POLL_WRITE, 'data' => 'modified_data'] +]); +?> +--EXPECT-- +Events matched - count: 1 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt new file mode 100644 index 0000000000000..3a8d6988d5822 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt @@ -0,0 +1,19 @@ +--TEST-- +Stream polling - socket read +--FILE-- + STREAM_POLL_READ, 'data' => 'socket_data', 'read' => 'test data'] +]); + +?> +--EXPECT-- +Events matched - count: 1 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt new file mode 100644 index 0000000000000..02589be2d6327 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt @@ -0,0 +1,38 @@ +--TEST-- +Stream polling - socket remove write +--FILE-- + STREAM_POLL_WRITE, 'data' => 'socket_data_1'], + ['events' => STREAM_POLL_WRITE, 'data' => 'socket_data_2'] +]); + +stream_poll_remove($poll_ctx, $socket1w); + +pt_expect_events(stream_poll_wait($poll_ctx, 0), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket_data_2'] +]); + +// check that both streams are still usable +var_dump(fwrite($socket1w, "test 1")); +var_dump(fwrite($socket2w, "test 2")); +var_dump(fread($socket1r, 100)); +var_dump(fread($socket2r, 100)); + +?> +--EXPECT-- +Events matched - count: 2 +Events matched - count: 1 +int(6) +int(6) +string(6) "test 1" +string(6) "test 2" diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt new file mode 100644 index 0000000000000..56154f57fc396 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt @@ -0,0 +1,62 @@ +--TEST-- +Stream polling - socket write / read multiple times with edge triggering +--SKIPIF-- + +--FILE-- + STREAM_POLL_WRITE, 'data' => 'socket2_data'] +]); + +pt_expect_events(stream_poll_wait($poll_ctx, 0), []); + +fwrite($socket1w, "test data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'test data'] +]); + +fwrite($socket1w, "more data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data'] +]); + +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); + +fwrite($socket1w, " and even more data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'more data and even more data'] +]); + +fclose($socket1r); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + [ + 'events' => ['default' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'poll' => STREAM_POLL_HUP], + 'data' => 'socket2_data' + ] +], $poll_ctx); + +fclose($socket1w); +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); + +?> +--EXPECT-- +Events matched - count: 1 +Events matched - count: 0 +Events matched - count: 1 +Events matched - count: 2 +Events matched - count: 0 +Events matched - count: 1 +Events matched - count: 1 +Events matched - count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt new file mode 100644 index 0000000000000..b7a05d6ce3904 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt @@ -0,0 +1,64 @@ +--TEST-- +Stream polling - socket write / read multiple times with level triggering +--FILE-- + STREAM_POLL_WRITE, 'data' => 'socket2_data'] +]); + +pt_expect_events(stream_poll_wait($poll_ctx, 0), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'] +]); + +fwrite($socket1w, "test data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'test data'] +]); + +fwrite($socket1w, "more data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data'] +]); + +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data'] +]); + +fwrite($socket1w, " and even more data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'more data and even more data'] +]); + +fclose($socket1r); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + [ + 'events' => ['default' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'poll' => STREAM_POLL_HUP], + 'data' => 'socket2_data' + ] +], $poll_ctx); + +fclose($socket1w); +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); + +?> +--EXPECT-- +Events matched - count: 1 +Events matched - count: 1 +Events matched - count: 2 +Events matched - count: 2 +Events matched - count: 2 +Events matched - count: 2 +Events matched - count: 1 +Events matched - count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_edge.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_edge.phpt new file mode 100644 index 0000000000000..7033d5a18d4d0 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_edge.phpt @@ -0,0 +1,29 @@ +--TEST-- +Stream polling - socket write / read few time only +--SKIPIF-- + +--FILE-- + STREAM_POLL_WRITE, 'data' => 'socket2_data'] +]); +fwrite($socket1w, "test data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'test data'] +]); + +?> +--EXPECT-- +Events matched - count: 1 +Events matched - count: 1 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt new file mode 100644 index 0000000000000..fecf992668e43 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt @@ -0,0 +1,25 @@ +--TEST-- +Stream polling - socket write / read few time only +--FILE-- + STREAM_POLL_WRITE, 'data' => 'socket2_data'] +]); +fwrite($socket1w, "test data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'test data'] +]); + +?> +--EXPECT-- +Events matched - count: 1 +Events matched - count: 2 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt new file mode 100644 index 0000000000000..ce6f5922ffe98 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt @@ -0,0 +1,18 @@ +--TEST-- +Stream polling - socket write +--FILE-- + STREAM_POLL_WRITE, 'data' => 'socket_data'] +]); + +?> +--EXPECT-- +Events matched - count: 1 diff --git a/ext/standard/tests/streams/stream_poll_basic_tcp_read_multiple_level.phpt b/ext/standard/tests/streams/stream_poll_basic_tcp_read_multiple_level.phpt new file mode 100644 index 0000000000000..d20751c28ea6f --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_tcp_read_multiple_level.phpt @@ -0,0 +1,41 @@ +--TEST-- +Stream polling - TCP read write level +--FILE-- + STREAM_POLL_READ, 'data' => "server{$i}_data", 'read' => "test $i data"]; +} +pt_expect_events(stream_poll_wait($poll_ctx, 100), $expected_events); + +pt_write_sleep($clients[1], "more data"); +pt_write_sleep($clients[2], "more data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_READ, 'data' => 'server1_data', 'read' => 'more data'], + ['events' => STREAM_POLL_READ, 'data' => 'server2_data', 'read' => 'more data'] +]); + +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); + +?> +--EXPECT-- +Events matched - count: 0 +Events matched - count: 20 +Events matched - count: 2 +Events matched - count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt b/ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt new file mode 100644 index 0000000000000..c2bcdb8330bef --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt @@ -0,0 +1,33 @@ +--TEST-- +Stream polling - TCP read write oneshot +--FILE-- + STREAM_POLL_READ, 'data' => 'server1_data', 'read' => 'test data'], + ['events' => STREAM_POLL_READ, 'data' => 'server2_data', 'read' => 'test data'] +]); + +pt_write_sleep($client1, "more data"); +pt_write_sleep($client2, "more data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); + +?> +--EXPECT-- +Events matched - count: 0 +Events matched - count: 2 +Events matched - count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt b/ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt new file mode 100644 index 0000000000000..7f90997b8f139 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt @@ -0,0 +1,28 @@ +--TEST-- +Stream polling - TCP read write oneshot combined +--FILE-- + STREAM_POLL_WRITE, 'data' => 'client_data'], + ['events' => STREAM_POLL_WRITE, 'data' => 'server_data'] +]); + +pt_write_sleep($client, "test data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); + +pt_write_sleep($client, "test data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); + +?> +--EXPECT-- +Events matched - count: 2 +Events matched - count: 0 +Events matched - count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_tcp_rw_single_level.phpt b/ext/standard/tests/streams/stream_poll_basic_tcp_rw_single_level.phpt new file mode 100644 index 0000000000000..f82bdc4f097de --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_tcp_rw_single_level.phpt @@ -0,0 +1,28 @@ +--TEST-- +Stream polling - TCP read write level combined +--FILE-- + STREAM_POLL_WRITE, 'data' => 'client_data'], + ['events' => STREAM_POLL_WRITE, 'data' => 'server_data'] +]); + +fwrite($client, "test data"); +usleep(10000); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'client_data'], + ['events' => STREAM_POLL_READ | STREAM_POLL_WRITE, 'data' => 'server_data', 'read' => 'test data'] +]); + +?> +--EXPECT-- +Events matched - count: 2 +Events matched - count: 2 diff --git a/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt b/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt new file mode 100644 index 0000000000000..1e5b602f50e30 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt @@ -0,0 +1,12 @@ +--TEST-- +Stream polling - only wait +--FILE-- + +--EXPECT-- +Events count: 0 diff --git a/ext/standard/tests/streams/stream_poll_modify_error_not_found.phpt b/ext/standard/tests/streams/stream_poll_modify_error_not_found.phpt new file mode 100644 index 0000000000000..04cc0fee4c80d --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_modify_error_not_found.phpt @@ -0,0 +1,17 @@ +--TEST-- +Stream polling - modify not found error +--FILE-- +getMessage() . "\n"; +} +?> +--EXPECT-- +ERROR: Stream not found diff --git a/ext/standard/tests/streams/stream_poll_remove_error_not_found.phpt b/ext/standard/tests/streams/stream_poll_remove_error_not_found.phpt new file mode 100644 index 0000000000000..b03fd08b8b299 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_remove_error_not_found.phpt @@ -0,0 +1,17 @@ +--TEST-- +Stream polling - remove not found error +--FILE-- +getMessage() . "\n"; +} +?> +--EXPECT-- +ERROR: Stream not found diff --git a/main/php_poll.h b/main/php_poll.h new file mode 100644 index 0000000000000..cffffb706ace9 --- /dev/null +++ b/main/php_poll.h @@ -0,0 +1,100 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_POLL_H +#define PHP_POLL_H + +#include "php.h" + +/* clang-format off */ + +/* Event types */ +#define PHP_POLL_READ 0x01 +#define PHP_POLL_WRITE 0x02 +#define PHP_POLL_ERROR 0x04 +#define PHP_POLL_HUP 0x08 +#define PHP_POLL_RDHUP 0x10 +#define PHP_POLL_ONESHOT 0x20 +#define PHP_POLL_ET 0x40 /* Edge-triggered */ + +/* Poll backend types */ +typedef enum { + PHP_POLL_BACKEND_AUTO = -1, + PHP_POLL_BACKEND_POLL = 0, + PHP_POLL_BACKEND_EPOLL, + PHP_POLL_BACKEND_KQUEUE, + PHP_POLL_BACKEND_EVENTPORT, + PHP_POLL_BACKEND_IOCP +} php_poll_backend_type; + +/* Error codes */ +typedef enum { + PHP_POLL_ERR_NONE, /* No error */ + PHP_POLL_ERR_SYSTEM, /* Generic system error */ + PHP_POLL_ERR_NOMEM, /* Out of memory (ENOMEM) */ + PHP_POLL_ERR_INVALID, /* Invalid argument (EINVAL, EBADF) */ + PHP_POLL_ERR_EXISTS, /* Already exists (EEXIST) */ + PHP_POLL_ERR_NOTFOUND, /* Not found (ENOENT) */ + PHP_POLL_ERR_TIMEOUT, /* Operation timed out (ETIME, ETIMEDOUT) */ + PHP_POLL_ERR_INTERRUPTED, /* Interrupted by signal (EINTR) */ + PHP_POLL_ERR_PERMISSION, /* Permission denied (EACCES, EPERM) */ + PHP_POLL_ERR_TOOBIG, /* Too many resources (EMFILE, ENFILE) */ + PHP_POLL_ERR_AGAIN, /* Try again (EAGAIN, EWOULDBLOCK) */ + PHP_POLL_ERR_NOSUPPORT, /* Not supported (ENOSYS, EOPNOTSUPP) */ +} php_poll_error; + +/* clang-format on */ + +/* Poll event structure */ +struct php_poll_event { + int fd; /* File descriptor */ + uint32_t events; /* Requested events */ + uint32_t revents; /* Returned events */ + void *data; /* User data pointer */ +}; + +/* Forward declarations */ +typedef struct php_poll_ctx php_poll_ctx; +typedef struct php_poll_backend_ops php_poll_backend_ops; +typedef struct php_poll_event php_poll_event; + +/* Public API */ +PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, bool persistent); +PHPAPI php_poll_ctx *php_poll_create_by_name(const char *preferred_backend, bool persistent); + +PHPAPI zend_result php_poll_set_max_events_hint(php_poll_ctx *ctx, int max_events); +PHPAPI zend_result php_poll_init(php_poll_ctx *ctx); +PHPAPI void php_poll_destroy(php_poll_ctx *ctx); + +PHPAPI zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data); +PHPAPI zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data); +PHPAPI zend_result php_poll_remove(php_poll_ctx *ctx, int fd); + +PHPAPI int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout); + +PHPAPI const char *php_poll_backend_name(php_poll_ctx *ctx); +PHPAPI php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx); +PHPAPI bool php_poll_supports_et(php_poll_ctx *ctx); +PHPAPI php_poll_error php_poll_get_error(php_poll_ctx *ctx); + +/* Get suitable max_events for backend */ +PHPAPI int php_poll_get_suitable_max_events(php_poll_ctx *ctx); + +/* Backend registration */ +PHPAPI void php_poll_register_backends(void); + +/* Error string for the error */ +PHPAPI const char *php_poll_error_string(php_poll_error error); + +#endif /* PHP_POLL_H */ diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h new file mode 100644 index 0000000000000..0bc3d86e10f70 --- /dev/null +++ b/main/poll/php_poll_internal.h @@ -0,0 +1,145 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_POLL_INTERNAL_H +#define PHP_POLL_INTERNAL_H + +#include "php_poll.h" +#include "php_network.h" + +/* Allocation macros */ +#define php_poll_calloc(nmemb, size, persistent) \ + ((persistent) ? calloc((nmemb), (size)) : ecalloc((nmemb), (size))) +#define php_poll_malloc(size, persistent) ((persistent) ? malloc((size)) : emalloc((size))) +#define php_poll_realloc(ptr, size, persistent) \ + ((persistent) ? realloc((ptr), (size)) : erealloc((ptr), (size))) + +/* Backend interface */ +typedef struct php_poll_backend_ops { + php_poll_backend_type type; + const char *name; + + /* Initialize backend */ + zend_result (*init)(php_poll_ctx *ctx); + + /* Cleanup backend */ + void (*cleanup)(php_poll_ctx *ctx); + + /* Add file descriptor */ + zend_result (*add)(php_poll_ctx *ctx, int fd, uint32_t events, void *data); + + /* Modify file descriptor */ + zend_result (*modify)(php_poll_ctx *ctx, int fd, uint32_t events, void *data); + + /* Remove file descriptor */ + zend_result (*remove)(php_poll_ctx *ctx, int fd); + + /* Wait for events */ + int (*wait)(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout); + + /* Check if backend is available */ + bool (*is_available)(void); + + /* Get suitable max_events for this backend */ + int (*get_suitable_max_events)(php_poll_ctx *ctx); + + /* Backend supports edge triggering natively */ + bool supports_et; +} php_poll_backend_ops; + +/* Main poll context */ +struct php_poll_ctx { + const php_poll_backend_ops *backend_ops; + php_poll_backend_type backend_type; + + bool initialized; + bool persistent; + + /* Optional capacity hint for backends */ + int max_events_hint; + + /* Last error */ + php_poll_error last_error; + + /* Backend-specific data */ + void *backend_data; +}; + +/* Generic FD entry structure */ +typedef struct php_poll_fd_entry { + int fd; + uint32_t events; + void *data; + bool active; + uint32_t last_revents; +} php_poll_fd_entry; + +/* FD tracking table */ +typedef struct php_poll_fd_table { + HashTable entries_ht; + bool persistent; +} php_poll_fd_table; + +/* Iterator callback function type */ +typedef bool (*php_poll_fd_iterator_func_t)(int fd, php_poll_fd_entry *entry, void *user_data); + +/* Poll FD helpers - clean API with accessor functions */ +php_poll_fd_table *php_poll_fd_table_init(int initial_capacity, bool persistent); +void php_poll_fd_table_cleanup(php_poll_fd_table *table); +php_poll_fd_entry *php_poll_fd_table_find(php_poll_fd_table *table, int fd); +php_poll_fd_entry *php_poll_fd_table_get(php_poll_fd_table *table, int fd); +void php_poll_fd_table_remove(php_poll_fd_table *table, int fd); + +/* Accessor functions for table properties */ +static inline int php_poll_fd_table_count(php_poll_fd_table *table) +{ + return zend_hash_num_elements(&table->entries_ht); +} + +static inline bool php_poll_fd_table_is_empty(php_poll_fd_table *table) +{ + return zend_hash_num_elements(&table->entries_ht) == 0; +} + +/* New helper functions for improved backend integration */ +void php_poll_fd_table_foreach( + php_poll_fd_table *table, php_poll_fd_iterator_func_t callback, void *user_data); +php_socket_t php_poll_fd_table_get_max_fd(php_poll_fd_table *table); +int php_poll_fd_table_collect_events( + php_poll_fd_table *table, php_poll_event *events, int max_events); + +/* Error helper functions */ +php_poll_error php_poll_errno_to_error(int err); + +static inline void php_poll_set_errno_error(php_poll_ctx *ctx, int err) +{ + ctx->last_error = php_poll_errno_to_error(err); +} + +static inline void php_poll_set_current_errno_error(php_poll_ctx *ctx) +{ + php_poll_set_errno_error(ctx, errno); +} + +static inline bool php_poll_is_not_found_error(void) +{ + return errno == ENOENT; +} + +static inline void php_poll_set_error(php_poll_ctx *ctx, php_poll_error error) +{ + ctx->last_error = error; +} + +#endif /* PHP_POLL_INTERNAL_H */ diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c new file mode 100644 index 0000000000000..5e523e41f4bb3 --- /dev/null +++ b/main/poll/poll_backend_epoll.c @@ -0,0 +1,240 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll_internal.h" + +#ifdef HAVE_EPOLL + +#include + +typedef struct { + int epoll_fd; + struct epoll_event *events; + int events_capacity; + int fd_count; +} epoll_backend_data_t; + +static uint32_t epoll_events_to_native(uint32_t events) +{ + uint32_t native = 0; + if (events & PHP_POLL_READ) { + native |= EPOLLIN; + } + if (events & PHP_POLL_WRITE) { + native |= EPOLLOUT; + } + if (events & PHP_POLL_ERROR) { + native |= EPOLLERR; + } + if (events & PHP_POLL_HUP) { + native |= EPOLLHUP; + } + if (events & PHP_POLL_RDHUP) { + native |= EPOLLRDHUP; + } + if (events & PHP_POLL_ONESHOT) { + native |= EPOLLONESHOT; + } + if (events & PHP_POLL_ET) { + native |= EPOLLET; + } + return native; +} + +static uint32_t epoll_events_from_native(uint32_t native) +{ + uint32_t events = 0; + if (native & EPOLLIN) { + events |= PHP_POLL_READ; + } + if (native & EPOLLOUT) { + events |= PHP_POLL_WRITE; + } + if (native & EPOLLERR) { + events |= PHP_POLL_ERROR; + } + if (native & EPOLLHUP) { + events |= PHP_POLL_HUP; + } + if (native & EPOLLRDHUP) { + events |= PHP_POLL_RDHUP; + } + return events; +} + +static zend_result epoll_backend_init(php_poll_ctx *ctx) +{ + epoll_backend_data_t *data = php_poll_calloc(1, sizeof(epoll_backend_data_t), ctx->persistent); + if (!data) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + data->epoll_fd = epoll_create1(EPOLL_CLOEXEC); + if (data->epoll_fd == -1) { + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; + } + + /* Use hint for initial allocation if provided, otherwise start with reasonable default */ + int initial_capacity = ctx->max_events_hint > 0 ? ctx->max_events_hint : 64; + data->events = php_poll_calloc(initial_capacity, sizeof(struct epoll_event), ctx->persistent); + if (!data->events) { + close(data->epoll_fd); + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + data->events_capacity = initial_capacity; + + ctx->backend_data = data; + return SUCCESS; +} + +static void epoll_backend_cleanup(php_poll_ctx *ctx) +{ + epoll_backend_data_t *data = (epoll_backend_data_t *) ctx->backend_data; + if (data) { + if (data->epoll_fd >= 0) { + close(data->epoll_fd); + } + pefree(data->events, ctx->persistent); + pefree(data, ctx->persistent); + ctx->backend_data = NULL; + } +} + +static zend_result epoll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + epoll_backend_data_t *backend_data = (epoll_backend_data_t *) ctx->backend_data; + + struct epoll_event ev = { 0 }; + ev.events = epoll_events_to_native(events); + ev.data.ptr = data; + + if (epoll_ctl(backend_data->epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) { + php_poll_set_error(ctx, (errno == EEXIST) ? PHP_POLL_ERR_EXISTS : PHP_POLL_ERR_SYSTEM); + return FAILURE; + } + backend_data->fd_count++; + + return SUCCESS; +} + +static zend_result epoll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + epoll_backend_data_t *backend_data = (epoll_backend_data_t *) ctx->backend_data; + + struct epoll_event ev = { 0 }; + ev.events = epoll_events_to_native(events); + ev.data.ptr = data; + + if (epoll_ctl(backend_data->epoll_fd, EPOLL_CTL_MOD, fd, &ev) == -1) { + php_poll_set_error(ctx, (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERR_SYSTEM); + return FAILURE; + } + + return SUCCESS; +} + +static zend_result epoll_backend_remove(php_poll_ctx *ctx, int fd) +{ + epoll_backend_data_t *backend_data = (epoll_backend_data_t *) ctx->backend_data; + + if (epoll_ctl(backend_data->epoll_fd, EPOLL_CTL_DEL, fd, NULL) == -1) { + php_poll_set_error(ctx, (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERR_SYSTEM); + return FAILURE; + } + backend_data->fd_count--; + + return SUCCESS; +} + +static int epoll_backend_wait( + php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) +{ + epoll_backend_data_t *backend_data = (epoll_backend_data_t *) ctx->backend_data; + + /* Ensure we have enough space for the requested events */ + if (max_events > backend_data->events_capacity) { + struct epoll_event *new_events = php_poll_realloc( + backend_data->events, max_events * sizeof(struct epoll_event), ctx->persistent); + if (!new_events) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return -1; + } + backend_data->events = new_events; + backend_data->events_capacity = max_events; + } + + int nfds = epoll_wait(backend_data->epoll_fd, backend_data->events, max_events, timeout); + + if (nfds > 0) { + for (int i = 0; i < nfds; i++) { + events[i].fd = backend_data->events[i].data.fd; + events[i].events = 0; /* Not used in results */ + events[i].revents = epoll_events_from_native(backend_data->events[i].events); + events[i].data = backend_data->events[i].data.ptr; + } + } + + return nfds; +} + +static int epoll_backend_get_suitable_max_events(php_poll_ctx *ctx) +{ + epoll_backend_data_t *backend_data = (epoll_backend_data_t *) ctx->backend_data; + + if (!backend_data) { + return -1; + } + + /* For epoll, we now track exactly how many FDs are registered */ + int active_fds = backend_data->fd_count; + + if (active_fds == 0) { + return 1; + } + + /* Epoll can return exactly one event per registered FD, + * so the suitable max_events is exactly the number of registered FDs */ + return active_fds; +} + +static bool epoll_backend_is_available(void) +{ + int fd = epoll_create1(EPOLL_CLOEXEC); + if (fd >= 0) { + close(fd); + return true; + } + return false; +} + +const php_poll_backend_ops php_poll_backend_epoll_ops = { + .type = PHP_POLL_BACKEND_EPOLL, + .name = "epoll", + .init = epoll_backend_init, + .cleanup = epoll_backend_cleanup, + .add = epoll_backend_add, + .modify = epoll_backend_modify, + .remove = epoll_backend_remove, + .wait = epoll_backend_wait, + .is_available = epoll_backend_is_available, + .get_suitable_max_events = epoll_backend_get_suitable_max_events, + .supports_et = true, +}; + +#endif /* HAVE_EPOLL */ diff --git a/main/poll/poll_backend_eventport.c b/main/poll/poll_backend_eventport.c new file mode 100644 index 0000000000000..26ff374f9dab1 --- /dev/null +++ b/main/poll/poll_backend_eventport.c @@ -0,0 +1,407 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll_internal.h" + +#ifdef HAVE_EVENT_PORTS + +#include +#include +#include +#include +#include + +typedef struct { + int port_fd; + port_event_t *events; + int events_capacity; + int active_associations; + php_poll_fd_table *fd_table; +} eventport_backend_data_t; + +/* Convert our event flags to event port flags */ +static int eventport_events_to_native(uint32_t events) +{ + int native = 0; + if (events & PHP_POLL_READ) { + native |= POLLIN; + } + if (events & PHP_POLL_WRITE) { + native |= POLLOUT; + } + if (events & PHP_POLL_ERROR) { + native |= POLLERR; + } + if (events & PHP_POLL_HUP) { + native |= POLLHUP; + } + if (events & PHP_POLL_RDHUP) { + native |= POLLHUP; /* Map RDHUP to HUP */ + } + return native; +} + +/* Convert event port flags back to our event flags */ +static uint32_t eventport_events_from_native(int native) +{ + uint32_t events = 0; + if (native & POLLIN) { + events |= PHP_POLL_READ; + } + if (native & POLLOUT) { + events |= PHP_POLL_WRITE; + } + if (native & POLLERR) { + events |= PHP_POLL_ERROR; + } + if (native & POLLHUP) { + events |= PHP_POLL_HUP; + } + if (native & POLLNVAL) { + events |= PHP_POLL_ERROR; + } + return events; +} + +/* Initialize event port backend */ +static zend_result eventport_backend_init(php_poll_ctx *ctx) +{ + eventport_backend_data_t *data + = php_poll_calloc(1, sizeof(eventport_backend_data_t), ctx->persistent); + if (!data) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + /* Create event port */ + data->port_fd = port_create(); + if (data->port_fd == -1) { + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; + } + + data->active_associations = 0; + + /* Use hint for initial allocation if provided, otherwise start with reasonable default */ + int initial_capacity = ctx->max_events_hint > 0 ? ctx->max_events_hint : 64; + data->events = php_poll_calloc(initial_capacity, sizeof(port_event_t), ctx->persistent); + if (!data->events) { + close(data->port_fd); + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + data->events_capacity = initial_capacity; + + /* Initialize FD tracking using helper */ + data->fd_table = php_poll_fd_table_init(initial_capacity, ctx->persistent); + if (!data->fd_table) { + close(data->port_fd); + pefree(data->events, ctx->persistent); + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + ctx->backend_data = data; + return SUCCESS; +} + +/* Cleanup event port backend */ +static void eventport_backend_cleanup(php_poll_ctx *ctx) +{ + eventport_backend_data_t *data = (eventport_backend_data_t *) ctx->backend_data; + if (data) { + if (data->port_fd >= 0) { + close(data->port_fd); + } + pefree(data->events, ctx->persistent); + php_poll_fd_table_cleanup(data->fd_table); + pefree(data, ctx->persistent); + ctx->backend_data = NULL; + } +} + +/* Add file descriptor to event port */ +static zend_result eventport_backend_add( + php_poll_ctx *ctx, int fd, uint32_t events, void *user_data) +{ + eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; + + if (php_poll_fd_table_find(backend_data->fd_table, fd)) { + php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); + return FAILURE; + } + + php_poll_fd_entry *entry = php_poll_fd_table_get(backend_data->fd_table, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + entry->events = events; + entry->data = user_data; + + int native_events = eventport_events_to_native(events); + + /* Associate file descriptor with event port */ + if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, user_data) == -1) { + php_poll_fd_table_remove(backend_data->fd_table, fd); + switch (errno) { + case EEXIST: + php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); + break; + case ENOMEM: + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + break; + case EBADF: + case EINVAL: + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; + default: + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; + } + return FAILURE; + } + + backend_data->active_associations++; + return SUCCESS; +} + +/* Modify file descriptor in event port */ +static zend_result eventport_backend_modify( + php_poll_ctx *ctx, int fd, uint32_t events, void *user_data) +{ + eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; + + php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + /* Update entry */ + entry->events = events; + entry->data = user_data; + + /* For event ports, we need to dissociate and re-associate */ + /* Note: dissociate might fail if the fd was already fired and auto-dissociated */ + port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd); + + int native_events = eventport_events_to_native(events); + if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, user_data) == -1) { + switch (errno) { + case ENOMEM: + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + break; + case EBADF: + case EINVAL: + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; + default: + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; + } + return FAILURE; + } + + return SUCCESS; +} + +/* Remove file descriptor from event port */ +static zend_result eventport_backend_remove(php_poll_ctx *ctx, int fd) +{ + eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; + + /* Check if exists using helper */ + if (!php_poll_fd_table_find(backend_data->fd_table, fd)) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + if (port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd) == -1) { + /* Only fail if it's not ENOENT (might already be dissociated) */ + if (!php_poll_is_not_found_error()) { + php_poll_set_current_errno_error(ctx); + return FAILURE; + } + } + + php_poll_fd_table_remove(backend_data->fd_table, fd); + backend_data->active_associations--; + return SUCCESS; +} + +/* Handle re-association after event */ +static void eventport_handle_reassociation( + eventport_backend_data_t *backend_data, int fd, uint32_t fired_events) +{ + php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, fd); + if (!entry) { + return; + } + + if (entry->events & PHP_POLL_ONESHOT) { + /* Oneshot: remove from tracking */ + php_poll_fd_table_remove(backend_data->fd_table, fd); + backend_data->active_associations--; + return; + } + + /* Determine which events to re-associate with */ + uint32_t reassoc_events = entry->events; + if (entry->events & PHP_POLL_ET) { + /* Edge-triggered: don't re-associate with events that just fired */ + reassoc_events &= ~fired_events; + reassoc_events &= ~PHP_POLL_ET; /* Remove ET flag for port_associate */ + } + + if (reassoc_events != 0) { + /* Re-associate for continued monitoring */ + int native_events = eventport_events_to_native(reassoc_events); + if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, entry->data) + != 0) { + /* Re-association failed - might be due to fd being closed */ + php_poll_fd_table_remove(backend_data->fd_table, fd); + backend_data->active_associations--; + } + } else { + /* No events to re-associate with */ + php_poll_fd_table_remove(backend_data->fd_table, fd); + backend_data->active_associations--; + } +} + +/* Wait for events using event port */ +static int eventport_backend_wait( + php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) +{ + eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; + + if (backend_data->active_associations == 0) { + /* No active associations, but we still need to respect timeout */ + if (timeout > 0) { + struct timespec ts; + ts.tv_sec = timeout / 1000; + ts.tv_nsec = (timeout % 1000) * 1000000; + nanosleep(&ts, NULL); + } + return 0; + } + + /* Ensure we have enough space for the requested events */ + if (max_events > backend_data->events_capacity) { + port_event_t *new_events = php_poll_realloc( + backend_data->events, max_events * sizeof(port_event_t), ctx->persistent); + if (!new_events) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return -1; + } + backend_data->events = new_events; + backend_data->events_capacity = max_events; + } + + /* Setup timeout structure */ + struct timespec ts = { 0 }, *tsp = NULL; + if (timeout >= 0) { + ts.tv_sec = timeout / 1000; + ts.tv_nsec = (timeout % 1000) * 1000000; + tsp = &ts; + } + + /* Retrieve events from port */ + uint_t nget = 1; /* We want to get multiple events if available */ + int result = port_getn(backend_data->port_fd, backend_data->events, max_events, &nget, tsp); + + if (result == -1) { + php_poll_set_current_errno_error(ctx); + } + + int nfds = (int) nget; + + /* Process the events and handle re-association */ + for (int i = 0; i < nfds; i++) { + port_event_t *port_event = &backend_data->events[i]; + + /* Only handle PORT_SOURCE_FD events */ + if (port_event->portev_source == PORT_SOURCE_FD) { + int fd = (int) port_event->portev_object; + events[i].fd = fd; + events[i].events = 0; /* Not used in results */ + events[i].revents = eventport_events_from_native(port_event->portev_events); + events[i].data = port_event->portev_user; + + /* Handle re-association based on event type */ + eventport_handle_reassociation(backend_data, fd, events[i].revents); + } else { + /* Handle other event sources if needed (timers, user events, etc.) */ + events[i].fd = -1; + events[i].events = 0; + events[i].revents = 0; + events[i].data = port_event->portev_user; + } + } + + return nfds; +} + +/* Check if event port backend is available */ +static bool eventport_backend_is_available(void) +{ + int fd = port_create(); + if (fd >= 0) { + close(fd); + return true; + } + return false; +} + +static int eventport_backend_get_suitable_max_events(php_poll_ctx *ctx) +{ + eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; + + if (!backend_data || !backend_data->fd_table) { + return -1; + } + + /* For event ports, we track exactly how many FD associations are active */ + int active_associations = backend_data->active_associations; + + if (active_associations == 0) { + return 1; + } + + /* Event ports can return exactly one event per association, + * so the suitable max_events is exactly the number of active associations */ + return active_associations; +} + +/* Event port backend operations structure */ +const php_poll_backend_ops php_poll_backend_eventport_ops = { + .type = PHP_POLL_BACKEND_EVENTPORT, + .name = "eventport", + .init = eventport_backend_init, + .cleanup = eventport_backend_cleanup, + .add = eventport_backend_add, + .modify = eventport_backend_modify, + .remove = eventport_backend_remove, + .wait = eventport_backend_wait, + .is_available = eventport_backend_is_available, + .get_suitable_max_events = eventport_backend_get_suitable_max_events, + .supports_et = true /* Supports both level and edge triggering */ +}; + +#endif /* HAVE_EVENT_PORTS */ diff --git a/main/poll/poll_backend_iocp.c b/main/poll/poll_backend_iocp.c new file mode 100644 index 0000000000000..164cb883c23ac --- /dev/null +++ b/main/poll/poll_backend_iocp.c @@ -0,0 +1,394 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll_internal.h" + +#ifdef PHP_WIN32 + +#include +#include +#include + +typedef struct iocp_operation { + OVERLAPPED overlapped; + int fd; + uint32_t events; + void *user_data; + bool active; + char buffer[1]; /* Minimal buffer for accept/recv operations */ +} iocp_operation_t; + +typedef struct { + int fd; + uint32_t events; + void *data; + bool active; + bool associated; /* Whether socket is associated with IOCP */ +} iocp_fd_entry; + +typedef struct { + HANDLE iocp_handle; + iocp_operation_t *operations; + int operations_capacity; + int operation_count; + + /* FD tracking */ + iocp_fd_entry *fd_entries; + int fd_entries_capacity; + int fd_count; + + LPFN_ACCEPTEX AcceptEx; + LPFN_CONNECTEX ConnectEx; + LPFN_GETACCEPTEXSOCKADDRS GetAcceptExSockaddrs; +} iocp_backend_data_t; + +/* Find FD entry */ +static iocp_fd_entry *iocp_find_fd_entry(iocp_backend_data_t *data, int fd) +{ + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (data->fd_entries[i].active && data->fd_entries[i].fd == fd) { + return &data->fd_entries[i]; + } + } + return NULL; +} + +/* Get or create FD entry */ +static iocp_fd_entry *iocp_get_fd_entry(iocp_backend_data_t *data, int fd, bool persistent) +{ + iocp_fd_entry *entry = iocp_find_fd_entry(data, fd); + if (entry) { + return entry; + } + + /* Find empty slot */ + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (!data->fd_entries[i].active) { + data->fd_entries[i].fd = fd; + data->fd_entries[i].active = true; + data->fd_entries[i].associated = false; + data->fd_count++; + return &data->fd_entries[i]; + } + } + + /* Need to grow the array */ + int new_capacity = data->fd_entries_capacity ? data->fd_entries_capacity * 2 : 64; + iocp_fd_entry *new_entries + = php_poll_realloc(data->fd_entries, new_capacity * sizeof(iocp_fd_entry), persistent); + if (!new_entries) { + return NULL; + } + + /* Initialize new entries */ + memset(new_entries + data->fd_entries_capacity, 0, + (new_capacity - data->fd_entries_capacity) * sizeof(iocp_fd_entry)); + + data->fd_entries = new_entries; + + /* Use first new slot */ + iocp_fd_entry *new_entry = &data->fd_entries[data->fd_entries_capacity]; + new_entry->fd = fd; + new_entry->active = true; + new_entry->associated = false; + data->fd_count++; + + data->fd_entries_capacity = new_capacity; + return new_entry; +} + +/* Remove FD entry */ +static void iocp_remove_fd_entry(iocp_backend_data_t *data, int fd) +{ + iocp_fd_entry *entry = iocp_find_fd_entry(data, fd); + if (entry) { + entry->active = false; + entry->associated = false; + data->fd_count--; + } +} + +static zend_result iocp_backend_init(php_poll_ctx *ctx) +{ + iocp_backend_data_t *data = php_poll_calloc(1, sizeof(iocp_backend_data_t), ctx->persistent); + if (!data) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + /* Create I/O Completion Port */ + data->iocp_handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); + if (data->iocp_handle == NULL) { + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; + } + + /* Use hint for initial allocation if provided, otherwise start with reasonable default */ + int initial_capacity = ctx->max_events_hint > 0 ? ctx->max_events_hint : 64; + + data->operations = php_poll_calloc(initial_capacity, sizeof(iocp_operation_t), ctx->persistent); + if (!data->operations) { + CloseHandle(data->iocp_handle); + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + data->operations_capacity = initial_capacity; + data->operation_count = 0; + + /* Initialize FD tracking array */ + data->fd_entries = php_poll_calloc(initial_capacity, sizeof(iocp_fd_entry), ctx->persistent); + if (!data->fd_entries) { + CloseHandle(data->iocp_handle); + pefree(data->operations, ctx->persistent); + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + data->fd_entries_capacity = initial_capacity; + data->fd_count = 0; + + /* Load Winsock extension functions */ + SOCKET dummy_socket = socket(AF_INET, SOCK_STREAM, 0); + if (dummy_socket != INVALID_SOCKET) { + GUID acceptex_guid = WSAID_ACCEPTEX; + GUID connectex_guid = WSAID_CONNECTEX; + GUID getacceptexsockaddrs_guid = WSAID_GETACCEPTEXSOCKADDRS; + DWORD bytes; + + WSAIoctl(dummy_socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &acceptex_guid, + sizeof(acceptex_guid), &data->AcceptEx, sizeof(data->AcceptEx), &bytes, NULL, NULL); + + WSAIoctl(dummy_socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &connectex_guid, + sizeof(connectex_guid), &data->ConnectEx, sizeof(data->ConnectEx), &bytes, NULL, + NULL); + + WSAIoctl(dummy_socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &getacceptexsockaddrs_guid, + sizeof(getacceptexsockaddrs_guid), &data->GetAcceptExSockaddrs, + sizeof(data->GetAcceptExSockaddrs), &bytes, NULL, NULL); + + closesocket(dummy_socket); + } + + ctx->backend_data = data; + return SUCCESS; +} + +static void iocp_backend_cleanup(php_poll_ctx *ctx) +{ + iocp_backend_data_t *data = (iocp_backend_data_t *) ctx->backend_data; + if (data) { + if (data->iocp_handle != NULL) { + CloseHandle(data->iocp_handle); + } + pefree(data->operations, ctx->persistent); + pefree(data->fd_entries, ctx->persistent); + pefree(data, ctx->persistent); + ctx->backend_data = NULL; + } +} + +static zend_result iocp_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + SOCKET sock = (SOCKET) fd; + + /* Check if FD already exists */ + if (iocp_find_fd_entry(backend_data, fd)) { + php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); + return FAILURE; + } + + /* Get FD entry for tracking */ + iocp_fd_entry *entry = iocp_get_fd_entry(backend_data, fd, ctx->persistent); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + entry->events = events; + entry->data = data; + + /* Associate socket with completion port if not already done */ + if (!entry->associated) { + HANDLE result = CreateIoCompletionPort( + (HANDLE) sock, backend_data->iocp_handle, (ULONG_PTR) sock, 0); + if (result == NULL) { + iocp_remove_fd_entry(backend_data, fd); + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; + } + entry->associated = true; + } + + /* Note: IOCP operations are typically initiated on-demand rather than pre-posted. + * For a polling API, we would need to simulate readiness using techniques like: + * - Zero-byte reads to check readability + * - Connect() to localhost to check writability + * This is complex and IOCP is better suited for async I/O rather than polling. + * For now, we just track the association. */ + + return SUCCESS; +} + +static zend_result iocp_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + + /* Find existing entry */ + iocp_fd_entry *entry = iocp_find_fd_entry(backend_data, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + /* Update entry */ + entry->events = events; + entry->data = data; + + return SUCCESS; +} + +static zend_result iocp_backend_remove(php_poll_ctx *ctx, int fd) +{ + iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + SOCKET sock = (SOCKET) fd; + + /* Find existing entry */ + iocp_fd_entry *entry = iocp_find_fd_entry(backend_data, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + /* Cancel all I/O operations on this socket */ + CancelIo((HANDLE) sock); + + /* Remove from tracking */ + iocp_remove_fd_entry(backend_data, fd); + + return SUCCESS; +} + +static int iocp_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) +{ + iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + + /* IOCP is fundamentally different from other polling mechanisms. + * It's completion-based rather than readiness-based. + * A proper implementation would need to: + * 1. Post overlapped I/O operations for all registered FDs + * 2. Wait for completions + * 3. Return the completed operations as events + * + * This is complex and doesn't map well to a traditional polling API. + * For now, we provide a minimal implementation that can detect some completions. */ + + if (backend_data->fd_count == 0) { + /* No FDs to monitor, but respect timeout */ + if (timeout > 0) { + Sleep(timeout); + } + return 0; + } + + DWORD bytes_transferred; + ULONG_PTR completion_key; + LPOVERLAPPED overlapped; + + BOOL result = GetQueuedCompletionStatus(backend_data->iocp_handle, &bytes_transferred, + &completion_key, &overlapped, (timeout < 0) ? INFINITE : timeout); + + if (!result && overlapped == NULL) { + /* Timeout or error */ + DWORD error = GetLastError(); + if (error == WAIT_TIMEOUT) { + return 0; + } else { + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return -1; + } + } + + if (overlapped != NULL) { + /* We got a completion, but since we're not posting operations in add(), + * this would only happen if the application is doing its own overlapped I/O. + * Try to match it to one of our tracked FDs. */ + + SOCKET completed_socket = (SOCKET) completion_key; + iocp_fd_entry *entry = iocp_find_fd_entry(backend_data, (int) completed_socket); + + if (entry && max_events > 0) { + events[0].fd = entry->fd; + events[0].events = entry->events; + events[0].data = entry->data; + + if (result) { + /* Successful completion - determine event type */ + events[0].revents = entry->events & (PHP_POLL_READ | PHP_POLL_WRITE); + } else { + /* Error completion */ + events[0].revents = PHP_POLL_ERROR; + } + + return 1; + } + } + + return 0; +} + +static bool iocp_backend_is_available(void) +{ + /* IOCP is available on Windows NT and later (basically all modern Windows) */ + return true; +} + +static int iocp_backend_get_suitable_max_events(php_poll_ctx *ctx) +{ + iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + + if (!backend_data) { + return -1; + } + + /* For IOCP, we track exactly how many FDs are registered */ + int active_fds = backend_data->fd_count; + + if (active_fds == 0) { + return 1; + } + + /* IOCP can potentially return multiple completions per socket, + * but typically it's one completion per operation. + * Since we're simulating polling behavior, use the FD count directly. */ + return active_fds; +} + +const php_poll_backend_ops php_poll_backend_iocp_ops = { + .type = PHP_POLL_BACKEND_IOCP, + .name = "iocp", + .init = iocp_backend_init, + .cleanup = iocp_backend_cleanup, + .add = iocp_backend_add, + .modify = iocp_backend_modify, + .remove = iocp_backend_remove, + .wait = iocp_backend_wait, + .is_available = iocp_backend_is_available, + .get_suitable_max_events = iocp_backend_get_suitable_max_events, + .supports_et = true /* IOCP provides completion-based model which is naturally edge-triggered */ +}; + +#endif /* PHP_WIN32 */ diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c new file mode 100644 index 0000000000000..6939972cb36da --- /dev/null +++ b/main/poll/poll_backend_kqueue.c @@ -0,0 +1,399 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll_internal.h" + +#ifdef HAVE_KQUEUE + +#include +#include +#include + +typedef struct { + int kqueue_fd; + struct kevent *events; + int events_capacity; + int fd_count; /* Track number of unique FDs (not individual filters) */ + HashTable *complete_oneshot_fds; /* Track FDs with both read+write oneshot */ + HashTable *garbage_oneshot_fds; /* Pre-cached hash table for FDs to delete */ +} kqueue_backend_data_t; + +static zend_result kqueue_backend_init(php_poll_ctx *ctx) +{ + kqueue_backend_data_t *data + = php_poll_calloc(1, sizeof(kqueue_backend_data_t), ctx->persistent); + if (!data) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + data->kqueue_fd = kqueue(); + if (data->kqueue_fd == -1) { + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; + } + + /* Use hint for initial allocation if provided, otherwise start with reasonable default */ + int initial_capacity = ctx->max_events_hint > 0 ? ctx->max_events_hint : 64; + data->events = php_poll_calloc(initial_capacity, sizeof(struct kevent), ctx->persistent); + if (!data->events) { + close(data->kqueue_fd); + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + data->events_capacity = initial_capacity; + data->fd_count = 0; /* Initialize FD counter */ + + /* Initialize oneshot related hash tables */ + data->complete_oneshot_fds = php_poll_malloc(sizeof(HashTable), ctx->persistent); + zend_hash_init(data->complete_oneshot_fds, 8, NULL, NULL, ctx->persistent); + data->garbage_oneshot_fds = php_poll_malloc(sizeof(HashTable), ctx->persistent); + zend_hash_init(data->garbage_oneshot_fds, 8, NULL, NULL, ctx->persistent); + + ctx->backend_data = data; + return SUCCESS; +} + +static void kqueue_backend_cleanup(php_poll_ctx *ctx) +{ + kqueue_backend_data_t *data = (kqueue_backend_data_t *) ctx->backend_data; + if (data) { + if (data->kqueue_fd >= 0) { + close(data->kqueue_fd); + } + pefree(data->events, ctx->persistent); + zend_hash_destroy(data->complete_oneshot_fds); + pefree(data->complete_oneshot_fds, ctx->persistent); + zend_hash_destroy(data->garbage_oneshot_fds); + pefree(data->garbage_oneshot_fds, ctx->persistent); + pefree(data, ctx->persistent); + ctx->backend_data = NULL; + } +} + +static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + + struct kevent changes[2]; /* Max 2 changes: read + write */ + int change_count = 0; + + uint16_t flags = EV_ADD | EV_ENABLE; + if (events & PHP_POLL_ONESHOT) { + flags |= EV_ONESHOT; + } + if (events & PHP_POLL_ET) { + flags |= EV_CLEAR; + } + + if (events & PHP_POLL_READ) { + EV_SET(&changes[change_count], fd, EVFILT_READ, flags, 0, 0, data); + change_count++; + } + + if (events & PHP_POLL_WRITE) { + EV_SET(&changes[change_count], fd, EVFILT_WRITE, flags, 0, 0, data); + change_count++; + } + + if (change_count > 0) { + int result = kevent(backend_data->kqueue_fd, changes, change_count, NULL, 0, NULL); + if (result == -1) { + php_poll_set_current_errno_error(ctx); + return FAILURE; + } + + /* Increment FD count only once per unique FD */ + backend_data->fd_count++; + + /* Track if this FD has both read+write oneshot */ + if ((events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT)) + == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT)) { + zend_hash_index_add_empty_element(backend_data->complete_oneshot_fds, fd); + } + } + + return SUCCESS; +} + +static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + + struct kevent deletes[2]; + struct kevent adds[2]; + int delete_count = 0; + int add_count = 0; + int successful_deletes = 0; + + uint16_t add_flags = EV_ADD | EV_ENABLE; + if (events & PHP_POLL_ONESHOT) { + add_flags |= EV_ONESHOT; + } + if (events & PHP_POLL_ET) { + add_flags |= EV_CLEAR; + } + + /* Delete existing filters that are not in the new events */ + if (!(events & PHP_POLL_READ)) { + EV_SET(&deletes[delete_count], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); + delete_count++; + } + if (!(events & PHP_POLL_WRITE)) { + EV_SET(&deletes[delete_count], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); + delete_count++; + } + + /* Prepare add operations for requested events */ + if (events & PHP_POLL_READ) { + EV_SET(&adds[add_count], fd, EVFILT_READ, add_flags, 0, 0, data); + add_count++; + } + if (events & PHP_POLL_WRITE) { + EV_SET(&adds[add_count], fd, EVFILT_WRITE, add_flags, 0, 0, data); + add_count++; + } + + /* Delete existing filters individually to count successes */ + for (int i = 0; i < delete_count; i++) { + int result = kevent(backend_data->kqueue_fd, &deletes[i], 1, NULL, 0, NULL); + if (result == 0) { + successful_deletes++; + } else if (!php_poll_is_not_found_error()) { + php_poll_set_current_errno_error(ctx); + return FAILURE; + } + /* ENOENT is ignored - filter didn't exist */ + } + + /* Add new filters */ + if (add_count > 0) { + int result = kevent(backend_data->kqueue_fd, adds, add_count, NULL, 0, NULL); + if (result == -1) { + php_poll_set_current_errno_error(ctx); + return FAILURE; + } + } + + /* Update FD count and oneshot tracking */ + if (successful_deletes > 0 && add_count == 0) { + /* Removed all filters - FD is gone */ + backend_data->fd_count--; + zend_hash_index_del(backend_data->complete_oneshot_fds, fd); + } else if (successful_deletes == 0 && add_count > 0) { + /* Added filters to previously empty FD */ + backend_data->fd_count++; + if ((events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT)) + == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT)) { + zend_hash_index_add_empty_element(backend_data->complete_oneshot_fds, fd); + } + } else if (successful_deletes > 0 || add_count > 0) { + /* One of the filter was deleted so remove from oneshot tracking */ + zend_hash_index_del(backend_data->complete_oneshot_fds, fd); + } + + return SUCCESS; +} + +static zend_result kqueue_backend_remove(php_poll_ctx *ctx, int fd) +{ + kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + struct kevent change; + int successful_deletes = 0; + + /* Try to remove read filter */ + EV_SET(&change, fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); + int result = kevent(backend_data->kqueue_fd, &change, 1, NULL, 0, NULL); + if (result == 0) { + successful_deletes++; + } else if (!php_poll_is_not_found_error()) { + php_poll_set_current_errno_error(ctx); + return FAILURE; + } + + /* Try to remove write filter */ + EV_SET(&change, fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); + result = kevent(backend_data->kqueue_fd, &change, 1, NULL, 0, NULL); + if (result == 0) { + successful_deletes++; + } else if (!php_poll_is_not_found_error()) { + php_poll_set_current_errno_error(ctx); + return FAILURE; + } + + /* If no filters were successfully deleted, that's an error */ + if (successful_deletes == 0) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + /* Update FD count - we removed all filters for this FD */ + backend_data->fd_count--; + + /* Remove from complete oneshot tracking */ + zend_hash_index_del(backend_data->complete_oneshot_fds, fd); + + return SUCCESS; +} + +static int kqueue_backend_wait( + php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) +{ + kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + + /* Ensure we have enough space for the requested events as kqueue can return up to 2 raw events + * per FD (read + write), we need capacity for potentially 2x max_events. */ + int required_capacity = max_events * 2; + if (required_capacity > backend_data->events_capacity) { + struct kevent *new_events = php_poll_realloc( + backend_data->events, required_capacity * sizeof(struct kevent), ctx->persistent); + if (!new_events) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return -1; + } + backend_data->events = new_events; + backend_data->events_capacity = required_capacity; + } + + struct timespec ts = { 0 }, *tsp = NULL; + if (timeout >= 0) { + ts.tv_sec = timeout / 1000; + ts.tv_nsec = (timeout % 1000) * 1000000; + tsp = &ts; + } + + int nfds = kevent( + backend_data->kqueue_fd, NULL, 0, backend_data->events, required_capacity, tsp); + + if (nfds > 0) { + /* Group events by FD and combine read/write events */ + int unique_events = 0, fd; + zend_hash_clean(backend_data->garbage_oneshot_fds); + + for (int i = 0; i < nfds; i++) { + fd = (int) backend_data->events[i].ident; + uint32_t revents = 0; + void *data = backend_data->events[i].udata; + bool is_oneshot = (backend_data->events[i].flags & EV_ONESHOT) != 0; + + /* Convert this event */ + if (backend_data->events[i].filter == EVFILT_READ) { + revents |= PHP_POLL_READ; + } else if (backend_data->events[i].filter == EVFILT_WRITE) { + revents |= PHP_POLL_WRITE; + } + + if (backend_data->events[i].flags & EV_EOF) { + revents |= PHP_POLL_HUP; + } + if (backend_data->events[i].flags & EV_ERROR) { + revents |= PHP_POLL_ERROR; + } + + /* Look for existing event for this FD */ + bool found = false; + for (int j = 0; j < unique_events; j++) { + if (events[j].fd == fd) { + /* Combine with existing event */ + events[j].revents |= revents; + found = true; + break; + } + } + + if (!found) { + /* New FD, create new event */ + ZEND_ASSERT(unique_events < max_events); + events[unique_events].fd = fd; + events[unique_events].events = 0; + events[unique_events].revents = revents; + events[unique_events].data = data; + unique_events++; + + if (is_oneshot && zend_hash_index_exists(backend_data->complete_oneshot_fds, fd)) { + zval dummy; + ZVAL_BOOL(&dummy, revents & PHP_POLL_READ); + zend_hash_index_add(backend_data->garbage_oneshot_fds, fd, &dummy); + zend_hash_index_del(backend_data->complete_oneshot_fds, fd); + backend_data->fd_count--; + } + } else if (is_oneshot) { + zend_hash_index_del(backend_data->garbage_oneshot_fds, fd); + } + } + + /* Clean up all the same FD filters for other read or write side */ + zval *item; + struct kevent cleanup_change; + ZEND_HASH_FOREACH_NUM_KEY_VAL(backend_data->garbage_oneshot_fds, fd, item) + { + int filter = Z_TYPE_P(item) == IS_TRUE ? EVFILT_WRITE : EVFILT_READ; + EV_SET(&cleanup_change, fd, filter, EV_DELETE, 0, 0, NULL); + kevent(backend_data->kqueue_fd, &cleanup_change, 1, NULL, 0, NULL); + } + ZEND_HASH_FOREACH_END(); + + return unique_events; + } + + return nfds; +} + +static bool kqueue_backend_is_available(void) +{ + int fd = kqueue(); + if (fd >= 0) { + close(fd); + return true; + } + return false; +} + +static int kqueue_backend_get_suitable_max_events(php_poll_ctx *ctx) +{ + kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + + if (!backend_data) { + return -1; + } + + /* For kqueue, we now track exactly how many unique FDs are registered */ + int active_fds = backend_data->fd_count; + + if (active_fds == 0) { + return 1; + } + + /* Kqueue backend will return one grouped event per FD (like epoll), + * so the suitable max_events is exactly the number of registered FDs */ + return active_fds; +} + +const php_poll_backend_ops php_poll_backend_kqueue_ops = { + .type = PHP_POLL_BACKEND_KQUEUE, + .name = "kqueue", + .init = kqueue_backend_init, + .cleanup = kqueue_backend_cleanup, + .add = kqueue_backend_add, + .modify = kqueue_backend_modify, + .remove = kqueue_backend_remove, + .wait = kqueue_backend_wait, + .is_available = kqueue_backend_is_available, + .get_suitable_max_events = kqueue_backend_get_suitable_max_events, + .supports_et = true /* kqueue supports EV_CLEAR for edge triggering */ +}; + +#endif /* HAVE_KQUEUE */ diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c new file mode 100644 index 0000000000000..e8fcef5af72c7 --- /dev/null +++ b/main/poll/poll_backend_poll.c @@ -0,0 +1,297 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll_internal.h" + +typedef struct { + php_poll_fd_table *fd_table; + php_pollfd *temp_fds; + int temp_fds_capacity; +} poll_backend_data_t; + +static uint32_t poll_events_to_native(uint32_t events) +{ + uint32_t native = 0; + if (events & PHP_POLL_READ) { + native |= POLLIN; + } + if (events & PHP_POLL_WRITE) { + native |= POLLOUT; + } + if (events & PHP_POLL_ERROR) { + native |= POLLERR; + } + if (events & PHP_POLL_HUP) { + native |= POLLHUP; + } + return native; +} + +static uint32_t poll_events_from_native(uint32_t native) +{ + uint32_t events = 0; + if (native & POLLIN) { + events |= PHP_POLL_READ; + } + if (native & POLLOUT) { + events |= PHP_POLL_WRITE; + } + if (native & POLLERR) { + events |= PHP_POLL_ERROR; + } + if (native & POLLHUP) { + events |= PHP_POLL_HUP; + } + if (native & POLLNVAL) { + events |= PHP_POLL_ERROR; /* Map invalid FD to error */ + } + return events; +} + +static zend_result poll_backend_init(php_poll_ctx *ctx) +{ + poll_backend_data_t *data = php_poll_calloc(1, sizeof(poll_backend_data_t), ctx->persistent); + if (!data) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + int initial_capacity = ctx->max_events_hint > 0 ? ctx->max_events_hint : 64; + + data->fd_table = php_poll_fd_table_init(initial_capacity, ctx->persistent); + if (!data->fd_table) { + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + data->temp_fds = php_poll_calloc(initial_capacity, sizeof(php_pollfd), ctx->persistent); + if (!data->temp_fds) { + php_poll_fd_table_cleanup(data->fd_table); + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + data->temp_fds_capacity = initial_capacity; + + ctx->backend_data = data; + return SUCCESS; +} + +static void poll_backend_cleanup(php_poll_ctx *ctx) +{ + poll_backend_data_t *data = (poll_backend_data_t *) ctx->backend_data; + if (data) { + php_poll_fd_table_cleanup(data->fd_table); + pefree(data->temp_fds, ctx->persistent); + pefree(data, ctx->persistent); + ctx->backend_data = NULL; + } +} + +static zend_result poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *user_data) +{ + poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; + + if (events & PHP_POLL_ET) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOSUPPORT); + return FAILURE; + } + + if (php_poll_fd_table_find(backend_data->fd_table, fd)) { + php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); + return FAILURE; + } + + php_poll_fd_entry *entry = php_poll_fd_table_get(backend_data->fd_table, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + entry->events = events; + entry->data = user_data; + + return SUCCESS; +} + +static zend_result poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *user_data) +{ + poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; + + if (events & PHP_POLL_ET) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOSUPPORT); + return FAILURE; + } + + php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + entry->events = events; + entry->data = user_data; + + return SUCCESS; +} + +static zend_result poll_backend_remove(php_poll_ctx *ctx, int fd) +{ + poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; + + if (!php_poll_fd_table_find(backend_data->fd_table, fd)) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + php_poll_fd_table_remove(backend_data->fd_table, fd); + return SUCCESS; +} + +/* Context for building php_pollfd array */ +typedef struct { + php_pollfd *fds; + int index; +} poll_build_context; + +/* Callback to build php_pollfd array from fd_table */ +static bool poll_build_fds_callback(int fd, php_poll_fd_entry *entry, void *user_data) +{ + poll_build_context *ctx = (poll_build_context *) user_data; + + ctx->fds[ctx->index].fd = fd; + ctx->fds[ctx->index].events + = poll_events_to_native(entry->events & ~(PHP_POLL_ET | PHP_POLL_ONESHOT)); + ctx->fds[ctx->index].revents = 0; + ctx->index++; + + return true; +} + +static void php_poll_msleep(int timeout_ms) +{ + if (timeout_ms <= 0) { + return; + } + +#ifdef PHP_WIN32 + Sleep(timeout_ms); +#else + struct timespec ts; + ts.tv_sec = timeout_ms / 1000; + ts.tv_nsec = (timeout_ms % 1000) * 1000000; + nanosleep(&ts, NULL); +#endif +} + +static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) +{ + poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; + + int fd_count = php_poll_fd_table_count(backend_data->fd_table); + if (fd_count == 0) { + if (timeout > 0) { + php_poll_msleep(timeout); + } + return 0; + } + + /* Ensure temp_fds array is large enough */ + if (fd_count > backend_data->temp_fds_capacity) { + php_pollfd *new_fds = php_poll_realloc( + backend_data->temp_fds, fd_count * sizeof(php_pollfd), ctx->persistent); + if (!new_fds) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return -1; + } + backend_data->temp_fds = new_fds; + backend_data->temp_fds_capacity = fd_count; + } + + /* Build php_pollfd array from fd_table */ + poll_build_context build_ctx = { .fds = backend_data->temp_fds, .index = 0 }; + php_poll_fd_table_foreach(backend_data->fd_table, poll_build_fds_callback, &build_ctx); + + /* Call poll() or its emulation (Windows) */ + int nfds = php_poll2(backend_data->temp_fds, fd_count, timeout); + + if (nfds <= 0) { + return nfds; /* Return 0 for timeout, -1 for error */ + } + + /* Process results - iterate through php_pollfd array directly */ + int event_count = 0; + for (int i = 0; i < fd_count && event_count < max_events; i++) { + php_pollfd *pfd = &backend_data->temp_fds[i]; + + if (pfd->revents != 0) { + php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, pfd->fd); + if (entry) { + /* Handle POLLNVAL by automatically removing the invalid FD */ + if (pfd->revents & POLLNVAL) { + php_poll_fd_table_remove(backend_data->fd_table, pfd->fd); + continue; /* Don't report this event */ + } + + events[event_count].fd = pfd->fd; + events[event_count].events = entry->events; + events[event_count].revents = poll_events_from_native(pfd->revents); + events[event_count].data = entry->data; + event_count++; + } + } + } + + /* Handle oneshot removals */ + for (int i = 0; i < event_count; i++) { + php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, events[i].fd); + if (entry && (entry->events & PHP_POLL_ONESHOT) && events[i].revents != 0) { + php_poll_fd_table_remove(backend_data->fd_table, events[i].fd); + } + } + + return event_count; +} + +static bool poll_backend_is_available(void) +{ + return true; +} + +static int poll_backend_get_suitable_max_events(php_poll_ctx *ctx) +{ + poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; + + if (UNEXPECTED(!backend_data || !backend_data->fd_table)) { + return -1; + } + + int active_fds = php_poll_fd_table_count(backend_data->fd_table); + return active_fds == 0 ? 1 : active_fds; +} + +const php_poll_backend_ops php_poll_backend_poll_ops = { + .type = PHP_POLL_BACKEND_POLL, + .name = "poll", + .init = poll_backend_init, + .cleanup = poll_backend_cleanup, + .add = poll_backend_add, + .modify = poll_backend_modify, + .remove = poll_backend_remove, + .wait = poll_backend_wait, + .is_available = poll_backend_is_available, + .get_suitable_max_events = poll_backend_get_suitable_max_events, + .supports_et = false, +}; diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c new file mode 100644 index 0000000000000..67c739a5270ea --- /dev/null +++ b/main/poll/poll_core.c @@ -0,0 +1,398 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll_internal.h" + +/* Backend registry */ +static const php_poll_backend_ops *registered_backends[16]; +static int num_registered_backends = 0; + +/* Forward declarations for backend ops */ + +#ifdef HAVE_EPOLL +extern const php_poll_backend_ops php_poll_backend_epoll_ops; +#endif +#ifdef HAVE_KQUEUE +extern const php_poll_backend_ops php_poll_backend_kqueue_ops; +#endif +#ifdef HAVE_EVENT_PORTS +extern const php_poll_backend_ops php_poll_backend_eventport_ops; +#endif +#ifdef PHP_WIN32 +extern const php_poll_backend_ops php_poll_backend_iocp_ops; +#endif +extern const php_poll_backend_ops php_poll_backend_poll_ops; + +/* Register all available backends */ +PHPAPI void php_poll_register_backends(void) +{ + num_registered_backends = 0; + +#ifdef _WIN32 + /* IOCP is preferred on Windows for high performance */ + if (php_poll_backend_iocp_ops.is_available()) { + registered_backends[num_registered_backends++] = &php_poll_backend_iocp_ops; + } +#endif + +#ifdef HAVE_EVENT_PORTS + /* Event Ports are preferred on Solaris */ + if (php_poll_backend_eventport_ops.is_available()) { + registered_backends[num_registered_backends++] = &php_poll_backend_eventport_ops; + } +#endif + +#ifdef HAVE_KQUEUE + if (php_poll_backend_kqueue_ops.is_available()) { + registered_backends[num_registered_backends++] = &php_poll_backend_kqueue_ops; + } +#endif + +#ifdef HAVE_EPOLL + if (php_poll_backend_epoll_ops.is_available()) { + registered_backends[num_registered_backends++] = &php_poll_backend_epoll_ops; + } +#endif + + /* Poll or its emulation is always available */ + registered_backends[num_registered_backends++] = &php_poll_backend_poll_ops; +} + +/* Get backend operations */ +static const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backend) +{ + if (backend == PHP_POLL_BACKEND_AUTO) { + /* Return the first (best) available backend */ + return num_registered_backends > 0 ? registered_backends[0] : NULL; + } + + for (int i = 0; i < num_registered_backends; i++) { + if (registered_backends[i] && registered_backends[i]->type == backend) { + return registered_backends[i]; + } + } + + return NULL; +} + +/* Get backend operations by backend name */ +static const php_poll_backend_ops *php_poll_get_backend_ops_by_name(const char *backend_name) +{ + if (!backend_name) { + return NULL; + } + + for (int i = 0; i < num_registered_backends; i++) { + if (registered_backends[i] && strcmp(registered_backends[i]->name, backend_name) == 0) { + return registered_backends[i]; + } + } + + return NULL; +} + +/* Create new poll context */ +PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, bool persistent) +{ + php_poll_ctx *ctx = php_poll_calloc(1, sizeof(php_poll_ctx), persistent); + if (!ctx) { + return NULL; + } + ctx->persistent = persistent; + + /* Get backend operations */ + ctx->backend_ops = php_poll_get_backend_ops(preferred_backend); + if (!ctx->backend_ops) { + pefree(ctx, persistent); + return NULL; + } + ctx->backend_type = preferred_backend; + + return ctx; +} + +/* Create new poll context */ +PHPAPI php_poll_ctx *php_poll_create_by_name(const char *preferred_backend, bool persistent) +{ + php_poll_ctx *ctx = php_poll_calloc(1, sizeof(php_poll_ctx), persistent); + if (!ctx) { + return NULL; + } + ctx->persistent = persistent; + + /* Get backend operations */ + ctx->backend_ops = php_poll_get_backend_ops_by_name(preferred_backend); + if (!ctx->backend_ops) { + pefree(ctx, persistent); + return NULL; + } + ctx->backend_type = ctx->backend_ops->type; + + return ctx; +} + +/* Set event capacity hint (optional optimization) */ +PHPAPI zend_result php_poll_set_max_events_hint(php_poll_ctx *ctx, int max_events) +{ + if (UNEXPECTED(!ctx || max_events <= 0)) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + return FAILURE; + } + + if (UNEXPECTED(ctx->initialized)) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + return FAILURE; /* Cannot change after init */ + } + + ctx->max_events_hint = max_events; + return SUCCESS; +} + +/* Initialize poll context */ +PHPAPI zend_result php_poll_init(php_poll_ctx *ctx) +{ + if (UNEXPECTED(!ctx)) { + return FAILURE; + } + + if (UNEXPECTED(ctx->initialized)) { + return SUCCESS; + } + + /* Initialize backend - can use ctx->max_events_hint if helpful */ + if (EXPECTED(ctx->backend_ops->init(ctx) == SUCCESS)) { + ctx->initialized = true; + return SUCCESS; + } + + php_poll_set_current_errno_error(ctx); + return FAILURE; +} + +/* Destroy poll context */ +PHPAPI void php_poll_destroy(php_poll_ctx *ctx) +{ + if (!ctx) { + return; + } + + if (ctx->backend_ops && ctx->backend_ops->cleanup) { + ctx->backend_ops->cleanup(ctx); + } + + pefree(ctx, ctx->persistent); +} + +/* Add file descriptor */ +PHPAPI zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + if (UNEXPECTED(!ctx || !ctx->initialized || fd < 0)) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + return FAILURE; + } + + /* Delegate to backend - it handles all validation and tracking */ + if (EXPECTED(ctx->backend_ops->add(ctx, fd, events, data) == SUCCESS)) { + return SUCCESS; + } + + php_poll_set_current_errno_error(ctx); + return FAILURE; +} + +/* Modify file descriptor */ +PHPAPI zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + if (UNEXPECTED(!ctx || !ctx->initialized || fd < 0)) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + return FAILURE; + } + + /* Delegate to backend - it handles validation */ + if (EXPECTED(ctx->backend_ops->modify(ctx, fd, events, data) == SUCCESS)) { + return SUCCESS; + } + + php_poll_set_current_errno_error(ctx); + return FAILURE; +} + +/* Remove file descriptor */ +PHPAPI zend_result php_poll_remove(php_poll_ctx *ctx, int fd) +{ + if (UNEXPECTED(!ctx || !ctx->initialized || fd < 0)) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + return FAILURE; + } + + /* Delegate to backend - it handles validation */ + if (EXPECTED(ctx->backend_ops->remove(ctx, fd) == SUCCESS)) { + return SUCCESS; + } + + php_poll_set_current_errno_error(ctx); + return FAILURE; +} + +/* Wait for events */ +PHPAPI int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) +{ + if (UNEXPECTED(!ctx || !ctx->initialized || !events || max_events <= 0)) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + return -1; + } + + /* Delegate to backend - it handles everything including ET simulation if needed */ + int nfds = ctx->backend_ops->wait(ctx, events, max_events, timeout); + + if (UNEXPECTED(nfds < 0)) { + php_poll_set_current_errno_error(ctx); + } + + return nfds; +} + +/* Get backend name */ +PHPAPI const char *php_poll_backend_name(php_poll_ctx *ctx) +{ + return ctx && ctx->backend_ops ? ctx->backend_ops->name : "unknown"; +} + +/* Get backend type */ +PHPAPI php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx) +{ + return ctx ? ctx->backend_type : PHP_POLL_BACKEND_AUTO; +} + +/* Check edge-triggering support */ +PHPAPI bool php_poll_supports_et(php_poll_ctx *ctx) +{ + return ctx && ctx->backend_ops && ctx->backend_ops->supports_et; +} + +/* Get suitable max_events for backend */ +PHPAPI int php_poll_get_suitable_max_events(php_poll_ctx *ctx) +{ + if (UNEXPECTED(!ctx || !ctx->backend_ops)) { + return -1; + } + + return ctx->backend_ops->get_suitable_max_events(ctx); +} + +/* Error retrieval */ +PHPAPI php_poll_error php_poll_get_error(php_poll_ctx *ctx) +{ + return ctx ? ctx->last_error : PHP_POLL_ERR_INVALID; +} + +/* Errno to php_poll_error mapping helper */ +php_poll_error php_poll_errno_to_error(int err) +{ + switch (err) { + case 0: + return PHP_POLL_ERR_NONE; + + case ENOMEM: + return PHP_POLL_ERR_NOMEM; + + case EINVAL: + case EBADF: + return PHP_POLL_ERR_INVALID; + + case EEXIST: + return PHP_POLL_ERR_EXISTS; + + case ENOENT: + return PHP_POLL_ERR_NOTFOUND; + +#ifdef ETIME + case ETIME: +#endif +#ifdef ETIMEDOUT + case ETIMEDOUT: +#endif + return PHP_POLL_ERR_TIMEOUT; + + case EINTR: + return PHP_POLL_ERR_INTERRUPTED; + + case EACCES: +#ifdef EPERM + case EPERM: +#endif + return PHP_POLL_ERR_PERMISSION; + +#ifdef EMFILE + case EMFILE: +#endif +#ifdef ENFILE + case ENFILE: +#endif + return PHP_POLL_ERR_TOOBIG; + + case EAGAIN: +#if defined(EWOULDBLOCK) && EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: +#endif + return PHP_POLL_ERR_AGAIN; + +#ifdef ENOSYS + case ENOSYS: +#endif +#if ENOTSUP + case ENOTSUP: +#endif +#if defined(EOPNOTSUPP) && EOPNOTSUPP != ENOTSUP + case EOPNOTSUPP: +#endif + return PHP_POLL_ERR_NOSUPPORT; + + default: + return PHP_POLL_ERR_SYSTEM; + } +} + +/* Get human-readable error description */ +PHPAPI const char *php_poll_error_string(php_poll_error error) +{ + switch (error) { + case PHP_POLL_ERR_NONE: + return "No error"; + case PHP_POLL_ERR_SYSTEM: + return "System error"; + case PHP_POLL_ERR_NOMEM: + return "Out of memory"; + case PHP_POLL_ERR_INVALID: + return "Invalid argument"; + case PHP_POLL_ERR_EXISTS: + return "File descriptor already exists"; + case PHP_POLL_ERR_NOTFOUND: + return "File descriptor not found"; + case PHP_POLL_ERR_TIMEOUT: + return "Operation timed out"; + case PHP_POLL_ERR_INTERRUPTED: + return "Operation interrupted"; + case PHP_POLL_ERR_PERMISSION: + return "Permission denied"; + case PHP_POLL_ERR_TOOBIG: + return "Too many open files"; + case PHP_POLL_ERR_AGAIN: + return "Resource temporarily unavailable"; + case PHP_POLL_ERR_NOSUPPORT: + return "Operation not supported"; + default: + return "Unknown error"; + } +} diff --git a/main/poll/poll_fd_table.c b/main/poll/poll_fd_table.c new file mode 100644 index 0000000000000..841bfcca8912f --- /dev/null +++ b/main/poll/poll_fd_table.c @@ -0,0 +1,113 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll_internal.h" + +php_poll_fd_table *php_poll_fd_table_init(int initial_capacity, bool persistent) +{ + php_poll_fd_table *table = php_poll_calloc(1, sizeof(php_poll_fd_table), persistent); + if (!table) { + return NULL; + } + + if (initial_capacity <= 0) { + initial_capacity = 64; + } + + _zend_hash_init(&table->entries_ht, initial_capacity, NULL, persistent); + table->persistent = persistent; + + return table; +} + +void php_poll_fd_table_cleanup(php_poll_fd_table *table) +{ + if (table) { + zval *zv; + + ZEND_HASH_FOREACH_VAL(&table->entries_ht, zv) + { + php_poll_fd_entry *entry = Z_PTR_P(zv); + pefree(entry, table->persistent); + } + ZEND_HASH_FOREACH_END(); + + zend_hash_destroy(&table->entries_ht); + pefree(table, table->persistent); + } +} + +php_poll_fd_entry *php_poll_fd_table_find(php_poll_fd_table *table, int fd) +{ + zval *zv = zend_hash_index_find(&table->entries_ht, (zend_ulong) fd); + return zv ? Z_PTR_P(zv) : NULL; +} + +php_poll_fd_entry *php_poll_fd_table_get(php_poll_fd_table *table, int fd) +{ + php_poll_fd_entry *entry = php_poll_fd_table_find(table, fd); + if (entry) { + return entry; + } + + entry = php_poll_calloc(1, sizeof(php_poll_fd_entry), table->persistent); + if (!entry) { + return NULL; + } + + entry->fd = fd; + entry->active = true; + entry->events = 0; + entry->data = NULL; + entry->last_revents = 0; + + zval zv; + ZVAL_PTR(&zv, entry); + if (!zend_hash_index_add(&table->entries_ht, (zend_ulong) fd, &zv)) { + pefree(entry, table->persistent); + return NULL; + } + + return entry; +} + +void php_poll_fd_table_remove(php_poll_fd_table *table, int fd) +{ + zval *zv = zend_hash_index_find(&table->entries_ht, (zend_ulong) fd); + if (zv) { + php_poll_fd_entry *entry = Z_PTR_P(zv); + pefree(entry, table->persistent); + zend_hash_index_del(&table->entries_ht, (zend_ulong) fd); + } +} + +/* Helper function for backends that need to iterate over all entries */ +typedef bool (*php_poll_fd_iterator_func_t)(int fd, php_poll_fd_entry *entry, void *user_data); + +/* Iterate over all active FD entries */ +void php_poll_fd_table_foreach( + php_poll_fd_table *table, php_poll_fd_iterator_func_t callback, void *user_data) +{ + zend_ulong fd; + zval *zv; + + ZEND_HASH_FOREACH_NUM_KEY_VAL(&table->entries_ht, fd, zv) + { + php_poll_fd_entry *entry = Z_PTR_P(zv); + if (entry->active && !callback((int) fd, entry, user_data)) { + break; /* Callback returned false, stop iteration */ + } + } + ZEND_HASH_FOREACH_END(); +} diff --git a/sapi/fpm/config.m4 b/sapi/fpm/config.m4 index 4d4952eee86e7..89c53a0c4d284 100644 --- a/sapi/fpm/config.m4 +++ b/sapi/fpm/config.m4 @@ -262,100 +262,16 @@ AS_VAR_IF([php_cv_have_SO_LISTENQLEN], [yes], [Define to 1 if you have 'SO_LISTENQ*'.])]) ]) -AC_DEFUN([PHP_FPM_KQUEUE], -[AC_CACHE_CHECK([for kqueue], - [php_cv_have_kqueue], - [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([dnl - #include - #include - #include - ], [dnl - int kfd; - struct kevent k; - kfd = kqueue(); - /* 0 -> STDIN_FILENO */ - EV_SET(&k, 0, EVFILT_READ , EV_ADD | EV_CLEAR, 0, 0, NULL); - (void)kfd; - ])], - [php_cv_have_kqueue=yes], - [php_cv_have_kqueue=no])]) -AS_VAR_IF([php_cv_have_kqueue], [yes], - [AC_DEFINE([HAVE_KQUEUE], [1], - [Define to 1 if system has a working 'kqueue' function.])]) -]) - -AC_DEFUN([PHP_FPM_EPOLL], -[AC_CACHE_CHECK([for epoll], - [php_cv_have_epoll], - [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([#include ], [dnl - int epollfd; - struct epoll_event e; - - epollfd = epoll_create(1); - if (epollfd < 0) { - return 1; - } - - e.events = EPOLLIN | EPOLLET; - e.data.fd = 0; - - if (epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &e) == -1) { - return 1; - } - - e.events = 0; - if (epoll_wait(epollfd, &e, 1, 1) < 0) { - return 1; - } - ])], - [php_cv_have_epoll=yes], - [php_cv_have_epoll=no])]) -AS_VAR_IF([php_cv_have_epoll], [yes], - [AC_DEFINE([HAVE_EPOLL], [1], [Define to 1 if system has a working epoll.])]) -]) - -AC_DEFUN([PHP_FPM_SELECT], -[AC_CACHE_CHECK([for select], - [php_cv_have_select], - [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([dnl - /* According to POSIX.1-2001 */ - #include - - /* According to earlier standards */ - #include - #include - #include - ], [dnl - fd_set fds; - struct timeval t; - t.tv_sec = 0; - t.tv_usec = 42; - FD_ZERO(&fds); - /* 0 -> STDIN_FILENO */ - FD_SET(0, &fds); - select(FD_SETSIZE, &fds, NULL, NULL, &t); - ])], - [php_cv_have_select=yes], - [php_cv_have_select=no])]) -AS_VAR_IF([php_cv_have_select], [yes], - [AC_DEFINE([HAVE_SELECT], [1], - [Define to 1 if system has a working 'select' function.])]) -]) - if test "$PHP_FPM" != "no"; then PHP_FPM_CLOCK PHP_FPM_TRACE PHP_FPM_BUILTIN_ATOMIC PHP_FPM_LQ - PHP_FPM_KQUEUE - PHP_FPM_EPOLL - PHP_FPM_SELECT AC_CHECK_FUNCS([clearenv setproctitle setproctitle_fast]) AC_CHECK_HEADER([priv.h], [AC_CHECK_FUNCS([setpflags])]) AC_CHECK_HEADER([sys/times.h], [AC_CHECK_FUNCS([times])]) - AC_CHECK_HEADER([port.h], [AC_CHECK_FUNCS([port_create])]) PHP_ARG_WITH([fpm-user],, [AS_HELP_STRING([[--with-fpm-user[=USER]]], diff --git a/sapi/fpm/fpm/events/port.c b/sapi/fpm/fpm/events/port.c index 73cf24c82c2c2..7731b7f97f2e9 100644 --- a/sapi/fpm/fpm/events/port.c +++ b/sapi/fpm/fpm/events/port.c @@ -19,7 +19,7 @@ #include "../fpm.h" #include "../zlog.h" -#ifdef HAVE_PORT_CREATE +#ifdef HAVE_EVENT_PORTS #include #include @@ -45,19 +45,19 @@ port_event_t *events = NULL; int nevents = 0; static int pfd = -1; -#endif /* HAVE_PORT_CREATE */ +#endif /* HAVE_EVENT_PORTS */ struct fpm_event_module_s *fpm_event_port_module(void) /* {{{ */ { -#ifdef HAVE_PORT_CREATE +#ifdef HAVE_EVENT_PORTS return &port_module; #else return NULL; -#endif /* HAVE_PORT_CREATE */ +#endif /* HAVE_EVENT_PORTS */ } /* }}} */ -#ifdef HAVE_PORT_CREATE +#ifdef HAVE_EVENT_PORTS /* * Init the module @@ -196,4 +196,4 @@ static int fpm_event_port_remove(struct fpm_event_s *ev) /* {{{ */ } /* }}} */ -#endif /* HAVE_PORT_CREATE */ +#endif /* HAVE_EVENT_PORTS */ diff --git a/win32/build/config.w32 b/win32/build/config.w32 index 403f0aa6efbfe..9fd56b142974f 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -298,6 +298,10 @@ AC_DEFINE('HAVE_STRNLEN', 1); AC_DEFINE('ZEND_CHECK_STACK_LIMIT', 1) +ADD_SOURCES("main/poll", "poll_backend_iocp.c poll_backend_poll.c poll_core.c \ + poll_fd_table.c"); +ADD_FLAG("CFLAGS_BD_MAIN_POLL", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + ADD_SOURCES("main/streams", "streams.c cast.c memory.c filter.c plain_wrapper.c \ userspace.c transports.c xp_socket.c mmap.c glob_wrapper.c"); ADD_FLAG("CFLAGS_BD_MAIN_STREAMS", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); @@ -309,7 +313,7 @@ ADD_SOURCES("win32", "dllmain.c readdir.c \ ADD_FLAG("CFLAGS_BD_WIN32", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); -PHP_INSTALL_HEADERS("", "Zend/ TSRM/ main/ main/streams/ win32/"); +PHP_INSTALL_HEADERS("", "Zend/ TSRM/ main/ main/poll/ main/streams/ win32/"); PHP_INSTALL_HEADERS("Zend/Optimizer", "zend_call_graph.h zend_cfg.h zend_dfg.h zend_dump.h zend_func_info.h zend_inference.h zend_optimizer.h zend_ssa.h zend_worklist.h"); STDOUT.WriteBlankLines(1);