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);