Commit 2d151087a70 for php.net
commit 2d151087a7017c8d2c63b08aaf6912c53879f1cd
Author: Jakub Zelenka <bukka@php.net>
Date: Sun Mar 15 17:13:34 2026 +0100
Add internal polling API
This introduces various IO polling backend including epoll, kqueue,
event ports, poll and WSAPoll. It makes the usage compatible between
backends.
diff --git a/build/php.m4 b/build/php.m4
index 516e18e1abf..74c069f5887 100644
--- a/build/php.m4
+++ b/build/php.m4
@@ -1382,6 +1382,50 @@ int main(void) {
])
])
+AC_DEFUN([PHP_POLL_MECHANISMS],
+[
+ AC_MSG_CHECKING([for polling mechanisms])
+ poll_mechanisms=""
+
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([
+ #include <sys/epoll.h>
+ ], [
+ int fd = epoll_create(1);
+ return fd;
+ ])], [
+ AC_DEFINE([HAVE_EPOLL], [1], [Define if epoll is available])
+ poll_mechanisms="$poll_mechanisms epoll"
+
+ AC_CHECK_FUNCS([epoll_pwait2], [], [], [#include <sys/epoll.h>])
+ ])
+
+ AC_COMPILE_IFELSE([AC_LANG_PROGRAM([
+ #include <sys/event.h>
+ #include <sys/time.h>
+ ], [
+ 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 <port.h>
+ ], [
+ 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 7b1fc3af784..63c00f508c1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -444,6 +444,7 @@ AC_CHECK_HEADERS(m4_normalize([
])
PHP_FOPENCOOKIE
+PHP_POLL_MECHANISMS
PHP_BROKEN_GETCWD
AS_VAR_IF([GCC], [yes], [PHP_BROKEN_GCC_STRLEN_OPT])
@@ -1682,6 +1683,17 @@ 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
+ poll_handle.c
+ ]),
+ [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1])
+
PHP_ADD_SOURCES([main/streams], m4_normalize([
cast.c
filter.c
diff --git a/main/php_poll.h b/main/php_poll.h
new file mode 100644
index 00000000000..e329c703399
--- /dev/null
+++ b/main/php_poll.h
@@ -0,0 +1,177 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 <https://www.php.net/license/>. |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka <bukka@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+#ifndef PHP_POLL_H
+#define PHP_POLL_H
+
+#include "php.h"
+#include "php_network.h"
+
+#include <time.h>
+
+/* ----- Public generic API ----- */
+
+/* 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 flags */
+#define PHP_POLL_FLAG_PERSISTENT 0x01
+#define PHP_POLL_FLAG_RAW_EVENTS 0x02
+
+/* 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_WSAPOLL
+} php_poll_backend_type;
+
+/* Error code constants for exception codes */
+#define PHP_POLL_ERROR_CODE_NONE 0
+#define PHP_POLL_ERROR_CODE_SYSTEM 1
+#define PHP_POLL_ERROR_CODE_NOMEM 2
+#define PHP_POLL_ERROR_CODE_INVALID 3
+#define PHP_POLL_ERROR_CODE_EXISTS 4
+#define PHP_POLL_ERROR_CODE_NOTFOUND 5
+#define PHP_POLL_ERROR_CODE_TIMEOUT 6
+#define PHP_POLL_ERROR_CODE_INTERRUPTED 7
+#define PHP_POLL_ERROR_CODE_PERMISSION 8
+#define PHP_POLL_ERROR_CODE_TOOBIG 9
+#define PHP_POLL_ERROR_CODE_AGAIN 10
+#define PHP_POLL_ERROR_CODE_NOSUPPORT 11
+
+/* Error codes */
+typedef enum {
+ PHP_POLL_ERR_NONE = PHP_POLL_ERROR_CODE_NONE, /* No error */
+ PHP_POLL_ERR_SYSTEM = PHP_POLL_ERROR_CODE_SYSTEM, /* Generic system error */
+ PHP_POLL_ERR_NOMEM = PHP_POLL_ERROR_CODE_NOMEM, /* Out of memory (ENOMEM) */
+ PHP_POLL_ERR_INVALID = PHP_POLL_ERROR_CODE_INVALID, /* Invalid argument (EINVAL, EBADF) */
+ PHP_POLL_ERR_EXISTS = PHP_POLL_ERROR_CODE_EXISTS, /* Already exists (EEXIST) */
+ PHP_POLL_ERR_NOTFOUND = PHP_POLL_ERROR_CODE_NOTFOUND, /* Not found (ENOENT) */
+ PHP_POLL_ERR_TIMEOUT = PHP_POLL_ERROR_CODE_TIMEOUT, /* Operation timed out (ETIME, ETIMEDOUT) */
+ PHP_POLL_ERR_INTERRUPTED = PHP_POLL_ERROR_CODE_INTERRUPTED, /* Interrupted by signal (EINTR) */
+ PHP_POLL_ERR_PERMISSION = PHP_POLL_ERROR_CODE_PERMISSION, /* Permission denied (EACCES, EPERM) */
+ PHP_POLL_ERR_TOOBIG = PHP_POLL_ERROR_CODE_TOOBIG, /* Too many resources (EMFILE, ENFILE) */
+ PHP_POLL_ERR_AGAIN = PHP_POLL_ERROR_CODE_AGAIN, /* Try again (EAGAIN, EWOULDBLOCK) */
+ PHP_POLL_ERR_NOSUPPORT = PHP_POLL_ERROR_CODE_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;
+
+PHPAPI bool php_poll_is_backend_available(php_poll_backend_type backend);
+PHPAPI bool php_poll_backend_supports_edge_triggering(php_poll_backend_type backend);
+
+PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, uint32_t flags);
+PHPAPI php_poll_ctx *php_poll_create_by_name(const char *preferred_backend, uint32_t flags);
+
+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,
+ const struct timespec *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);
+
+/* ----- Public extension API ----- */
+
+typedef struct php_poll_handle_ops php_poll_handle_ops;
+typedef struct php_poll_handle_object php_poll_handle_object;
+
+/* Handle operations structure - extensions can provide their own */
+struct php_poll_handle_ops {
+ /**
+ * Get file descriptor for this handle
+ * @param handle The handle object
+ * @return File descriptor or SOCK_ERR if invalid/not applicable
+ */
+ php_socket_t (*get_fd)(php_poll_handle_object *handle);
+
+ /**
+ * Check if handle is still valid
+ * @param handle The handle object
+ * @return true if valid, false if invalid
+ */
+ int (*is_valid)(php_poll_handle_object *handle);
+
+ /**
+ * Cleanup handle-specific data
+ * @param handle The handle object
+ */
+ void (*cleanup)(php_poll_handle_object *handle);
+};
+
+/* Base poll handle object structure */
+struct php_poll_handle_object {
+ php_poll_handle_ops *ops;
+ void *handle_data;
+ zend_object std;
+};
+
+#define PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(obj) \
+ ((php_poll_handle_object *) ((char *) (obj) - XtOffsetOf(php_poll_handle_object, std)))
+
+#define PHP_POLL_HANDLE_OBJ_FROM_ZV(zv) PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(Z_OBJ_P(zv))
+
+/* Default operations */
+extern php_poll_handle_ops php_poll_handle_default_ops;
+
+/* Utility functions for extensions */
+PHPAPI php_poll_handle_object *php_poll_handle_object_create(
+ size_t obj_size, zend_class_entry *ce, php_poll_handle_ops *ops);
+PHPAPI void php_poll_handle_object_free(zend_object *obj);
+
+/* Get file descriptor from any poll handle */
+PHPAPI php_socket_t php_poll_handle_get_fd(php_poll_handle_object *handle);
+
+#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 00000000000..95152131439
--- /dev/null
+++ b/main/poll/php_poll_internal.h
@@ -0,0 +1,176 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 <https://www.php.net/license/>. |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka <bukka@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+#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,
+ const struct timespec *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;
+ php_poll_error last_error;
+
+ /* Optional capacity hint for backends */
+ int max_events_hint;
+
+ /* Flags */
+ uint32_t initialized : 1;
+ uint32_t persistent : 1;
+ uint32_t raw_events : 1;
+
+ /* 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);
+php_poll_fd_entry *php_poll_fd_table_get_new(php_poll_fd_table *table, int fd);
+bool 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 bool php_poll_is_timeout_error(void)
+{
+#if defined(ETIME) && defined(ETIMEDOUT)
+ return errno == ETIME || errno == ETIMEDOUT;
+#elif defined(ETIME)
+ return errno == ETIME;
+#elif defined(ETIMEDOUT)
+ return errno == ETIMEDOUT;
+#else
+ return false;
+#endif
+}
+
+
+static inline void php_poll_set_error(php_poll_ctx *ctx, php_poll_error error)
+{
+ ctx->last_error = error;
+}
+
+static inline int php_poll_timespec_to_ms(const struct timespec *timeout)
+{
+ if (timeout == NULL) {
+ return -1;
+ }
+
+ int ms = (int) (timeout->tv_sec * 1000);
+ /* Round nanoseconds up to the next millisecond to avoid premature return */
+ if (timeout->tv_nsec > 0) {
+ ms += (int) ((timeout->tv_nsec + 999999) / 1000000);
+ }
+
+ return ms;
+}
+
+#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 00000000000..b0dbc4c7dbc
--- /dev/null
+++ b/main/poll/poll_backend_epoll.c
@@ -0,0 +1,247 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 <https://www.php.net/license/>. |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka <bukka@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+#include "php_poll_internal.h"
+
+#ifdef HAVE_EPOLL
+
+#include <sys/epoll.h>
+
+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,
+ const struct timespec *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;
+#ifdef HAVE_EPOLL_PWAIT2
+ nfds = epoll_pwait2(backend_data->epoll_fd, backend_data->events, max_events, timeout, NULL);
+#else
+ int timeout_ms = php_poll_timespec_to_ms(timeout);
+ nfds = epoll_wait(backend_data->epoll_fd, backend_data->events, max_events, timeout_ms);
+#endif
+
+ 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 00000000000..f3bb3fa66e3
--- /dev/null
+++ b/main/poll/poll_backend_eventport.c
@@ -0,0 +1,403 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 <https://www.php.net/license/>. |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka <bukka@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+#include "php_poll_internal.h"
+
+#ifdef HAVE_EVENT_PORTS
+
+#include <port.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+typedef struct {
+ int port_fd;
+ port_event_t *events;
+ int events_capacity;
+ php_poll_fd_table *fd_table;
+} eventport_backend_data_t;
+
+/* We use last_revents field to track if fd needs (re)association */
+#define EVENTPORT_NEEDS_ASSOC 1
+#define EVENTPORT_IS_ASSOCIATED 0
+
+/* 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;
+ }
+
+ /* 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 - just store in table */
+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_new(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;
+ entry->last_revents = EVENTPORT_NEEDS_ASSOC; /* Mark as needing association */
+
+ return SUCCESS;
+}
+
+/* Modify file descriptor in event port - just update table */
+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;
+
+ /* If currently associated, dissociate so we can re-associate with new events */
+ if (entry->last_revents == EVENTPORT_IS_ASSOCIATED) {
+ port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd);
+ }
+
+ entry->last_revents = EVENTPORT_NEEDS_ASSOC; /* Mark as needing re-association */
+
+ 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;
+
+ 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;
+ }
+
+ /* Only dissociate if it was actually associated */
+ if (entry->last_revents == EVENTPORT_IS_ASSOCIATED) {
+ if (port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd) == -1) {
+ /* Only fail if it's not ENOENT (might already be auto-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);
+ return SUCCESS;
+}
+
+/* Callback context for associating fds */
+typedef struct {
+ eventport_backend_data_t *backend_data;
+ php_poll_ctx *ctx;
+ bool has_error;
+} eventport_associate_ctx;
+
+/* Callback to associate fds that need association */
+static bool eventport_associate_callback(int fd, php_poll_fd_entry *entry, void *user_data)
+{
+ eventport_associate_ctx *assoc_ctx = (eventport_associate_ctx *) user_data;
+
+ /* Only associate if marked as needing association */
+ if (entry->last_revents == EVENTPORT_NEEDS_ASSOC) {
+ int native_events = eventport_events_to_native(entry->events);
+
+ if (port_associate(assoc_ctx->backend_data->port_fd, PORT_SOURCE_FD, fd, native_events,
+ entry->data)
+ == -1) {
+ /* Association failed - could set error here if needed */
+ switch (errno) {
+ case EBADFD:
+ /* fd got closed - remove it */
+ php_poll_fd_table_remove(assoc_ctx->backend_data->fd_table, fd);
+ return true;
+ case ENOMEM:
+ php_poll_set_error(assoc_ctx->ctx, PHP_POLL_ERR_NOMEM);
+ break;
+ case EINVAL:
+ php_poll_set_error(assoc_ctx->ctx, PHP_POLL_ERR_INVALID);
+ break;
+ default:
+ php_poll_set_error(assoc_ctx->ctx, PHP_POLL_ERR_SYSTEM);
+ break;
+ }
+ assoc_ctx->has_error = true;
+ return false; /* Stop iteration */
+ }
+
+ /* Mark as associated */
+ entry->last_revents = EVENTPORT_IS_ASSOCIATED;
+ }
+
+ return true; /* Continue iteration */
+}
+
+/* Wait for events using event port */
+static int eventport_backend_wait(
+ php_poll_ctx *ctx, php_poll_event *events, int max_events,
+ const struct timespec *timeout)
+{
+ eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data;
+
+ int fd_count = php_poll_fd_table_count(backend_data->fd_table);
+ if (fd_count == 0) {
+ /* No fds to monitor, but we still need to respect timeout */
+ if (timeout != NULL && (timeout->tv_sec > 0 || timeout->tv_nsec > 0)) {
+ nanosleep(timeout, NULL);
+ }
+ return 0;
+ }
+
+ /* First: associate all fds that need association */
+ eventport_associate_ctx assoc_ctx
+ = { .backend_data = backend_data, .ctx = ctx, .has_error = false };
+
+ php_poll_fd_table_foreach(backend_data->fd_table, eventport_associate_callback, &assoc_ctx);
+
+ if (assoc_ctx.has_error) {
+ return -1;
+ }
+
+ /* 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;
+ }
+
+ /* port_getn accepts NULL timeout for indefinite wait, or a timespec pointer -
+ * this maps directly to our new API. We need a mutable copy since port_getn
+ * may modify the timeout. */
+ struct timespec ts_copy;
+ struct timespec *tsp = NULL;
+ if (timeout != NULL) {
+ ts_copy = *timeout;
+ tsp = &ts_copy;
+ }
+
+ /* 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) {
+ if (php_poll_is_timeout_error()) {
+ return 0;
+ }
+ php_poll_set_current_errno_error(ctx);
+ return -1;
+ }
+
+ int nfds = (int) nget;
+
+ /* Process events */
+ 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;
+ events[i].revents = eventport_events_from_native(port_event->portev_events);
+ events[i].data = port_event->portev_user;
+
+ /* After event fires, the association is automatically removed by event ports */
+ php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, fd);
+ if (entry) {
+ if (entry->events & PHP_POLL_ONESHOT) {
+ /* Oneshot: remove from tracking completely */
+ php_poll_fd_table_remove(backend_data->fd_table, fd);
+ } else {
+ /* Mark for re-association on next wait() call */
+ entry->last_revents = EVENTPORT_NEEDS_ASSOC;
+ }
+ }
+ } 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;
+ }
+
+ int fd_count = php_poll_fd_table_count(backend_data->fd_table);
+
+ if (fd_count == 0) {
+ return 1;
+ }
+
+ /* Event ports can return exactly one event per association,
+ * so the suitable max_events is exactly the number of tracked fds */
+ return fd_count;
+}
+
+/* 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 = false /* Event ports are level-triggered only */
+};
+
+#endif /* HAVE_EVENT_PORTS */
diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c
new file mode 100644
index 00000000000..9a654c716d5
--- /dev/null
+++ b/main/poll/poll_backend_kqueue.c
@@ -0,0 +1,492 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 <https://www.php.net/license/>. |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka <bukka@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+#include "php_poll_internal.h"
+
+#ifdef HAVE_KQUEUE
+
+#include <sys/types.h>
+#include <sys/event.h>
+#include <sys/time.h>
+
+/* Flags for tracking FD state in single hash table */
+#define KQUEUE_FD_PRESENT (1 << 0) /* FD is registered */
+#define KQUEUE_FD_ONESHOT_COMPLETE (1 << 1) /* Has both read+write oneshot */
+#define KQUEUE_FD_GARBAGE_READ (1 << 2) /* Read filter fired, needs write cleanup */
+#define KQUEUE_FD_GARBAGE_WRITE (1 << 3) /* Write filter fired, needs read cleanup */
+#define KQUEUE_FD_HAS_GARBAGE (KQUEUE_FD_GARBAGE_READ | KQUEUE_FD_GARBAGE_WRITE)
+
+typedef struct {
+ int kqueue_fd;
+ struct kevent *events;
+ int events_capacity;
+ int fd_count; /* Track number of unique FDs (not individual filters) */
+ int filter_count; /* Track total number of filters for raw events */
+ HashTable *fd_tracking; /* Single hash table for all FD state tracking */
+} 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;
+ data->filter_count = 0;
+
+ /* Initialize single tracking hash table (only if not using raw events) */
+ if (!ctx->raw_events) {
+ data->fd_tracking = php_poll_malloc(sizeof(HashTable), ctx->persistent);
+ zend_hash_init(data->fd_tracking, 8, NULL, NULL, ctx->persistent);
+ } else {
+ data->fd_tracking = NULL;
+ }
+
+ 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);
+
+ /* Cleanup tracking hash table if initialized */
+ if (data->fd_tracking) {
+ zend_hash_destroy(data->fd_tracking);
+ pefree(data->fd_tracking, 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;
+
+ /* Check for duplicate in non-raw mode */
+ if (!ctx->raw_events) {
+ zval *existing = zend_hash_index_find(backend_data->fd_tracking, fd);
+ if (existing && (Z_LVAL_P(existing) & KQUEUE_FD_PRESENT)) {
+ php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS);
+ return FAILURE;
+ }
+ }
+
+ 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 counters */
+ backend_data->fd_count++;
+ backend_data->filter_count += change_count;
+
+ /* Track FD state in non-raw mode */
+ if (!ctx->raw_events) {
+ zend_long tracking_flags = KQUEUE_FD_PRESENT;
+
+ /* Mark as complete oneshot if both read+write with oneshot */
+ if ((events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT))
+ == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT)) {
+ tracking_flags |= KQUEUE_FD_ONESHOT_COMPLETE;
+ }
+
+ zval tracking_zval;
+ ZVAL_LONG(&tracking_zval, tracking_flags);
+ zend_hash_index_update(backend_data->fd_tracking, fd, &tracking_zval);
+ }
+ }
+
+ 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 counters and tracking */
+ if (successful_deletes > 0 && add_count == 0) {
+ /* Removed all filters - FD is gone */
+ backend_data->fd_count--;
+ backend_data->filter_count -= successful_deletes;
+ if (!ctx->raw_events) {
+ zend_hash_index_del(backend_data->fd_tracking, fd);
+ }
+ } else if (add_count > 0) {
+ if (successful_deletes == 0) {
+ /* Added filters to previously empty FD */
+ backend_data->fd_count++;
+ backend_data->filter_count += add_count;
+ } else {
+ /* Mixed operation when successful_deletes > 0 - update filter count */
+ backend_data->filter_count = backend_data->filter_count - successful_deletes + add_count;
+ }
+
+ if (!ctx->raw_events) {
+ zend_long tracking_flags = KQUEUE_FD_PRESENT;
+ if ((events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT))
+ == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT)) {
+ tracking_flags |= KQUEUE_FD_ONESHOT_COMPLETE;
+ }
+ zval tracking_zval;
+ ZVAL_LONG(&tracking_zval, tracking_flags);
+ zend_hash_index_update(backend_data->fd_tracking, fd, &tracking_zval);
+ }
+ }
+
+ 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 counters */
+ backend_data->fd_count--;
+ backend_data->filter_count -= successful_deletes;
+
+ /* Remove from tracking */
+ if (!ctx->raw_events) {
+ zend_hash_index_del(backend_data->fd_tracking, fd);
+ }
+
+ return SUCCESS;
+}
+
+static int kqueue_backend_wait(
+ php_poll_ctx *ctx, php_poll_event *events, int max_events,
+ const struct timespec *timeout)
+{
+ kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data;
+
+ /* For raw events, we need capacity for max_events.
+ * For grouped events, kqueue can return up to 2 events per FD, so we need 2x capacity. */
+ int required_capacity = ctx->raw_events ? max_events : (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;
+ }
+
+ int nfds = kevent(
+ backend_data->kqueue_fd, NULL, 0, backend_data->events, required_capacity, timeout);
+
+ if (nfds > 0) {
+ if (ctx->raw_events) {
+ /* Raw events mode - direct 1:1 mapping, no grouping */
+ for (int i = 0; i < nfds && i < max_events; i++) {
+ events[i].fd = (int) backend_data->events[i].ident;
+ events[i].events = 0;
+ events[i].revents = 0;
+ events[i].data = backend_data->events[i].udata;
+
+ /* Convert kqueue filter to poll event */
+ if (backend_data->events[i].filter == EVFILT_READ) {
+ events[i].revents |= PHP_POLL_READ;
+ } else if (backend_data->events[i].filter == EVFILT_WRITE) {
+ events[i].revents |= PHP_POLL_WRITE;
+ }
+
+ /* Convert kqueue flags to poll events */
+ if (backend_data->events[i].flags & EV_EOF) {
+ events[i].revents |= PHP_POLL_HUP;
+ }
+ if (backend_data->events[i].flags & EV_ERROR) {
+ events[i].revents |= PHP_POLL_ERROR;
+ }
+ }
+ return nfds > max_events ? max_events : nfds;
+ } else {
+ /* Grouped events mode with improved oneshot tracking */
+ int unique_events = 0, garbage_events = 0, fd;
+
+ 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++;
+
+ /* Handle oneshot tracking */
+ if (is_oneshot) {
+ zval *tracking = zend_hash_index_find(backend_data->fd_tracking, fd);
+ if (tracking && (Z_LVAL_P(tracking) & KQUEUE_FD_ONESHOT_COMPLETE)) {
+ /* Mark which filter fired for garbage collection */
+ zend_long flags = Z_LVAL_P(tracking);
+ flags &= ~KQUEUE_FD_ONESHOT_COMPLETE; /* Clear complete flag */
+ if (revents & PHP_POLL_READ) {
+ flags |= KQUEUE_FD_GARBAGE_READ; /* Need to clean write */
+ }
+ if (revents & PHP_POLL_WRITE) {
+ flags |= KQUEUE_FD_GARBAGE_WRITE; /* Need to clean read */
+ }
+ ZVAL_LONG(tracking, flags);
+ backend_data->fd_count--;
+ garbage_events++;
+ }
+ }
+ } else if (is_oneshot) {
+ /* Second filter for same FD fired - clear garbage flags */
+ zval *tracking = zend_hash_index_find(backend_data->fd_tracking, fd);
+ if (tracking) {
+ /* Remove FD from tracking as it gets deleted from kqueue as well */
+ zend_hash_index_del(backend_data->fd_tracking, fd);
+ garbage_events--;
+ }
+ }
+ }
+
+ if (garbage_events > 0) {
+ /* Clean up orphaned filters from complete oneshot FDs */
+ zend_ulong fd_key;
+ zval *tracking;
+ struct kevent cleanup_change;
+ ZEND_HASH_FOREACH_NUM_KEY_VAL(backend_data->fd_tracking, fd_key, tracking)
+ {
+ zend_long flags = Z_LVAL_P(tracking);
+ if (flags & KQUEUE_FD_HAS_GARBAGE) {
+ int filter = (flags & KQUEUE_FD_GARBAGE_READ) ? EVFILT_WRITE : EVFILT_READ;
+ EV_SET(&cleanup_change, fd_key, filter, EV_DELETE, 0, 0, NULL);
+ kevent(backend_data->kqueue_fd, &cleanup_change, 1, NULL, 0, NULL);
+
+ /* Remove FD from tracking after cleanup */
+ zend_hash_index_del(backend_data->fd_tracking, fd_key);
+ }
+ }
+ 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;
+ }
+
+ if (ctx->raw_events) {
+ /* For raw events, return the total number of filters */
+ int active_filters = backend_data->filter_count;
+ return active_filters == 0 ? 1 : active_filters;
+ } else {
+ /* For grouped events, return the number of unique FDs */
+ int active_fds = backend_data->fd_count;
+ return active_fds == 0 ? 1 : 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
+};
+
+
+#endif /* HAVE_KQUEUE */
diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c
new file mode 100644
index 00000000000..311c48529bc
--- /dev/null
+++ b/main/poll/poll_backend_poll.c
@@ -0,0 +1,287 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 <https://www.php.net/license/>. |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka <bukka@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+#include "php_poll_internal.h"
+
+#ifndef PHP_WIN32
+
+typedef struct {
+ php_poll_fd_table *fd_table;
+ struct 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(struct 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_new(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_remove(backend_data->fd_table, fd)) {
+ php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND);
+ return FAILURE;
+ }
+
+ return SUCCESS;
+}
+
+/* Context for building struct pollfd array */
+typedef struct {
+ struct pollfd *fds;
+ int index;
+} poll_build_context;
+
+/* Callback to build struct 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 int poll_backend_wait(
+ php_poll_ctx *ctx, php_poll_event *events, int max_events,
+ const struct timespec *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 != NULL && (timeout->tv_sec > 0 || timeout->tv_nsec > 0)) {
+ nanosleep(timeout, NULL);
+ }
+ return 0;
+ }
+
+ /* Ensure temp_fds array is large enough */
+ if (fd_count > backend_data->temp_fds_capacity) {
+ struct pollfd *new_fds = php_poll_realloc(
+ backend_data->temp_fds, fd_count * sizeof(struct 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 struct 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);
+
+ /* Convert timespec to milliseconds (poll() only supports ms resolution) */
+ int timeout_ms = php_poll_timespec_to_ms(timeout);
+ int nfds = poll(backend_data->temp_fds, fd_count, timeout_ms);
+
+ if (nfds <= 0) {
+ return nfds; /* Return 0 for timeout, -1 for error */
+ }
+
+ /* Process results - iterate through struct pollfd array directly */
+ int event_count = 0;
+ for (int i = 0; i < fd_count && event_count < max_events; i++) {
+ struct 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,
+};
+
+#endif
diff --git a/main/poll/poll_backend_wsapoll.c b/main/poll/poll_backend_wsapoll.c
new file mode 100644
index 00000000000..d8135d7f32a
--- /dev/null
+++ b/main/poll/poll_backend_wsapoll.c
@@ -0,0 +1,335 @@
+/*
++----------------------------------------------------------------------+
+| 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 <https://www.php.net/license/>. |
+| |
+| SPDX-License-Identifier: BSD-3-Clause |
++----------------------------------------------------------------------+
+| Authors: Jakub Zelenka <bukka@php.net> |
++----------------------------------------------------------------------+
+*/
+
+#include "php_poll_internal.h"
+
+#ifdef PHP_WIN32
+
+#include <winsock2.h>
+#include <ws2tcpip.h>
+
+typedef struct {
+ php_poll_fd_table *fd_table;
+ WSAPOLLFD *temp_fds;
+ int temp_fds_capacity;
+} wsapoll_backend_data_t;
+
+static uint32_t wsapoll_events_to_native(uint32_t events)
+{
+ uint32_t native = 0;
+ if (events & PHP_POLL_READ) {
+ native |= POLLRDNORM;
+ }
+ if (events & PHP_POLL_WRITE) {
+ native |= POLLWRNORM;
+ }
+ if (events & PHP_POLL_ERROR) {
+ native |= POLLERR;
+ }
+ if (events & PHP_POLL_HUP) {
+ native |= POLLHUP;
+ }
+ return native;
+}
+
+static uint32_t wsapoll_events_from_native(uint32_t native)
+{
+ uint32_t events = 0;
+ if (native & POLLRDNORM) {
+ events |= PHP_POLL_READ;
+ }
+ if (native & POLLWRNORM) {
+ 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 socket to error */
+ }
+ return events;
+}
+
+static zend_result wsapoll_backend_init(php_poll_ctx *ctx)
+{
+ wsapoll_backend_data_t *data
+ = php_poll_calloc(1, sizeof(wsapoll_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(WSAPOLLFD), 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 wsapoll_backend_cleanup(php_poll_ctx *ctx)
+{
+ wsapoll_backend_data_t *data = (wsapoll_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 wsapoll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *user_data)
+{
+ wsapoll_backend_data_t *backend_data = (wsapoll_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_new(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 wsapoll_backend_modify(
+ php_poll_ctx *ctx, int fd, uint32_t events, void *user_data)
+{
+ wsapoll_backend_data_t *backend_data = (wsapoll_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 wsapoll_backend_remove(php_poll_ctx *ctx, int fd)
+{
+ wsapoll_backend_data_t *backend_data = (wsapoll_backend_data_t *) ctx->backend_data;
+
+ if (!php_poll_fd_table_remove(backend_data->fd_table, fd)) {
+ php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND);
+ return FAILURE;
+ }
+
+ return SUCCESS;
+}
+
+/* Context for building WSAPOLLFD array */
+typedef struct {
+ WSAPOLLFD *fds;
+ int index;
+} wsapoll_build_context;
+
+/* Callback to build WSAPOLLFD array from fd_table */
+static bool wsapoll_build_fds_callback(int fd, php_poll_fd_entry *entry, void *user_data)
+{
+ wsapoll_build_context *ctx = (wsapoll_build_context *) user_data;
+
+ ctx->fds[ctx->index].fd = (SOCKET) fd;
+ ctx->fds[ctx->index].events
+ = (SHORT) wsapoll_events_to_native(entry->events & ~(PHP_POLL_ET | PHP_POLL_ONESHOT));
+ ctx->fds[ctx->index].revents = 0;
+ ctx->index++;
+
+ return true;
+}
+
+static int wsapoll_backend_wait(
+ php_poll_ctx *ctx, php_poll_event *events, int max_events,
+ const struct timespec *timeout)
+{
+ wsapoll_backend_data_t *backend_data = (wsapoll_backend_data_t *) ctx->backend_data;
+
+ int fd_count = php_poll_fd_table_count(backend_data->fd_table);
+ if (fd_count == 0) {
+ if (timeout != NULL && (timeout->tv_sec > 0 || timeout->tv_nsec > 0)) {
+ /* Convert to milliseconds, rounding up */
+ int sleep_ms = php_poll_timespec_to_ms(timeout);
+ if (sleep_ms > 0) {
+ Sleep(sleep_ms);
+ }
+ }
+ return 0;
+ }
+
+ /* Ensure temp_fds array is large enough */
+ if (fd_count > backend_data->temp_fds_capacity) {
+ WSAPOLLFD *new_fds = php_poll_realloc(
+ backend_data->temp_fds, fd_count * sizeof(WSAPOLLFD), 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 WSAPOLLFD array from fd_table */
+ wsapoll_build_context build_ctx = { .fds = backend_data->temp_fds, .index = 0 };
+ php_poll_fd_table_foreach(backend_data->fd_table, wsapoll_build_fds_callback, &build_ctx);
+
+ /* Convert timespec to milliseconds (WSAPoll only supports ms resolution, round up) */
+ int timeout_ms = php_poll_timespec_to_ms(timeout);
+
+ /* Call WSAPoll */
+ int nfds = WSAPoll(backend_data->temp_fds, fd_count, timeout_ms);
+
+ if (nfds == SOCKET_ERROR) {
+ /* WSAPoll specific error handling */
+ int wsa_error = WSAGetLastError();
+ php_poll_error error_code;
+
+ switch (wsa_error) {
+ case WSAENOTSOCK:
+ /* Special case: all sockets in array are invalid
+ * WSAPoll fails entirely, but we should clean up and return 0
+ * This differs from Unix poll() which would report POLLNVAL per socket */
+
+ /* Remove all invalid sockets from fd_table */
+ for (int i = 0; i < fd_count; i++) {
+ int fd = (int) backend_data->temp_fds[i].fd;
+ php_poll_fd_table_remove(backend_data->fd_table, fd);
+ }
+ return 0;
+ case WSAENOBUFS:
+ error_code = PHP_POLL_ERR_NOMEM;
+ break;
+ case WSAEINVAL:
+ case WSAEFAULT:
+ error_code = PHP_POLL_ERR_INVALID;
+ break;
+ default:
+ error_code = PHP_POLL_ERR_SYSTEM;
+ break;
+ }
+
+ php_poll_set_error(ctx, error_code);
+ return -1;
+ }
+
+ if (nfds == 0) {
+ return 0; /* Timeout */
+ }
+
+ int event_count = 0;
+ for (int i = 0; i < fd_count && event_count < max_events; i++) {
+ WSAPOLLFD *pfd = &backend_data->temp_fds[i];
+
+ if (pfd->revents != 0) {
+ int fd = (int) pfd->fd;
+ php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, fd);
+ if (entry) {
+ /* WSAPoll-specific handling of POLLNVAL */
+ if (pfd->revents & POLLNVAL) {
+ php_poll_fd_table_remove(backend_data->fd_table, fd);
+ continue; /* Do not report this event */
+ }
+
+ /* Convert WSAPoll events to PHP poll events */
+ uint32_t converted_events = wsapoll_events_from_native(pfd->revents);
+
+ events[event_count].fd = fd;
+ events[event_count].events = entry->events;
+ events[event_count].revents = converted_events;
+ 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 wsapoll_backend_is_available(void)
+{
+ /* Always available on the currently supported Windows versions. */
+ return true;
+}
+
+static int wsapoll_backend_get_suitable_max_events(php_poll_ctx *ctx)
+{
+ wsapoll_backend_data_t *backend_data = (wsapoll_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_wsapoll_ops = {
+ .type = PHP_POLL_BACKEND_WSAPOLL,
+ .name = "wsapoll",
+ .init = wsapoll_backend_init,
+ .cleanup = wsapoll_backend_cleanup,
+ .add = wsapoll_backend_add,
+ .modify = wsapoll_backend_modify,
+ .remove = wsapoll_backend_remove,
+ .wait = wsapoll_backend_wait,
+ .is_available = wsapoll_backend_is_available,
+ .get_suitable_max_events = wsapoll_backend_get_suitable_max_events,
+ .supports_et = false,
+};
+
+#endif /* PHP_WIN32 */
diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c
new file mode 100644
index 00000000000..8422e46c937
--- /dev/null
+++ b/main/poll/poll_core.c
@@ -0,0 +1,438 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 <https://www.php.net/license/>. |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka <bukka@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+#include "php_poll_internal.h"
+
+/* Backend registry */
+static const php_poll_backend_ops *registered_backends[8];
+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_wsapoll_ops;
+#else
+extern const php_poll_backend_ops php_poll_backend_poll_ops;
+#endif
+
+/* Register all available backends */
+PHPAPI void php_poll_register_backends(void)
+{
+ num_registered_backends = 0;
+
+#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
+
+#ifdef PHP_WIN32
+ registered_backends[num_registered_backends++] = &php_poll_backend_wsapoll_ops;
+#else
+ registered_backends[num_registered_backends++] = &php_poll_backend_poll_ops;
+#endif
+}
+
+/* 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;
+}
+
+PHPAPI bool php_poll_is_backend_available(php_poll_backend_type backend)
+{
+ if (backend == PHP_POLL_BACKEND_AUTO) {
+ return true; /* Auto is always available */
+ }
+
+ for (int i = 0; i < num_registered_backends; i++) {
+ if (registered_backends[i] && registered_backends[i]->type == backend) {
+ return registered_backends[i]->is_available();
+ }
+ }
+
+ return false;
+}
+
+PHPAPI bool php_poll_backend_supports_edge_triggering(php_poll_backend_type backend)
+{
+ if (backend == PHP_POLL_BACKEND_AUTO) {
+ /* Check the first (best) available backend */
+ if (num_registered_backends > 0 && registered_backends[0]) {
+ return registered_backends[0]->supports_et;
+ }
+ return false;
+ }
+
+ for (int i = 0; i < num_registered_backends; i++) {
+ if (registered_backends[i] && registered_backends[i]->type == backend) {
+ return registered_backends[i]->supports_et;
+ }
+ }
+
+ return false;
+}
+
+static php_poll_ctx *php_poll_create_context(uint32_t flags)
+{
+ bool persistent = flags & PHP_POLL_FLAG_PERSISTENT;
+ php_poll_ctx *ctx = php_poll_calloc(1, sizeof(php_poll_ctx), persistent);
+ if (!ctx) {
+ return NULL;
+ }
+ ctx->persistent = persistent;
+ ctx->raw_events = (flags & PHP_POLL_FLAG_RAW_EVENTS) != 0;
+
+ return ctx;
+}
+
+/* Create new poll context */
+PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, uint32_t flags)
+{
+ php_poll_ctx *ctx = php_poll_create_context(flags);
+ if (ctx == NULL) {
+ return NULL;
+ }
+
+ /* Get backend operations */
+ ctx->backend_ops = php_poll_get_backend_ops(preferred_backend);
+ if (!ctx->backend_ops) {
+ pefree(ctx, 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, uint32_t flags)
+{
+ if (!strcmp(preferred_backend, "auto")) {
+ return php_poll_create(PHP_POLL_BACKEND_AUTO, flags);
+ }
+
+ php_poll_ctx *ctx = php_poll_create_context(flags);
+ if (ctx == NULL) {
+ return NULL;
+ }
+
+ /* Get backend operations */
+ ctx->backend_ops = php_poll_get_backend_ops_by_name(preferred_backend);
+ if (!ctx->backend_ops) {
+ pefree(ctx, 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;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ return FAILURE;
+}
+
+/* Wait for events */
+PHPAPI int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events,
+ const struct timespec *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);
+
+ 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_ops ? ctx->backend_ops->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 00000000000..97231072322
--- /dev/null
+++ b/main/poll/poll_fd_table.c
@@ -0,0 +1,119 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 <https://www.php.net/license/>. |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka <bukka@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+#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_new(php_poll_fd_table *table, int fd)
+{
+ php_poll_fd_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;
+}
+
+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;
+ }
+
+ return php_poll_fd_table_get_new(table, fd);
+}
+
+bool 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 == NULL) {
+ return false;
+ }
+ php_poll_fd_entry *entry = Z_PTR_P(zv);
+ pefree(entry, table->persistent);
+ return zend_hash_index_del(&table->entries_ht, (zend_ulong) fd) == SUCCESS;
+}
+
+/* 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/main/poll/poll_handle.c b/main/poll/poll_handle.c
new file mode 100644
index 00000000000..0c0628ac49d
--- /dev/null
+++ b/main/poll/poll_handle.c
@@ -0,0 +1,97 @@
+/*
+ +----------------------------------------------------------------------+
+ | 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 <https://www.php.net/license/>. |
+ | |
+ | SPDX-License-Identifier: BSD-3-Clause |
+ +----------------------------------------------------------------------+
+ | Authors: Jakub Zelenka <bukka@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+#include "php_poll.h"
+#include "zend_exceptions.h"
+
+/* Default get_fd implementation - calls PHP method */
+static php_socket_t php_poll_handle_default_get_fd(php_poll_handle_object *handle)
+{
+ zval retval;
+ zval obj;
+ zval func_name;
+
+ ZVAL_OBJ(&obj, &handle->std);
+
+ /* Prepare function name as zval */
+ ZVAL_STRING(&func_name, "getFileDescriptor");
+
+ /* Call getFileDescriptor() method */
+ if (EXPECTED(call_user_function(NULL, &obj, &func_name, &retval, 0, NULL) == SUCCESS)) {
+ if (Z_TYPE(retval) == IS_LONG) {
+ php_socket_t fd = Z_LVAL(retval) < 0 ? SOCK_ERR : (php_socket_t) Z_LVAL(retval);
+ zval_ptr_dtor(&retval);
+ zval_ptr_dtor(&func_name); /* Clean up function name */
+ return fd;
+ }
+ zval_ptr_dtor(&retval);
+ }
+
+ zval_ptr_dtor(&func_name); /* Clean up function name */
+ return SOCK_ERR; /* Invalid socket */
+}
+
+/* Default is_valid implementation - assume valid if we can get FD */
+static int php_poll_handle_default_is_valid(php_poll_handle_object *handle)
+{
+ return php_poll_handle_get_fd(handle) != SOCK_ERR;
+}
+
+/* Default cleanup */
+static void php_poll_handle_default_cleanup(php_poll_handle_object *handle)
+{
+ /* Base implementation has no cleanup */
+}
+
+/* Default operations that call PHP userspace methods */
+php_poll_handle_ops php_poll_handle_default_ops = { .get_fd = php_poll_handle_default_get_fd,
+ .is_valid = php_poll_handle_default_is_valid,
+ .cleanup = php_poll_handle_default_cleanup };
+
+/* Allocate a new poll handle object */
+PHPAPI php_poll_handle_object *php_poll_handle_object_create(
+ size_t obj_size, zend_class_entry *ce, php_poll_handle_ops *ops)
+{
+ php_poll_handle_object *intern = zend_object_alloc(obj_size, ce);
+
+ zend_object_std_init(&intern->std, ce);
+ object_properties_init(&intern->std, ce);
+
+ intern->ops = ops ? ops : &php_poll_handle_default_ops;
+ intern->handle_data = NULL;
+
+ return intern;
+}
+
+/* Free poll handle object */
+PHPAPI void php_poll_handle_object_free(zend_object *obj)
+{
+ php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(obj);
+
+ if (intern->ops && intern->ops->cleanup) {
+ intern->ops->cleanup(intern);
+ }
+
+ zend_object_std_dtor(&intern->std);
+}
+
+/* Get file descriptor from handle using ops */
+PHPAPI php_socket_t php_poll_handle_get_fd(php_poll_handle_object *handle)
+{
+ if (!handle || !handle->ops || !handle->ops->get_fd) {
+ return SOCK_ERR;
+ }
+
+ return handle->ops->get_fd(handle);
+}
diff --git a/sapi/fpm/config.m4 b/sapi/fpm/config.m4
index 4d4952eee86..89c53a0c4d2 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 <sys/types.h>
- #include <sys/event.h>
- #include <sys/time.h>
- ], [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 <sys/epoll.h>], [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 <sys/select.h>
-
- /* According to earlier standards */
- #include <sys/time.h>
- #include <sys/types.h>
- #include <unistd.h>
- ], [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 848ac8e84af..b7f5715abad 100644
--- a/sapi/fpm/fpm/events/port.c
+++ b/sapi/fpm/fpm/events/port.c
@@ -17,7 +17,7 @@
#include "../fpm.h"
#include "../zlog.h"
-#ifdef HAVE_PORT_CREATE
+#ifdef HAVE_EVENT_PORTS
#include <port.h>
#include <poll.h>
@@ -43,19 +43,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
@@ -194,4 +194,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 6cd6907f282..e099167c076 100644
--- a/win32/build/config.w32
+++ b/win32/build/config.w32
@@ -298,6 +298,9 @@ AC_DEFINE('HAVE_STRNLEN', 1);
AC_DEFINE('ZEND_CHECK_STACK_LIMIT', 1)
+ADD_SOURCES("main/poll", "poll_backend_wsapoll.c poll_core.c poll_fd_table.c poll_handle.c");
+ADD_FLAG("CFLAGS_BD_MAIN_POLL", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1");
+
ADD_SOURCES("main/streams", "streams.c stream_errors.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 +312,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);