Commit 6c6fb56fa63 for php.net
commit 6c6fb56fa6347ec372725adfa2864ba876b53632
Author: Jakub Zelenka <bukka@php.net>
Date: Sun Mar 15 17:16:13 2026 +0100
Introduce Io\Poll userspace API
It makes use of internal polling API and exposes its functionality to
user space.
Closes GH-19572
diff --git a/NEWS b/NEWS
index eff542c0e46..efc85058719 100644
--- a/NEWS
+++ b/NEWS
@@ -92,6 +92,9 @@ PHP NEWS
. Fixed UConverter::transcode() silently truncating from_subst and to_subst
option lengths greater than 127 bytes. (Weilin Du)
+- IO:
+ . Added new polling API. (Jakub Zelenka)
+
- JSON:
. Enriched JSON last error / exception message with error location.
(Juan Morales)
diff --git a/UPGRADING b/UPGRADING
index 2ec6ce92aa5..bdadc6efbef 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -197,6 +197,10 @@ PHP 8.6 UPGRADE NOTES
IntlNumberRangeFormatter::IDENTITY_FALLBACK_RANGE identity fallbacks.
It is supported from icu 63.
+- IO:
+ . Added new polling API.
+ RFC: https://wiki.php.net/rfc/poll_api
+
- JSON:
. Added extra info about error location to the JSON error messages returned
from json_last_error_msg() and JsonException message.
@@ -351,6 +355,23 @@ PHP 8.6 UPGRADE NOTES
. enum StreamErrorMode
. enum StreamErrorCode
RFC: https://wiki.php.net/rfc/stream_errors
+ . Io\Poll\Context
+ . Io\Poll\Watcher
+ . enum Io\Poll\Backend
+ . enum Io\Poll\Event
+ . interface Io\Poll\Handle
+ . Io\IoException
+ . Io\Poll\PollException
+ . Io\Poll\FailedPollOperationException
+ . Io\Poll\FailedContextInitializationException
+ . Io\Poll\FailedHandleAddException
+ . Io\Poll\FailedWatcherModificationException
+ . Io\Poll\FailedPollWaitException
+ . Io\Poll\BackendUnavailableException
+ . Io\Poll\InactiveWatcherException
+ . Io\Poll\HandleAlreadyWatchedException
+ . Io\Poll\InvalidHandleException
+ . StreamPollHandle
========================================
8. Removed Extensions and SAPIs
diff --git a/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt b/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt
index 9ef87f42b22..c497855f734 100644
--- a/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt
+++ b/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt
@@ -14,6 +14,22 @@
--EXPECT--
AssertionError
Directory
+Io\IoException
+Io\Poll\Backend
+Io\Poll\BackendUnavailableException
+Io\Poll\Context
+Io\Poll\Event
+Io\Poll\FailedContextInitializationException
+Io\Poll\FailedHandleAddException
+Io\Poll\FailedPollOperationException
+Io\Poll\FailedPollWaitException
+Io\Poll\FailedWatcherModificationException
+Io\Poll\Handle
+Io\Poll\HandleAlreadyWatchedException
+Io\Poll\InactiveWatcherException
+Io\Poll\InvalidHandleException
+Io\Poll\PollException
+Io\Poll\Watcher
RoundingMode
SortDirection
StreamBucket
@@ -22,5 +38,6 @@
StreamErrorMode
StreamErrorStore
StreamException
+StreamPollHandle
__PHP_Incomplete_Class
php_user_filter
diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c
index 0c1e2f8222a..f8b1cb5c4fd 100644
--- a/ext/standard/basic_functions.c
+++ b/ext/standard/basic_functions.c
@@ -302,6 +302,7 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */
BASIC_MINIT_SUBMODULE(browscap)
BASIC_MINIT_SUBMODULE(standard_filters)
BASIC_MINIT_SUBMODULE(user_filters)
+ BASIC_MINIT_SUBMODULE(poll)
BASIC_MINIT_SUBMODULE(password)
BASIC_MINIT_SUBMODULE(image)
diff --git a/ext/standard/basic_functions.h b/ext/standard/basic_functions.h
index 6bd227eac5d..4d5a4c02aec 100644
--- a/ext/standard/basic_functions.h
+++ b/ext/standard/basic_functions.h
@@ -42,6 +42,7 @@ PHP_MINFO_FUNCTION(basic);
ZEND_API void php_get_highlight_struct(zend_syntax_highlighter_ini *syntax_highlighter_ini);
+PHP_MINIT_FUNCTION(poll);
PHP_MINIT_FUNCTION(user_filters);
PHP_RSHUTDOWN_FUNCTION(user_filters);
PHP_RSHUTDOWN_FUNCTION(browscap);
diff --git a/ext/standard/config.m4 b/ext/standard/config.m4
index 1d64c7ff696..67c36b93ba3 100644
--- a/ext/standard/config.m4
+++ b/ext/standard/config.m4
@@ -418,6 +418,7 @@ PHP_NEW_EXTENSION([standard], m4_normalize([
image.c
incomplete_class.c
info.c
+ io_poll.c
iptc.c
levenshtein.c
libavifinfo/avifinfo.c
diff --git a/ext/standard/config.w32 b/ext/standard/config.w32
index 589929027f6..50031ac3b8a 100644
--- a/ext/standard/config.w32
+++ b/ext/standard/config.w32
@@ -28,14 +28,15 @@ EXTENSION("standard", "array.c base64.c basic_functions.c browscap.c \
crypt_sha512.c php_crypt_r.c " + (TARGET_ARCH != 'arm64'? " crc32_x86.c" : "") + " \
datetime.c dir.c dl.c dns.c dns_win32.c exec.c \
file.c filestat.c formatted_print.c fsock.c head.c html.c image.c \
- info.c iptc.c link.c mail.c math.c md5.c metaphone.c microtime.c \
+ info.c io_poll.c iptc.c link.c mail.c math.c md5.c metaphone.c microtime.c \
net.c pack.c pageinfo.c quot_print.c soundex.c \
string.c scanf.c syslog.c type.c uniqid.c url.c var.c \
versioning.c assert.c strnatcmp.c levenshtein.c incomplete_class.c \
url_scanner_ex.c ftp_fopen_wrapper.c http_fopen_wrapper.c \
php_fopen_wrapper.c credits.c css.c var_unserializer.c ftok.c sha1.c \
user_filters.c uuencode.c filters.c proc_open.c password.c \
- streamsfuncs.c http.c flock_compat.c hrtime.c", false /* never shared */,
+ streamsfuncs.c http.c flock_compat.c hrtime.c",
+ false /* never shared */,
'/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');
ADD_SOURCES("ext/standard/libavifinfo", "avifinfo.c", "standard");
PHP_STANDARD = "yes";
diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c
new file mode 100644
index 00000000000..05e54696d69
--- /dev/null
+++ b/ext/standard/io_poll.c
@@ -0,0 +1,928 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright (c) The PHP Group |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | https://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | license@php.net so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+ | Author: Jakub Zelenka <bukka@php.net> |
+ +----------------------------------------------------------------------+
+*/
+
+#include "php.h"
+#include "zend_enum.h"
+#include "zend_exceptions.h"
+#include "php_network.h"
+#include "php_poll.h"
+#include "io_poll_arginfo.h"
+
+/* Class entries */
+static zend_class_entry *php_io_poll_backend_class_entry;
+static zend_class_entry *php_io_poll_event_class_entry;
+static zend_class_entry *php_io_poll_context_class_entry;
+static zend_class_entry *php_io_poll_watcher_class_entry;
+static zend_class_entry *php_io_poll_handle_class_entry;
+static zend_class_entry *php_io_exception_class_entry;
+static zend_class_entry *php_io_poll_exception_class_entry;
+static zend_class_entry *php_io_poll_failed_backend_unavailable_class_entry;
+static zend_class_entry *php_io_poll_failed_operation_class_entry;
+static zend_class_entry *php_io_poll_failed_context_init_class_entry;
+static zend_class_entry *php_io_poll_failed_handle_add_class_entry;
+static zend_class_entry *php_io_poll_failed_watcher_mod_class_entry;
+static zend_class_entry *php_io_poll_failed_wait_class_entry;
+static zend_class_entry *php_io_poll_inactive_watcher_class_entry;
+static zend_class_entry *php_io_poll_handle_already_watched_class_entry;
+static zend_class_entry *php_io_poll_invalid_handle_class_entry;
+static zend_class_entry *php_stream_poll_handle_class_entry;
+
+/* Object handlers */
+static zend_object_handlers php_io_poll_context_object_handlers;
+static zend_object_handlers php_io_poll_watcher_object_handlers;
+static zend_object_handlers php_io_poll_handle_object_handlers;
+
+/* Watcher object structure */
+typedef struct {
+ php_poll_handle_object *handle;
+ uint32_t watched_events;
+ uint32_t triggered_events;
+ zval data;
+ bool active;
+ php_poll_ctx *poll_ctx; /* Back reference to poll context */
+ zend_object std;
+} php_io_poll_watcher_object;
+
+/* Context object structure */
+typedef struct {
+ php_poll_ctx *ctx;
+ HashTable *watchers; /* Maps handle pointer -> watcher object */
+ zend_object std;
+} php_io_poll_context_object;
+
+/* Stream poll handle specific data */
+typedef struct {
+ php_stream *stream;
+} php_stream_poll_handle_data;
+
+/* Accessor macros */
+#define PHP_POLL_CONTEXT_OBJ_FROM_ZOBJ(_obj) \
+ ((php_io_poll_context_object *) ((char *) (_obj) - offsetof(php_io_poll_context_object, std)))
+
+#define PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(_obj) \
+ ((php_io_poll_watcher_object *) ((char *) (_obj) - offsetof(php_io_poll_watcher_object, std)))
+
+#define PHP_POLL_WATCHER_OBJ_FROM_ZV(_zv) PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(Z_OBJ_P(_zv))
+#define PHP_POLL_CONTEXT_OBJ_FROM_ZV(_zv) PHP_POLL_CONTEXT_OBJ_FROM_ZOBJ(Z_OBJ_P(_zv))
+
+/* Helper to throw failed operation exceptions with error code */
+static inline void php_io_poll_throw_failed_operation(
+ zend_class_entry *ce, const char *message, php_poll_error error)
+{
+ zend_throw_exception(ce, message, (zend_long) error);
+}
+
+/* Event enum to bit mask mapping */
+static uint32_t php_io_poll_event_enum_to_bit(zend_object *event_enum)
+{
+ zval *case_name = zend_enum_fetch_case_name(event_enum);
+ const char *name = Z_STRVAL_P(case_name);
+
+ if (strcmp(name, "Read") == 0) {
+ return PHP_POLL_READ;
+ } else if (strcmp(name, "Write") == 0) {
+ return PHP_POLL_WRITE;
+ } else if (strcmp(name, "Error") == 0) {
+ return PHP_POLL_ERROR;
+ } else if (strcmp(name, "HangUp") == 0) {
+ return PHP_POLL_HUP;
+ } else if (strcmp(name, "ReadHangUp") == 0) {
+ return PHP_POLL_RDHUP;
+ } else if (strcmp(name, "OneShot") == 0) {
+ return PHP_POLL_ONESHOT;
+ } else if (strcmp(name, "EdgeTriggered") == 0) {
+ return PHP_POLL_ET;
+ }
+
+ return 0;
+}
+
+static uint32_t php_io_poll_event_enums_to_events(zval *event_enums)
+{
+ HashTable *ht;
+ zval *entry;
+ uint32_t events = 0;
+
+ if (Z_TYPE_P(event_enums) != IS_ARRAY) {
+ return 0;
+ }
+
+ ht = Z_ARRVAL_P(event_enums);
+
+ ZEND_HASH_FOREACH_VAL(ht, entry) {
+ if (Z_TYPE_P(entry) != IS_OBJECT
+ || !instanceof_function(Z_OBJCE_P(entry), php_io_poll_event_class_entry)) {
+ return 0;
+ }
+ events |= php_io_poll_event_enum_to_bit(Z_OBJ_P(entry));
+ }
+ ZEND_HASH_FOREACH_END();
+
+ return events;
+}
+
+static zend_result php_io_poll_events_to_event_enums(uint32_t events, zval *event_enums)
+{
+ zval enum_case;
+
+ array_init(event_enums);
+
+ if (events & PHP_POLL_READ) {
+ ZVAL_OBJ(&enum_case, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Read"));
+ GC_ADDREF(Z_OBJ(enum_case));
+ add_next_index_zval(event_enums, &enum_case);
+ }
+ if (events & PHP_POLL_WRITE) {
+ ZVAL_OBJ(&enum_case, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Write"));
+ GC_ADDREF(Z_OBJ(enum_case));
+ add_next_index_zval(event_enums, &enum_case);
+ }
+ if (events & PHP_POLL_ERROR) {
+ ZVAL_OBJ(&enum_case, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Error"));
+ GC_ADDREF(Z_OBJ(enum_case));
+ add_next_index_zval(event_enums, &enum_case);
+ }
+ if (events & PHP_POLL_HUP) {
+ ZVAL_OBJ(&enum_case, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "HangUp"));
+ GC_ADDREF(Z_OBJ(enum_case));
+ add_next_index_zval(event_enums, &enum_case);
+ }
+ if (events & PHP_POLL_RDHUP) {
+ ZVAL_OBJ(&enum_case, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "ReadHangUp"));
+ GC_ADDREF(Z_OBJ(enum_case));
+ add_next_index_zval(event_enums, &enum_case);
+ }
+ if (events & PHP_POLL_ONESHOT) {
+ ZVAL_OBJ(&enum_case, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "OneShot"));
+ GC_ADDREF(Z_OBJ(enum_case));
+ add_next_index_zval(event_enums, &enum_case);
+ }
+ if (events & PHP_POLL_ET) {
+ ZVAL_OBJ(&enum_case, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "EdgeTriggered"));
+ GC_ADDREF(Z_OBJ(enum_case));
+ add_next_index_zval(event_enums, &enum_case);
+ }
+
+ return SUCCESS;
+}
+
+/* Backend enum name to backend type mapping */
+static php_poll_backend_type php_io_poll_backend_enum_to_type(zend_object *backend_enum)
+{
+ zval *case_name = zend_enum_fetch_case_name(backend_enum);
+ const char *name = Z_STRVAL_P(case_name);
+
+ if (strcmp(name, "Auto") == 0) {
+ return PHP_POLL_BACKEND_AUTO;
+ } else if (strcmp(name, "Poll") == 0) {
+ return PHP_POLL_BACKEND_POLL;
+ } else if (strcmp(name, "Epoll") == 0) {
+ return PHP_POLL_BACKEND_EPOLL;
+ } else if (strcmp(name, "Kqueue") == 0) {
+ return PHP_POLL_BACKEND_KQUEUE;
+ } else if (strcmp(name, "EventPorts") == 0) {
+ return PHP_POLL_BACKEND_EVENTPORT;
+ } else if (strcmp(name, "WSAPoll") == 0) {
+ return PHP_POLL_BACKEND_WSAPOLL;
+ }
+
+ return PHP_POLL_BACKEND_AUTO;
+}
+
+static const char *php_io_poll_backend_type_to_name(php_poll_backend_type type)
+{
+ switch (type) {
+ case PHP_POLL_BACKEND_POLL:
+ return "Poll";
+ case PHP_POLL_BACKEND_EPOLL:
+ return "Epoll";
+ case PHP_POLL_BACKEND_KQUEUE:
+ return "Kqueue";
+ case PHP_POLL_BACKEND_EVENTPORT:
+ return "EventPorts";
+ case PHP_POLL_BACKEND_WSAPOLL:
+ return "WSAPoll";
+ case PHP_POLL_BACKEND_AUTO:
+ default:
+ return "Auto";
+ }
+}
+
+/* Stream Poll Handle Implementation */
+
+static php_socket_t php_stream_poll_handle_get_fd(php_poll_handle_object *handle)
+{
+ php_stream_poll_handle_data *data = (php_stream_poll_handle_data *) handle->handle_data;
+ php_socket_t fd;
+
+ if (!data || !data->stream) {
+ return SOCK_ERR;
+ }
+
+ if (php_stream_cast(data->stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL,
+ (void *) &fd, 1)
+ != SUCCESS
+ || fd == -1) {
+ return SOCK_ERR;
+ }
+
+ return fd;
+}
+
+static int php_stream_poll_handle_is_valid(php_poll_handle_object *handle)
+{
+ php_stream_poll_handle_data *data = (php_stream_poll_handle_data *) handle->handle_data;
+ return data && data->stream && !php_stream_eof(data->stream);
+}
+
+static void php_stream_poll_handle_cleanup(php_poll_handle_object *handle)
+{
+ php_stream_poll_handle_data *data = (php_stream_poll_handle_data *) handle->handle_data;
+ if (data) {
+ /* Don't close the stream - user still owns it */
+ efree(data);
+ handle->handle_data = NULL;
+ }
+}
+
+static php_poll_handle_ops php_stream_poll_handle_ops = {
+ .get_fd = php_stream_poll_handle_get_fd,
+ .is_valid = php_stream_poll_handle_is_valid,
+ .cleanup = php_stream_poll_handle_cleanup
+};
+
+/* Handle interface internal only */
+static int php_stream_poll_handle_implement_interface(zend_class_entry *interface, zend_class_entry *implementor)
+{
+ if (implementor->type == ZEND_USER_CLASS) {
+ zend_error_noreturn(E_ERROR, "Io\\Poll\\Handle cannot be implemented by user classes");
+ return FAILURE;
+ }
+
+ return SUCCESS;
+}
+
+/* Object Creation Functions */
+
+static zend_object *php_stream_poll_handle_create_object(zend_class_entry *ce)
+{
+ php_poll_handle_object *intern = php_poll_handle_object_create(
+ sizeof(php_poll_handle_object), ce, &php_stream_poll_handle_ops);
+ return &intern->std;
+}
+
+static zend_object *php_io_poll_watcher_create_object(zend_class_entry *ce)
+{
+ php_io_poll_watcher_object *intern = zend_object_alloc(sizeof(php_io_poll_watcher_object), ce);
+
+ zend_object_std_init(&intern->std, ce);
+ object_properties_init(&intern->std, ce);
+
+ intern->handle = NULL;
+ intern->watched_events = 0;
+ intern->triggered_events = 0;
+ intern->active = false;
+ intern->poll_ctx = NULL;
+ ZVAL_NULL(&intern->data);
+
+ return &intern->std;
+}
+
+static zend_object *php_io_poll_context_create_object(zend_class_entry *ce)
+{
+ php_io_poll_context_object *intern = zend_object_alloc(sizeof(php_io_poll_context_object), ce);
+
+ zend_object_std_init(&intern->std, ce);
+ object_properties_init(&intern->std, ce);
+
+ intern->ctx = NULL;
+ intern->watchers = NULL;
+
+ return &intern->std;
+}
+
+/* Object Destruction Functions */
+
+static void php_io_poll_watcher_free_object(zend_object *obj)
+{
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(obj);
+
+ zval_ptr_dtor(&intern->data);
+
+ if (intern->handle) {
+ OBJ_RELEASE(&intern->handle->std);
+ }
+
+ zend_object_std_dtor(&intern->std);
+}
+
+static void php_io_poll_context_free_object(zend_object *obj)
+{
+ php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZOBJ(obj);
+
+ if (intern->ctx) {
+ php_poll_destroy(intern->ctx);
+ }
+
+ if (intern->watchers) {
+ zend_hash_destroy(intern->watchers);
+ efree(intern->watchers);
+ }
+
+ zend_object_std_dtor(&intern->std);
+}
+
+/* Utility functions */
+
+static zend_always_inline zend_ulong php_io_poll_compute_ptr_key(void *ptr)
+{
+ zend_ulong key = (zend_ulong) (uintptr_t) ptr;
+ return (key >> 3) | (key << ((sizeof(key) * 8) - 3));
+}
+
+static zend_result php_io_poll_watcher_modify_events(
+ php_io_poll_watcher_object *watcher, uint32_t events)
+{
+ if (!watcher->active || !watcher->poll_ctx) {
+ zend_throw_exception(
+ php_io_poll_inactive_watcher_class_entry, "Cannot modify inactive watcher", 0);
+ return FAILURE;
+ }
+
+ php_socket_t fd = php_poll_handle_get_fd(watcher->handle);
+ if (fd == SOCK_ERR) {
+ zend_throw_exception(
+ php_io_poll_invalid_handle_class_entry, "Invalid handle for polling", 0);
+ return FAILURE;
+ }
+
+ /* Modify in poll context */
+ if (php_poll_modify(watcher->poll_ctx, (int) fd, events, watcher) != SUCCESS) {
+ php_poll_error err = php_poll_get_error(watcher->poll_ctx);
+ php_io_poll_throw_failed_operation(php_io_poll_failed_watcher_mod_class_entry,
+ "Failed to modify watcher in polling system", err);
+ return FAILURE;
+ }
+
+ /* Update watcher state */
+ watcher->watched_events = events;
+
+ return SUCCESS;
+}
+
+static zend_result php_io_poll_watcher_modify_data(php_io_poll_watcher_object *watcher, zval *data)
+{
+ if (!watcher->active) {
+ zend_throw_exception(
+ php_io_poll_inactive_watcher_class_entry, "Cannot modify inactive watcher", 0);
+ return FAILURE;
+ }
+
+ /* Update user data */
+ zval_ptr_dtor(&watcher->data);
+ ZVAL_COPY(&watcher->data, data);
+
+ return SUCCESS;
+}
+
+/* PHP Method Implementations */
+
+PHP_METHOD(Io_Poll_Backend, getAvailableBackends)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ array_init(return_value);
+
+ /* Check each backend type for availability */
+ php_poll_backend_type backends[] = {PHP_POLL_BACKEND_POLL, PHP_POLL_BACKEND_EPOLL,
+ PHP_POLL_BACKEND_KQUEUE, PHP_POLL_BACKEND_EVENTPORT, PHP_POLL_BACKEND_WSAPOLL};
+
+ for (size_t i = 0; i < sizeof(backends) / sizeof(backends[0]); i++) {
+ if (php_poll_is_backend_available(backends[i])) {
+ const char *name = php_io_poll_backend_type_to_name(backends[i]);
+ zval enum_case;
+ ZVAL_OBJ(&enum_case, zend_enum_get_case_cstr(php_io_poll_backend_class_entry, name));
+ add_next_index_zval(return_value, &enum_case);
+ }
+ }
+}
+
+PHP_METHOD(Io_Poll_Backend, isAvailable)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ zend_object *enum_obj = Z_OBJ_P(ZEND_THIS);
+ php_poll_backend_type type = php_io_poll_backend_enum_to_type(enum_obj);
+
+ RETURN_BOOL(php_poll_is_backend_available(type));
+}
+
+PHP_METHOD(Io_Poll_Backend, supportsEdgeTriggering)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ zend_object *enum_obj = Z_OBJ_P(ZEND_THIS);
+ php_poll_backend_type type = php_io_poll_backend_enum_to_type(enum_obj);
+
+ RETURN_BOOL(php_poll_backend_supports_edge_triggering(type));
+}
+
+PHP_METHOD(StreamPollHandle, __construct)
+{
+ php_stream *stream;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ PHP_Z_PARAM_STREAM(stream)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis());
+
+ /* Set up stream-specific data */
+ php_stream_poll_handle_data *data = emalloc(sizeof(php_stream_poll_handle_data));
+ data->stream = stream;
+ intern->handle_data = data;
+
+ /* Add reference to stream */
+ GC_ADDREF(stream->res);
+}
+
+PHP_METHOD(StreamPollHandle, getStream)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis());
+ php_stream_poll_handle_data *data = (php_stream_poll_handle_data *) intern->handle_data;
+
+ if (!data || !data->stream) {
+ RETURN_NULL();
+ }
+
+ GC_ADDREF(data->stream->res);
+ php_stream_to_zval(data->stream, return_value);
+}
+
+PHP_METHOD(StreamPollHandle, isValid)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis());
+ RETURN_BOOL(intern->ops->is_valid(intern));
+}
+
+PHP_METHOD(StreamPollHandle, getFileDescriptor)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis());
+ php_socket_t fd = php_poll_handle_get_fd(intern);
+
+ if (fd == SOCK_ERR) {
+ RETURN_LONG(0);
+ }
+
+ RETURN_LONG((zend_long) fd);
+}
+
+PHP_METHOD(Io_Poll_Watcher, __construct)
+{
+ zend_throw_error(NULL, "Cannot directly construct Watcher, use Context::add");
+}
+
+PHP_METHOD(Io_Poll_Watcher, getHandle)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
+ if (!intern->handle) {
+ RETURN_NULL();
+ }
+
+ RETURN_OBJ_COPY(&intern->handle->std);
+}
+
+PHP_METHOD(Io_Poll_Watcher, getWatchedEvents)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
+ php_io_poll_events_to_event_enums(intern->watched_events, return_value);
+}
+
+PHP_METHOD(Io_Poll_Watcher, getTriggeredEvents)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
+ php_io_poll_events_to_event_enums(intern->triggered_events, return_value);
+}
+
+PHP_METHOD(Io_Poll_Watcher, getData)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
+ ZVAL_COPY(return_value, &intern->data);
+}
+
+PHP_METHOD(Io_Poll_Watcher, hasTriggered)
+{
+ zval *event_enum;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_OBJECT_OF_CLASS(event_enum, php_io_poll_event_class_entry)
+ ZEND_PARSE_PARAMETERS_END();
+
+ uint32_t event_bit = php_io_poll_event_enum_to_bit(Z_OBJ_P(event_enum));
+
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
+ RETURN_BOOL((intern->triggered_events & event_bit) != 0);
+}
+
+PHP_METHOD(Io_Poll_Watcher, isActive)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
+ RETURN_BOOL(intern->active);
+}
+
+PHP_METHOD(Io_Poll_Watcher, modify)
+{
+ zval *event_enums;
+ zval *data = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(1, 2)
+ Z_PARAM_ARRAY(event_enums)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_ZVAL(data)
+ ZEND_PARSE_PARAMETERS_END();
+
+ uint32_t events = php_io_poll_event_enums_to_events(event_enums);
+ if (!events) {
+ zend_argument_type_error(1, "must be array of Event enums");
+ RETURN_THROWS();
+ }
+
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
+
+ /* Modify events first */
+ if (php_io_poll_watcher_modify_events(intern, events) != SUCCESS) {
+ RETURN_THROWS();
+ }
+
+ /* Then modify data if provided */
+ if (data) {
+ if (php_io_poll_watcher_modify_data(intern, data) != SUCCESS) {
+ RETURN_THROWS();
+ }
+ }
+}
+
+PHP_METHOD(Io_Poll_Watcher, modifyEvents)
+{
+ zval *event_enums;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_ARRAY(event_enums)
+ ZEND_PARSE_PARAMETERS_END();
+
+ uint32_t events = php_io_poll_event_enums_to_events(event_enums);
+ if (!events) {
+ zend_argument_type_error(1, "must be array of Event enums");
+ RETURN_THROWS();
+ }
+
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
+
+ if (php_io_poll_watcher_modify_events(intern, events) != SUCCESS) {
+ RETURN_THROWS();
+ }
+}
+
+PHP_METHOD(Io_Poll_Watcher, modifyData)
+{
+ zval *data;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_ZVAL(data)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
+
+ if (php_io_poll_watcher_modify_data(intern, data) != SUCCESS) {
+ RETURN_THROWS();
+ }
+}
+
+PHP_METHOD(Io_Poll_Watcher, remove)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis());
+
+ if (!intern->active || !intern->poll_ctx) {
+ zend_throw_exception(
+ php_io_poll_inactive_watcher_class_entry, "Cannot remove inactive watcher", 0);
+ RETURN_THROWS();
+ }
+
+ php_socket_t fd = php_poll_handle_get_fd(intern->handle);
+ if (fd != SOCK_ERR) {
+ php_poll_remove(intern->poll_ctx, (int) fd);
+ }
+
+ intern->active = false;
+ intern->poll_ctx = NULL;
+}
+
+PHP_METHOD(Io_Poll_Context, __construct)
+{
+ zval *backend_obj = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_OBJECT_OF_CLASS(backend_obj, php_io_poll_backend_class_entry)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis());
+
+ php_poll_backend_type backend_type = PHP_POLL_BACKEND_AUTO;
+ if (backend_obj != NULL) {
+ backend_type = php_io_poll_backend_enum_to_type(Z_OBJ_P(backend_obj));
+ }
+
+ intern->ctx = php_poll_create(backend_type, 0);
+
+ if (!intern->ctx) {
+ zend_throw_exception_ex(
+ php_io_poll_failed_backend_unavailable_class_entry, 0, "Backend %s not available",
+ php_io_poll_backend_type_to_name(backend_type));
+ RETURN_THROWS();
+ }
+
+ if (php_poll_init(intern->ctx) != SUCCESS) {
+ php_poll_error err = php_poll_get_error(intern->ctx);
+ php_poll_destroy(intern->ctx);
+ intern->ctx = NULL;
+ php_io_poll_throw_failed_operation(php_io_poll_failed_context_init_class_entry,
+ "Failed to initialize polling context", err);
+ RETURN_THROWS();
+ }
+
+ intern->watchers = emalloc(sizeof(HashTable));
+ zend_hash_init(intern->watchers, 8, NULL, ZVAL_PTR_DTOR, 0);
+}
+
+PHP_METHOD(Io_Poll_Context, add)
+{
+ zval *handle_obj, *event_enums;
+ uint32_t events;
+ zval *data = NULL;
+
+ ZEND_PARSE_PARAMETERS_START(2, 3)
+ Z_PARAM_OBJECT_OF_CLASS(handle_obj, php_io_poll_handle_class_entry)
+ Z_PARAM_ARRAY(event_enums)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_ZVAL(data)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis());
+ php_poll_handle_object *handle = PHP_POLL_HANDLE_OBJ_FROM_ZV(handle_obj);
+
+ /* Get file descriptor */
+ php_socket_t fd = php_poll_handle_get_fd(handle);
+ if (fd == SOCK_ERR) {
+ zend_throw_exception(
+ php_io_poll_invalid_handle_class_entry, "Invalid handle for polling", 0);
+ RETURN_THROWS();
+ }
+
+ /* Create watcher object */
+ object_init_ex(return_value, php_io_poll_watcher_class_entry);
+ php_io_poll_watcher_object *watcher = PHP_POLL_WATCHER_OBJ_FROM_ZV(return_value);
+
+ events = php_io_poll_event_enums_to_events(event_enums);
+ if (!events) {
+ zend_argument_type_error(2, "must be array of Event enums");
+ RETURN_THROWS();
+ }
+
+ watcher->handle = handle;
+ watcher->watched_events = events;
+ watcher->triggered_events = 0;
+ watcher->active = true;
+ watcher->poll_ctx = intern->ctx;
+
+ GC_ADDREF(&handle->std);
+
+ if (data) {
+ ZVAL_COPY(&watcher->data, data);
+ } else {
+ ZVAL_NULL(&watcher->data);
+ }
+
+ /* Add to poll context */
+ if (php_poll_add(intern->ctx, (int) fd, events, watcher) != SUCCESS) {
+ php_poll_error err = php_poll_get_error(intern->ctx);
+ if (err == PHP_POLL_ERR_EXISTS) {
+ zend_throw_exception(
+ php_io_poll_handle_already_watched_class_entry, "Handle already added", 0);
+ } else {
+ php_io_poll_throw_failed_operation(
+ php_io_poll_failed_handle_add_class_entry, "Failed to add handle", err);
+ }
+ RETURN_THROWS();
+ }
+
+ /* Store in our watchers map using shifted pointer as key */
+ zval watcher_zv;
+ ZVAL_OBJ(&watcher_zv, &watcher->std);
+ GC_ADDREF(&watcher->std);
+
+ zend_ulong hash_key = php_io_poll_compute_ptr_key(handle);
+ zend_hash_index_add(intern->watchers, hash_key, &watcher_zv);
+}
+
+PHP_METHOD(Io_Poll_Context, wait)
+{
+ zend_long timeout_seconds = -1;
+ bool timeout_seconds_is_null = true;
+ zend_long timeout_microseconds = 0;
+ zend_long max_events = 0;
+ bool max_events_is_null = true;
+
+ ZEND_PARSE_PARAMETERS_START(0, 3)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG_OR_NULL(timeout_seconds, timeout_seconds_is_null)
+ Z_PARAM_LONG(timeout_microseconds)
+ Z_PARAM_LONG_OR_NULL(max_events, max_events_is_null)
+ ZEND_PARSE_PARAMETERS_END();
+
+ php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis());
+
+ /* Build timespec from seconds + microseconds, or NULL for indefinite */
+ struct timespec ts;
+ const struct timespec *timeout = NULL;
+ if (timeout_seconds >= 0) {
+ if (timeout_microseconds < 0) {
+ zend_argument_value_error(2, "must be greater than or equal to 0");
+ RETURN_THROWS();
+ }
+
+ /* Allow microseconds >= 1000000, carry overflow into seconds
+ * (same behavior as stream_select) */
+ ts.tv_sec = (time_t) (timeout_seconds + (timeout_microseconds / 1000000));
+ ts.tv_nsec = (long) ((timeout_microseconds % 1000000) * 1000);
+ timeout = &ts;
+ } else if (!timeout_seconds_is_null) {
+ zend_argument_value_error(1, "must be greater than or equal to 0");
+ RETURN_THROWS();
+ }
+
+ if (max_events_is_null) {
+ max_events = php_poll_get_suitable_max_events(intern->ctx);
+ if (max_events <= 0) {
+ max_events = 64;
+ }
+ } else if (max_events <= 0) {
+ zend_argument_value_error(3, "must be greater than 0");
+ RETURN_THROWS();
+ }
+
+ php_poll_event *events = emalloc(sizeof(php_poll_event) * max_events);
+ int num_events = php_poll_wait(intern->ctx, events, (int) max_events, timeout);
+
+ if (num_events < 0) {
+ php_poll_error err = php_poll_get_error(intern->ctx);
+ efree(events);
+ php_io_poll_throw_failed_operation(
+ php_io_poll_failed_wait_class_entry, "Poll wait failed", err);
+ RETURN_THROWS();
+ }
+
+ array_init(return_value);
+
+ for (int i = 0; i < num_events; i++) {
+ php_io_poll_watcher_object *watcher = (php_io_poll_watcher_object *) events[i].data;
+ if (watcher) {
+ watcher->triggered_events = events[i].revents;
+
+ zval watcher_zv;
+ ZVAL_OBJ(&watcher_zv, &watcher->std);
+ GC_ADDREF(&watcher->std);
+
+ add_next_index_zval(return_value, &watcher_zv);
+ }
+ }
+
+ efree(events);
+}
+
+PHP_METHOD(Io_Poll_Context, getBackend)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis());
+ php_poll_backend_type backend_type = php_poll_get_backend_type(intern->ctx);
+ const char *backend_name = php_io_poll_backend_type_to_name(backend_type);
+
+ RETURN_OBJ_COPY(zend_enum_get_case_cstr(php_io_poll_backend_class_entry, backend_name));
+}
+
+/* Initialize the stream poll classes - add to PHP_MINIT_FUNCTION */
+PHP_MINIT_FUNCTION(poll)
+{
+ /* Register backend enum */
+ php_io_poll_backend_class_entry = register_class_Io_Poll_Backend();
+
+ /* Register event enum */
+ php_io_poll_event_class_entry = register_class_Io_Poll_Event();
+
+ /* Register Handle interface */
+ php_io_poll_handle_class_entry = register_class_Io_Poll_Handle();
+ php_io_poll_handle_class_entry->interface_gets_implemented = php_stream_poll_handle_implement_interface;
+
+ /* Register StreamPollHandle class */
+ php_stream_poll_handle_class_entry
+ = register_class_StreamPollHandle(php_io_poll_handle_class_entry);
+ php_stream_poll_handle_class_entry->create_object = php_stream_poll_handle_create_object;
+
+ memcpy(&php_io_poll_handle_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
+ php_io_poll_handle_object_handlers.offset = offsetof(php_poll_handle_object, std);
+ php_io_poll_handle_object_handlers.free_obj = php_poll_handle_object_free;
+ php_stream_poll_handle_class_entry->default_object_handlers = &php_io_poll_handle_object_handlers;
+
+ /* Register Watcher class */
+ php_io_poll_watcher_class_entry = register_class_Io_Poll_Watcher();
+ php_io_poll_watcher_class_entry->create_object = php_io_poll_watcher_create_object;
+
+ memcpy(&php_io_poll_watcher_object_handlers, &std_object_handlers,
+ sizeof(zend_object_handlers));
+ php_io_poll_watcher_object_handlers.offset = offsetof(php_io_poll_watcher_object, std);
+ php_io_poll_watcher_object_handlers.free_obj = php_io_poll_watcher_free_object;
+ php_io_poll_watcher_class_entry->default_object_handlers = &php_io_poll_watcher_object_handlers;
+
+ /* Register Context class */
+ php_io_poll_context_class_entry = register_class_Io_Poll_Context();
+ php_io_poll_context_class_entry->create_object = php_io_poll_context_create_object;
+
+ memcpy(&php_io_poll_context_object_handlers, &std_object_handlers,
+ sizeof(zend_object_handlers));
+ php_io_poll_context_object_handlers.offset = offsetof(php_io_poll_context_object, std);
+ php_io_poll_context_object_handlers.free_obj = php_io_poll_context_free_object;
+ php_io_poll_context_class_entry->default_object_handlers = &php_io_poll_context_object_handlers;
+
+ /* Register exception hierarchy */
+ php_io_exception_class_entry = register_class_Io_IoException(zend_ce_exception);
+
+ php_io_poll_exception_class_entry
+ = register_class_Io_Poll_PollException(php_io_exception_class_entry);
+
+ php_io_poll_failed_operation_class_entry = register_class_Io_Poll_FailedPollOperationException(
+ php_io_poll_exception_class_entry);
+
+ php_io_poll_failed_context_init_class_entry
+ = register_class_Io_Poll_FailedContextInitializationException(
+ php_io_poll_failed_operation_class_entry);
+
+ php_io_poll_failed_handle_add_class_entry = register_class_Io_Poll_FailedHandleAddException(
+ php_io_poll_failed_operation_class_entry);
+
+ php_io_poll_failed_watcher_mod_class_entry
+ = register_class_Io_Poll_FailedWatcherModificationException(
+ php_io_poll_failed_operation_class_entry);
+
+ php_io_poll_failed_wait_class_entry = register_class_Io_Poll_FailedPollWaitException(
+ php_io_poll_failed_operation_class_entry);
+
+ php_io_poll_failed_backend_unavailable_class_entry = register_class_Io_Poll_BackendUnavailableException(
+ php_io_poll_exception_class_entry);
+
+ php_io_poll_inactive_watcher_class_entry = register_class_Io_Poll_InactiveWatcherException(
+ php_io_poll_exception_class_entry);
+
+ php_io_poll_handle_already_watched_class_entry
+ = register_class_Io_Poll_HandleAlreadyWatchedException(
+ php_io_poll_exception_class_entry);
+
+ php_io_poll_invalid_handle_class_entry
+ = register_class_Io_Poll_InvalidHandleException(php_io_poll_exception_class_entry);
+
+ /* Initialize polling backends */
+ php_poll_register_backends();
+
+ return SUCCESS;
+}
diff --git a/ext/standard/io_poll.stub.php b/ext/standard/io_poll.stub.php
new file mode 100644
index 00000000000..689099a0b39
--- /dev/null
+++ b/ext/standard/io_poll.stub.php
@@ -0,0 +1,162 @@
+<?php
+
+/** @generate-class-entries */
+
+namespace Io {
+ class IoException extends \Exception {}
+}
+
+namespace Io\Poll {
+
+ enum Backend
+ {
+ case Auto;
+ case Poll;
+ case Epoll;
+ case Kqueue;
+ case EventPorts;
+ case WSAPoll;
+
+ /** @return list<Backend> */
+ static public function getAvailableBackends(): array {}
+
+ public function isAvailable(): bool {}
+
+ public function supportsEdgeTriggering(): bool {}
+ }
+
+ enum Event {
+ case Read;
+ case Write;
+ case Error;
+ case HangUp;
+ case ReadHangUp;
+ case OneShot;
+ case EdgeTriggered;
+ }
+
+ interface Handle
+ {
+ }
+
+ /*
+ * @strict-properties
+ * @not-serializable
+ */
+ final class Watcher
+ {
+ private final function __construct() {}
+
+ public function getHandle(): Handle {}
+
+ /** @return list<Event> */
+ public function getWatchedEvents(): array {}
+
+ /** @return list<Event> */
+ public function getTriggeredEvents(): array {}
+
+ public function getData(): mixed {}
+
+ public function hasTriggered(Event $event): bool {}
+
+ public function isActive(): bool {}
+
+ public function modify(array $events, mixed $data = null): void {}
+
+ public function modifyEvents(array $events): void {}
+
+ public function modifyData(mixed $data): void {}
+
+ public function remove(): void {}
+ }
+
+ /*
+ * @strict-properties
+ * @not-serializable
+ */
+ final class Context
+ {
+ public function __construct(Backend $backend = Backend::Auto) {}
+
+ public function add(Handle $handle, array $events, mixed $data = null): Watcher {}
+
+ /** @return list<Watcher> */
+ public function wait(?int $timeoutSeconds = null, int $timeoutMicroseconds = 0, ?int $maxEvents = null): array {}
+
+ public function getBackend(): Backend {}
+ }
+
+ class PollException extends \Io\IoException {}
+
+ abstract class FailedPollOperationException extends PollException
+ {
+ /** @cvalue PHP_POLL_ERROR_CODE_NONE */
+ public const int ERROR_NONE = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_SYSTEM */
+ public const int ERROR_SYSTEM = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_NOMEM */
+ public const int ERROR_NOMEM = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_INVALID */
+ public const int ERROR_INVALID = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_EXISTS */
+ public const int ERROR_EXISTS = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_NOTFOUND */
+ public const int ERROR_NOTFOUND = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_TIMEOUT */
+ public const int ERROR_TIMEOUT = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_INTERRUPTED */
+ public const int ERROR_INTERRUPTED = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_PERMISSION */
+ public const int ERROR_PERMISSION = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_TOOBIG */
+ public const int ERROR_TOOBIG = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_AGAIN */
+ public const int ERROR_AGAIN = UNKNOWN;
+
+ /** @cvalue PHP_POLL_ERROR_CODE_NOSUPPORT */
+ public const int ERROR_NOSUPPORT = UNKNOWN;
+ }
+
+ class FailedContextInitializationException extends FailedPollOperationException {}
+
+ class FailedHandleAddException extends FailedPollOperationException {}
+
+ class FailedWatcherModificationException extends FailedPollOperationException {}
+
+ class FailedPollWaitException extends FailedPollOperationException {}
+
+ class BackendUnavailableException extends PollException {}
+
+ class InactiveWatcherException extends PollException {}
+
+ class HandleAlreadyWatchedException extends PollException {}
+
+ class InvalidHandleException extends PollException {}
+}
+
+namespace {
+ /*
+ * @strict-properties
+ * @not-serializable
+ */
+ final class StreamPollHandle implements Io\Poll\Handle
+ {
+ /** @param resource $stream */
+ public function __construct($stream) {}
+
+ /** @return resource */
+ public function getStream() {}
+
+ public function isValid(): bool {}
+ }
+}
diff --git a/ext/standard/io_poll_arginfo.h b/ext/standard/io_poll_arginfo.h
new file mode 100644
index 00000000000..5272a2b2739
Binary files /dev/null and b/ext/standard/io_poll_arginfo.h differ
diff --git a/ext/standard/tests/poll/poll.inc b/ext/standard/tests/poll/poll.inc
new file mode 100644
index 00000000000..a1ca01fc7d4
--- /dev/null
+++ b/ext/standard/tests/poll/poll.inc
@@ -0,0 +1,321 @@
+<?php
+// stream Poll Testing helper
+
+function pt_new_stream_poll(): Io\Poll\Context {
+ $backend = getenv('POLL_TEST_BACKEND');
+ if ($backend === false) {
+ return new Io\Poll\Context(Io\Poll\Backend::Auto);
+ }
+
+ // Map string to enum case using match
+ $backend_enum = match(strtolower($backend)) {
+ 'auto' => Io\Poll\Backend::Auto,
+ 'poll' => Io\Poll\Backend::Poll,
+ 'epoll' => Io\Poll\Backend::Epoll,
+ 'kqueue' => Io\Poll\Backend::Kqueue,
+ 'eventports' => Io\Poll\Backend::EventPorts,
+ 'wsapoll' => Io\Poll\Backend::WSAPoll,
+ default => throw new ValueError("Unknown backend: $backend"),
+ };
+
+ return new Io\Poll\Context($backend_enum);
+}
+
+function pt_stream_poll_add($poll_ctx, $stream, $events, $data = null): Io\Poll\Watcher {
+ return $poll_ctx->add(new StreamPollHandle($stream), $events, $data);
+}
+
+function pt_skip_for_backend($backend, $msg): void {
+ $backends_to_skip = is_array($backend) ? $backend : array($backend);
+ $current_backend = pt_new_stream_poll()->getBackend();
+ if (in_array($current_backend->name, $backends_to_skip)) {
+ die("skip backend {$current_backend->name} $msg\n");
+ }
+}
+
+function pt_new_socket_pair(): array {
+ $domain = (strtoupper(substr(PHP_OS, 0, 3) == 'WIN') ? STREAM_PF_INET : STREAM_PF_UNIX);
+ $sockets = stream_socket_pair($domain, STREAM_SOCK_STREAM, 0);
+ if ($sockets === false) {
+ die("Cannot create socket pair\n");
+ }
+ return $sockets;
+}
+
+function pt_new_tcp_socket_pair(): array {
+ $server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr);
+ if (!$server) {
+ die("Cannot create TCP server: $errstr\n");
+ }
+ $address = stream_socket_get_name($server, false);
+
+ $client = stream_socket_client("tcp://$address", $errno, $errstr);
+ if (!$client) {
+ fclose($server);
+ die("Cannot connect to TCP server: $errstr\n");
+ }
+
+ $server_conn = stream_socket_accept($server);
+ if (!$server_conn) {
+ fclose($server);
+ fclose($client);
+ die("Cannot accept connection\n");
+ }
+
+ // Close the listening socket (no longer needed)
+ fclose($server);
+
+ return [$client, $server_conn];
+}
+
+function pt_new_tcp_socket_connections(int $num_conns): array {
+ $server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr);
+ if (!$server) {
+ die("Cannot create TCP server: $errstr\n");
+ }
+ $address = stream_socket_get_name($server, false);
+
+ $clients = [];
+ $server_conns = [];
+ for ($i = 0; $i < $num_conns; ++$i) {
+ $clients[$i] = stream_socket_client("tcp://$address", $errno, $errstr);
+ if (!$clients[$i]) {
+ fclose($server);
+ die("Cannot connect to TCP server: $errstr\n");
+ }
+
+ $server_conns[$i] = stream_socket_accept($server);
+ if (!$server_conns[$i]) {
+ fclose($server);
+ die("Cannot accept connection\n");
+ }
+ }
+
+ // Close the listening socket (no longer needed)
+ fclose($server);
+
+ return [$clients, $server_conns];
+}
+
+function pt_write_sleep($stream, $data, $delay = 10000): int|false {
+ $result = fwrite($stream, $data);
+ usleep($delay);
+ return $result;
+}
+
+function pt_event_array_to_string(array $events): string {
+ $names = [];
+ foreach ($events as $event) {
+ $names[] = $event->name;
+ }
+ return empty($names) ? 'NONE' : implode('|', $names);
+}
+
+function pt_print_events($watchers, $read_data = false): void {
+ if (!is_array($watchers)) {
+ die("Events must be an array\n");
+ }
+ echo "Events count: " . count($watchers) . "\n";
+ foreach ($watchers as $i => $watcher) {
+ if (!$watcher instanceof Io\Poll\Watcher) {
+ die('Invalid event type');
+ }
+ echo "Event[$i]: " . pt_event_array_to_string($watcher->getTriggeredEvents()) . ", user data: " . $watcher->getData();
+ if ($read_data && $watcher->hasTriggered(Io\Poll\Event::Read)) {
+ $data = fread($watcher->getHandle()->getStream(), 1024);
+ echo ", read data: '$data'";
+ }
+ echo "\n";
+ }
+}
+
+function pt_expect_events($watchers, $expected, ?Io\Poll\Context $poll_ctx = null): void {
+ if (!is_array($watchers)) {
+ die("Events must be an array\n");
+ }
+
+ if (!is_array($expected)) {
+ die("Expected events must be an array\n");
+ }
+
+ $event_count = count($watchers);
+ $expected_count = count($expected);
+
+ // Get the current backend for backend-specific expectations
+ $backend = $poll_ctx ? $poll_ctx->getBackend()->name : 'unknown';
+
+ if ($event_count !== $expected_count) {
+ echo "Event count mismatch: got $event_count, expected $expected_count\n";
+ pt_print_mismatched_events($watchers, $expected, [], $backend);
+ return;
+ }
+
+ // Convert events to comparable format for matching
+ $actual_events = [];
+ foreach ($watchers as $watcher) {
+ if (!$watcher instanceof Io\Poll\Watcher) {
+ die('Invalid event type');
+ }
+
+ $event_data = [
+ 'events' => $watcher->getTriggeredEvents(),
+ 'data' => $watcher->getData(),
+ ];
+
+ $actual_events[] = $event_data;
+ }
+
+ // Resolve backend-specific expectations
+ $resolved_expected = [];
+ foreach ($expected as $exp_event) {
+ $resolved_event = $exp_event;
+
+ // Check if events field is a backend-specific map (associative array with string keys)
+ if (isset($exp_event['events']) && is_array($exp_event['events'])) {
+ // Check if it's a backend map (has string keys like 'default', 'Kqueue', etc.)
+ $keys = array_keys($exp_event['events']);
+ $is_backend_map = false;
+ foreach ($keys as $key) {
+ if (is_string($key)) {
+ $is_backend_map = true;
+ break;
+ }
+ }
+
+ if ($is_backend_map) {
+ // It's a backend-specific map, resolve it
+ $resolved_event['events'] = pt_resolve_backend_specific_value($exp_event['events'], $backend);
+ }
+ // Otherwise it's already an array of Event enums, leave it as-is
+ }
+
+ $resolved_expected[] = $resolved_event;
+ }
+
+ // Try to match each expected event with an actual event
+ $matched = [];
+ $unmatched_expected = [];
+
+ foreach ($resolved_expected as $exp_idx => $exp_event) {
+ $found_match = false;
+
+ foreach ($actual_events as $act_idx => $act_event) {
+ if (isset($matched[$act_idx])) {
+ continue; // Already matched
+ }
+
+ // Check if events and data match
+ if (pt_events_equal($act_event['events'], $exp_event['events']) &&
+ $act_event['data'] === $exp_event['data']) {
+
+ // If read data is expected, check it
+ if (isset($exp_event['read'])) {
+ $read_data = fread($watchers[$act_idx]->getHandle()->getStream(), 1024);
+ if ($read_data !== $exp_event['read']) {
+ continue; // Read data doesn't match
+ }
+ }
+
+ $matched[$act_idx] = $exp_idx;
+ $found_match = true;
+ break;
+ }
+ }
+
+ if (!$found_match) {
+ $unmatched_expected[] = $exp_event;
+ }
+ }
+
+ // Check if all events matched
+ if (count($matched) === $event_count && empty($unmatched_expected)) {
+ echo "Events matched - count: $event_count\n";
+ } else {
+ echo "Events did not match:\n";
+ pt_print_mismatched_events($watchers, $expected, $matched, $backend);
+ }
+}
+
+function pt_events_equal($actual, $expected): bool {
+ // Both should be arrays of Event enums
+ if (!is_array($actual) || !is_array($expected)) {
+ return false;
+ }
+
+ if (count($actual) !== count($expected)) {
+ return false;
+ }
+
+ // Sort both arrays by event name for comparison
+ $actual_names = array_map(fn($e) => $e->name, $actual);
+ $expected_names = array_map(fn($e) => $e->name, $expected);
+ sort($actual_names);
+ sort($expected_names);
+
+ return $actual_names === $expected_names;
+}
+
+function pt_resolve_backend_specific_value($backend_map, $current_backend) {
+ // Direct backend match
+ if (isset($backend_map[$current_backend])) {
+ return $backend_map[$current_backend];
+ }
+
+ // Check for multi-backend keys (e.g., "Kqueue|EventPorts")
+ foreach ($backend_map as $key => $value) {
+ if (strpos($key, '|') !== false) {
+ $backends = array_map('trim', explode('|', $key));
+ if (in_array($current_backend, $backends)) {
+ return $value;
+ }
+ }
+ }
+
+ // Fall back to default
+ if (isset($backend_map['default'])) {
+ return $backend_map['default'];
+ }
+
+ // If no match found, this is an error
+ die("No backend-specific value found for '$current_backend' and no default specified\n");
+}
+
+function pt_print_mismatched_events($actual_watchers, $expected_watchers, $matched = [], $backend_name = null): void {
+ echo "Actual events:\n";
+ foreach ($actual_watchers as $i => $watcher) {
+ $match_status = isset($matched[$i]) ? " [MATCHED]" : " [UNMATCHED]";
+ $watcher_names = pt_event_array_to_string($watcher->getTriggeredEvents());
+ echo " Event[$i]: $watcher_names, user data: " . $watcher->getData() . $match_status . "\n";
+ }
+
+ echo "Expected events:\n";
+ foreach ($expected_watchers as $i => $exp_event) {
+ $was_matched = in_array($i, $matched);
+ $match_status = $was_matched ? " [MATCHED]" : " [UNMATCHED]";
+
+ $events_value = $exp_event['events'];
+
+ // Check if it's a backend-specific map
+ if (is_array($events_value) && $backend_name !== null) {
+ $keys = array_keys($events_value);
+ $is_backend_map = false;
+ foreach ($keys as $key) {
+ if (is_string($key)) {
+ $is_backend_map = true;
+ break;
+ }
+ }
+
+ if ($is_backend_map) {
+ $events_value = pt_resolve_backend_specific_value($events_value, $backend_name);
+ }
+ }
+
+ $watcher_names = is_array($events_value) ? pt_event_array_to_string($events_value) : 'INVALID';
+ echo " Event[$i]: $watcher_names, user data: " . $exp_event['data'];
+ if (isset($exp_event['read'])) {
+ echo ", read data: '" . $exp_event['read'] . "'";
+ }
+ echo $match_status . "\n";
+ }
+}
diff --git a/ext/standard/tests/poll/poll_ctx_backend_unix.phpt b/ext/standard/tests/poll/poll_ctx_backend_unix.phpt
new file mode 100644
index 00000000000..89f4f3eea10
--- /dev/null
+++ b/ext/standard/tests/poll/poll_ctx_backend_unix.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Poll context - backend on Unix
+--SKIPIF--
+<?php
+if (substr(PHP_OS, 0, 3) == 'WIN') {
+ die ("skip not for Windows");
+}
+?>
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+// this just prints default poll ctx
+$poll_ctx = new Io\Poll\Context();
+var_dump($poll_ctx->getBackend());
+// test with poll that is always available on Unix systems
+$poll_ctx = new Io\Poll\Context(Io\Poll\Backend::Poll);
+$backend = $poll_ctx->getBackend();
+var_dump($backend->name);
+try {
+ new Io\Poll\Context(Io\Poll\Backend::WSAPoll);
+} catch (\Io\Poll\BackendUnavailableException $e) {
+ var_dump($e->getMessage());
+}
+?>
+--EXPECTF--
+enum(Io\Poll\Backend::%s)
+string(4) "Poll"
+string(29) "Backend WSAPoll not available"
diff --git a/ext/standard/tests/poll/poll_ctx_backend_windows.phpt b/ext/standard/tests/poll/poll_ctx_backend_windows.phpt
new file mode 100644
index 00000000000..a8f8498800c
--- /dev/null
+++ b/ext/standard/tests/poll/poll_ctx_backend_windows.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Poll context - backend on Windows
+--SKIPIF--
+<?php
+if (substr(PHP_OS, 0, 3) != 'WIN') {
+ die ("skip only for Windows");
+}
+?>
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+// this just prints default poll ctx
+$poll_ctx = new Io\Poll\Context();
+var_dump($poll_ctx->getBackend());
+// test with WSAPoll
+$poll_ctx = new Io\Poll\Context(Io\Poll\Backend::WSAPoll);
+$backend = $poll_ctx->getBackend();
+var_dump($backend->name);
+try {
+ new Io\Poll\Context(Io\Poll\Backend::Epoll);
+} catch (\Io\Poll\BackendUnavailableException $e) {
+ var_dump($e->getMessage());
+}
+?>
+--EXPECTF--
+enum(Io\Poll\Backend::%s)
+string(7) "WSAPoll"
+string(27) "Backend Epoll not available"
diff --git a/ext/standard/tests/poll/poll_stream_add_error_duplicate.phpt b/ext/standard/tests/poll/poll_stream_add_error_duplicate.phpt
new file mode 100644
index 00000000000..8753cad7eb8
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_add_error_duplicate.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Poll stream - add duplicite error
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write], "socket2_data");
+
+try {
+ pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write], "socket2_data");
+} catch (Io\Poll\HandleAlreadyWatchedException $e) {
+ echo "ERROR: " . $e->getMessage() . "\n";
+}
+?>
+--EXPECT--
+ERROR: Handle already added
diff --git a/ext/standard/tests/poll/poll_stream_sock_add_only.phpt b/ext/standard/tests/poll/poll_stream_sock_add_only.phpt
new file mode 100644
index 00000000000..b282ae84632
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_add_only.phpt
@@ -0,0 +1,17 @@
+--TEST--
+Poll stream - only add
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1, $socket2) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $socket2, [Io\Poll\Event::Write], "socket2_data");
+
+var_dump($poll_ctx);
+
+?>
+--EXPECT--
+object(Io\Poll\Context)#1 (0) {
+}
diff --git a/ext/standard/tests/poll/poll_stream_sock_modify_write.phpt b/ext/standard/tests/poll/poll_stream_sock_modify_write.phpt
new file mode 100644
index 00000000000..4f32ec28c88
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_modify_write.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Poll stream - socket modify write
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1, $socket2) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+$watcher = pt_stream_poll_add($poll_ctx, $socket2, [Io\Poll\Event::Write], "socket_data");
+$watcher->modify([Io\Poll\Event::Write], "modified_data");
+
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'modified_data']
+]);
+?>
+--EXPECT--
+Events matched - count: 1
diff --git a/ext/standard/tests/poll/poll_stream_sock_read.phpt b/ext/standard/tests/poll/poll_stream_sock_read.phpt
new file mode 100644
index 00000000000..70aebc9cd92
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_read.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Poll stream - socket read
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $socket1r, [Io\Poll\Event::Read], "socket_data");
+
+fwrite($socket1w, "test data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket_data', 'read' => 'test data']
+]);
+
+?>
+--EXPECT--
+Events matched - count: 1
diff --git a/ext/standard/tests/poll/poll_stream_sock_remove_write.phpt b/ext/standard/tests/poll/poll_stream_sock_remove_write.phpt
new file mode 100644
index 00000000000..c5fec539165
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_remove_write.phpt
@@ -0,0 +1,38 @@
+--TEST--
+Poll stream - socket remove write
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_socket_pair();
+list($socket2r, $socket2w) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+$watcher1w = pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write], "socket_data_1");
+pt_stream_poll_add($poll_ctx, $socket2w, [Io\Poll\Event::Write], "socket_data_2");
+
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket_data_1'],
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket_data_2']
+]);
+
+$watcher1w->remove();
+
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket_data_2']
+]);
+
+// check that both streams are still usable
+var_dump(fwrite($socket1w, "test 1"));
+var_dump(fwrite($socket2w, "test 2"));
+var_dump(fread($socket1r, 100));
+var_dump(fread($socket2r, 100));
+
+?>
+--EXPECT--
+Events matched - count: 2
+Events matched - count: 1
+int(6)
+int(6)
+string(6) "test 1"
+string(6) "test 2"
diff --git a/ext/standard/tests/poll/poll_stream_sock_rw_close.phpt b/ext/standard/tests/poll/poll_stream_sock_rw_close.phpt
new file mode 100644
index 00000000000..4edd1619d30
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_rw_close.phpt
@@ -0,0 +1,32 @@
+--TEST--
+Poll stream - socket write / read close
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $socket1r, [Io\Poll\Event::Read], "socket1_data");
+pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write], "socket2_data");
+
+fwrite($socket1w, "test data");
+
+fclose($socket1r);
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ [
+ 'events' => [
+ 'default' => [Io\Poll\Event::Write, Io\Poll\Event::Error, Io\Poll\Event::HangUp],
+ 'Kqueue|EventPorts' => [Io\Poll\Event::Write, Io\Poll\Event::HangUp],
+ ],
+ 'data' => 'socket2_data'
+ ]
+], $poll_ctx);
+
+fclose($socket1w);
+pt_expect_events($poll_ctx->wait(0, 100000), []);
+
+?>
+--EXPECT--
+Events matched - count: 1
+Events matched - count: 0
diff --git a/ext/standard/tests/poll/poll_stream_sock_rw_multi_edge.phpt b/ext/standard/tests/poll/poll_stream_sock_rw_multi_edge.phpt
new file mode 100644
index 00000000000..f25cb973ddc
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_rw_multi_edge.phpt
@@ -0,0 +1,62 @@
+--TEST--
+Poll stream - socket write / read multiple times with edge triggering
+--SKIPIF--
+<?php
+require_once __DIR__ . '/poll.inc';
+pt_skip_for_backend(['Poll', 'WSAPoll', 'EventPorts'], 'does not support edge triggering')
+?>
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $socket1r, [Io\Poll\Event::Read, Io\Poll\Event::EdgeTriggered], "socket1_data");
+pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write, Io\Poll\Event::EdgeTriggered], "socket2_data");
+
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data']
+]);
+
+pt_expect_events($poll_ctx->wait(0), []);
+
+fwrite($socket1w, "test data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket1_data', 'read' => 'test data']
+]);
+
+fwrite($socket1w, "more data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data'],
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket1_data']
+]);
+
+pt_expect_events($poll_ctx->wait(0, 100000), []);
+
+fwrite($socket1w, " and even more data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket1_data', 'read' => 'more data and even more data']
+]);
+
+fclose($socket1r);
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ [
+ 'events' => ['default' => [Io\Poll\Event::Write, Io\Poll\Event::HangUp]],
+ 'data' => 'socket2_data'
+ ]
+], $poll_ctx);
+
+fclose($socket1w);
+pt_expect_events($poll_ctx->wait(0, 100000), []);
+
+?>
+--EXPECT--
+Events matched - count: 1
+Events matched - count: 0
+Events matched - count: 1
+Events matched - count: 2
+Events matched - count: 0
+Events matched - count: 1
+Events matched - count: 1
+Events matched - count: 0
diff --git a/ext/standard/tests/poll/poll_stream_sock_rw_multi_level.phpt b/ext/standard/tests/poll/poll_stream_sock_rw_multi_level.phpt
new file mode 100644
index 00000000000..57fc335c26c
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_rw_multi_level.phpt
@@ -0,0 +1,61 @@
+--TEST--
+Poll stream - socket write / read multiple times with level triggering
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $socket1r, [Io\Poll\Event::Read], "socket1_data");
+pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write], "socket2_data");
+
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data']
+]);
+
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data']
+]);
+
+fwrite($socket1w, "test data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data'],
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket1_data', 'read' => 'test data']
+]);
+
+fwrite($socket1w, "more data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data'],
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket1_data']
+]);
+
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data'],
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket1_data']
+]);
+
+fwrite($socket1w, " and even more data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data'],
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket1_data', 'read' => 'more data and even more data']
+]);
+
+fclose($socket1r);
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Write, Io\Poll\Event::HangUp], 'data' => 'socket2_data']
+], $poll_ctx);
+
+fclose($socket1w);
+pt_expect_events($poll_ctx->wait(0, 100000), []);
+
+?>
+--EXPECT--
+Events matched - count: 1
+Events matched - count: 1
+Events matched - count: 2
+Events matched - count: 2
+Events matched - count: 2
+Events matched - count: 2
+Events matched - count: 1
+Events matched - count: 0
diff --git a/ext/standard/tests/poll/poll_stream_sock_rw_single_edge.phpt b/ext/standard/tests/poll/poll_stream_sock_rw_single_edge.phpt
new file mode 100644
index 00000000000..4a7b67b8243
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_rw_single_edge.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Poll stream - socket write / read few time only
+--SKIPIF--
+<?php
+require_once __DIR__ . '/poll.inc';
+pt_skip_for_backend(['Poll', 'WSAPoll', 'EventPorts'], 'does not support edge triggering')
+?>
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $socket1r, [Io\Poll\Event::Read, Io\Poll\Event::EdgeTriggered], "socket1_data");
+pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write, Io\Poll\Event::EdgeTriggered], "socket2_data");
+
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data']
+]);
+fwrite($socket1w, "test data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket1_data', 'read' => 'test data']
+]);
+
+?>
+--EXPECT--
+Events matched - count: 1
+Events matched - count: 1
diff --git a/ext/standard/tests/poll/poll_stream_sock_rw_single_level.phpt b/ext/standard/tests/poll/poll_stream_sock_rw_single_level.phpt
new file mode 100644
index 00000000000..a807e573c5f
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_rw_single_level.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Poll stream - socket write / read few time only
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $socket1r, [Io\Poll\Event::Read], "socket1_data");
+pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write], "socket2_data");
+
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data']
+]);
+fwrite($socket1w, "test data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket2_data'],
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket1_data', 'read' => 'test data']
+]);
+
+?>
+--EXPECT--
+Events matched - count: 1
+Events matched - count: 2
diff --git a/ext/standard/tests/poll/poll_stream_sock_write.phpt b/ext/standard/tests/poll/poll_stream_sock_write.phpt
new file mode 100644
index 00000000000..2d937d7e9a3
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_write.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Poll stream - socket write
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write], "socket_data");
+
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'socket_data']
+]);
+
+?>
+--EXPECT--
+Events matched - count: 1
diff --git a/ext/standard/tests/poll/poll_stream_sock_write_close.phpt b/ext/standard/tests/poll/poll_stream_sock_write_close.phpt
new file mode 100644
index 00000000000..8927e017b05
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_sock_write_close.phpt
@@ -0,0 +1,20 @@
+--TEST--
+Poll stream - socket write close
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+list($socket2r, $socket2w) = pt_new_socket_pair();
+
+pt_stream_poll_add($poll_ctx, $socket1w, [Io\Poll\Event::Write], "socket1w_data");
+pt_stream_poll_add($poll_ctx, $socket2w, [Io\Poll\Event::Write], "socket2w_data");
+
+fclose($socket1w);
+fclose($socket2w);
+pt_expect_events($poll_ctx->wait(0, 100000), []);
+
+?>
+--EXPECT--
+Events matched - count: 0
diff --git a/ext/standard/tests/poll/poll_stream_tcp_read.phpt b/ext/standard/tests/poll/poll_stream_tcp_read.phpt
new file mode 100644
index 00000000000..71c7af0ca73
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_tcp_read.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Poll stream - socket read
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($socket1r, $socket1w) = pt_new_tcp_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $socket1r, [Io\Poll\Event::Read], "socket_data");
+
+pt_write_sleep($socket1w, "test data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Read], 'data' => 'socket_data', 'read' => 'test data']
+]);
+
+?>
+--EXPECT--
+Events matched - count: 1
diff --git a/ext/standard/tests/poll/poll_stream_tcp_read_multiple_level.phpt b/ext/standard/tests/poll/poll_stream_tcp_read_multiple_level.phpt
new file mode 100644
index 00000000000..db2ff71fb7c
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_tcp_read_multiple_level.phpt
@@ -0,0 +1,41 @@
+--TEST--
+Poll stream - TCP read write level
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($clients, $servers) = pt_new_tcp_socket_connections(20);
+$poll_ctx = pt_new_stream_poll();
+
+for ($i = 0; $i < count($servers); $i++) {
+ pt_stream_poll_add($poll_ctx, $servers[$i], [Io\Poll\Event::Read], "server{$i}_data");
+}
+
+pt_expect_events($poll_ctx->wait(0), []);
+
+for ($i = 0; $i < count($clients); $i++) {
+ pt_write_sleep($clients[$i], "test $i data");
+}
+
+// Build expected events for all 20 connections
+$expected_events = [];
+for ($i = 0; $i < 20; $i++) {
+ $expected_events[] = ['events' => [Io\Poll\Event::Read], 'data' => "server{$i}_data", 'read' => "test $i data"];
+}
+pt_expect_events($poll_ctx->wait(0, 100000), $expected_events);
+
+pt_write_sleep($clients[1], "more data");
+pt_write_sleep($clients[2], "more data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Read], 'data' => 'server1_data', 'read' => 'more data'],
+ ['events' => [Io\Poll\Event::Read], 'data' => 'server2_data', 'read' => 'more data']
+]);
+
+pt_expect_events($poll_ctx->wait(0, 100000), []);
+
+?>
+--EXPECT--
+Events matched - count: 0
+Events matched - count: 20
+Events matched - count: 2
+Events matched - count: 0
diff --git a/ext/standard/tests/poll/poll_stream_tcp_read_one_shot.phpt b/ext/standard/tests/poll/poll_stream_tcp_read_one_shot.phpt
new file mode 100644
index 00000000000..02db6355ca1
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_tcp_read_one_shot.phpt
@@ -0,0 +1,33 @@
+--TEST--
+Poll stream - TCP read write oneshot
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($client1, $server1) = pt_new_tcp_socket_pair();
+list($client2, $server2) = pt_new_tcp_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $client1, [Io\Poll\Event::Read, Io\Poll\Event::OneShot], "client1_data");
+pt_stream_poll_add($poll_ctx, $server1, [Io\Poll\Event::Read, Io\Poll\Event::OneShot], "server1_data");
+pt_stream_poll_add($poll_ctx, $client2, [Io\Poll\Event::Read, Io\Poll\Event::OneShot], "client2_data");
+pt_stream_poll_add($poll_ctx, $server2, [Io\Poll\Event::Read, Io\Poll\Event::OneShot], "server2_data");
+
+pt_expect_events($poll_ctx->wait(0), []);
+
+pt_write_sleep($client1, "test data");
+pt_write_sleep($client2, "test data");
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Read], 'data' => 'server1_data', 'read' => 'test data'],
+ ['events' => [Io\Poll\Event::Read], 'data' => 'server2_data', 'read' => 'test data']
+]);
+
+pt_write_sleep($client1, "more data");
+pt_write_sleep($client2, "more data");
+pt_expect_events($poll_ctx->wait(0, 100000), []);
+
+?>
+--EXPECT--
+Events matched - count: 0
+Events matched - count: 2
+Events matched - count: 0
diff --git a/ext/standard/tests/poll/poll_stream_tcp_rw_one_shot.phpt b/ext/standard/tests/poll/poll_stream_tcp_rw_one_shot.phpt
new file mode 100644
index 00000000000..6132975e1b5
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_tcp_rw_one_shot.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Poll stream - TCP read write oneshot combined
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($client, $server) = pt_new_tcp_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $client, [Io\Poll\Event::Read, Io\Poll\Event::Write, Io\Poll\Event::OneShot], "client_data");
+pt_stream_poll_add($poll_ctx, $server, [Io\Poll\Event::Read, Io\Poll\Event::Write, Io\Poll\Event::OneShot], "server_data");
+
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'client_data'],
+ ['events' => [Io\Poll\Event::Write], 'data' => 'server_data']
+]);
+
+pt_write_sleep($client, "test data");
+pt_expect_events($poll_ctx->wait(0, 100000), []);
+
+pt_write_sleep($client, "test data");
+pt_expect_events($poll_ctx->wait(0, 100000), []);
+
+?>
+--EXPECT--
+Events matched - count: 2
+Events matched - count: 0
+Events matched - count: 0
diff --git a/ext/standard/tests/poll/poll_stream_tcp_rw_one_shot_mixed.phpt b/ext/standard/tests/poll/poll_stream_tcp_rw_one_shot_mixed.phpt
new file mode 100644
index 00000000000..c480caf1b73
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_tcp_rw_one_shot_mixed.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Poll stream - TCP read write oneshot combined and mixed
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($client, $server) = pt_new_tcp_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $client, [Io\Poll\Event::Read, Io\Poll\Event::Write, Io\Poll\Event::OneShot], "client_data");
+pt_stream_poll_add($poll_ctx, $server, [Io\Poll\Event::Read, Io\Poll\Event::Write, Io\Poll\Event::OneShot], "server_data");
+
+pt_write_sleep($client, "test data");
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'client_data'],
+ ['events' => [Io\Poll\Event::Read, Io\Poll\Event::Write], 'data' => 'server_data']
+]);
+
+pt_write_sleep($client, "test data");
+pt_expect_events($poll_ctx->wait(0, 100000), []);
+
+?>
+--EXPECT--
+Events matched - count: 2
+Events matched - count: 0
diff --git a/ext/standard/tests/poll/poll_stream_tcp_rw_single_level.phpt b/ext/standard/tests/poll/poll_stream_tcp_rw_single_level.phpt
new file mode 100644
index 00000000000..f29a74f19ed
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_tcp_rw_single_level.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Poll stream - TCP read write level combined
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+list($client, $server) = pt_new_tcp_socket_pair();
+$poll_ctx = pt_new_stream_poll();
+
+pt_stream_poll_add($poll_ctx, $client, [Io\Poll\Event::Read, Io\Poll\Event::Write], "client_data");
+pt_stream_poll_add($poll_ctx, $server, [Io\Poll\Event::Read, Io\Poll\Event::Write], "server_data");
+
+pt_expect_events($poll_ctx->wait(0), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'client_data'],
+ ['events' => [Io\Poll\Event::Write], 'data' => 'server_data']
+]);
+
+fwrite($client, "test data");
+usleep(10000);
+pt_expect_events($poll_ctx->wait(0, 100000), [
+ ['events' => [Io\Poll\Event::Write], 'data' => 'client_data'],
+ ['events' => [Io\Poll\Event::Read, Io\Poll\Event::Write], 'data' => 'server_data', 'read' => 'test data']
+]);
+
+?>
+--EXPECT--
+Events matched - count: 2
+Events matched - count: 2
diff --git a/ext/standard/tests/poll/poll_stream_wait_no_add.phpt b/ext/standard/tests/poll/poll_stream_wait_no_add.phpt
new file mode 100644
index 00000000000..a297fa5e116
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_wait_no_add.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Poll stream - only wait
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+$poll_ctx = pt_new_stream_poll();
+$events = $poll_ctx->wait(0, 100000);
+pt_print_events($events);
+
+?>
+--EXPECT--
+Events count: 0
diff --git a/main/php_poll.h b/main/php_poll.h
index e329c703399..254f885a6d3 100644
--- a/main/php_poll.h
+++ b/main/php_poll.h
@@ -159,7 +159,7 @@ struct php_poll_handle_object {
};
#define PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(obj) \
- ((php_poll_handle_object *) ((char *) (obj) - XtOffsetOf(php_poll_handle_object, std)))
+ ((php_poll_handle_object *) ((char *) (obj) - offsetof(php_poll_handle_object, std)))
#define PHP_POLL_HANDLE_OBJ_FROM_ZV(zv) PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(Z_OBJ_P(zv))