From 4d735db36405b76951144481a11e6488967e471b Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 15 Aug 2025 21:00:13 +0200 Subject: [PATCH 01/52] Add initial poll API --- main/php_poll.h | 136 ++++++++++++ main/poll/poll_backend_epoll.c | 191 +++++++++++++++++ main/poll/poll_backend_iocp.c | 243 ++++++++++++++++++++++ main/poll/poll_backend_kqueue.c | 197 ++++++++++++++++++ main/poll/poll_backend_poll.c | 196 ++++++++++++++++++ main/poll/poll_backend_port.c | 303 +++++++++++++++++++++++++++ main/poll/poll_backend_select.c | 245 ++++++++++++++++++++++ main/poll/poll_core.c | 353 ++++++++++++++++++++++++++++++++ 8 files changed, 1864 insertions(+) create mode 100644 main/php_poll.h create mode 100644 main/poll/poll_backend_epoll.c create mode 100644 main/poll/poll_backend_iocp.c create mode 100644 main/poll/poll_backend_kqueue.c create mode 100644 main/poll/poll_backend_poll.c create mode 100644 main/poll/poll_backend_port.c create mode 100644 main/poll/poll_backend_select.c create mode 100644 main/poll/poll_core.c diff --git a/main/php_poll.h b/main/php_poll.h new file mode 100644 index 0000000000000..eb656dcd6ab9d --- /dev/null +++ b/main/php_poll.h @@ -0,0 +1,136 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_POLL_H +#define PHP_POLL_H + +#include "php.h" + +/* Event types */ +#define PHP_POLL_READ 0x01 +#define PHP_POLL_WRITE 0x02 +#define PHP_POLL_ERROR 0x04 +#define PHP_POLL_HUP 0x08 +#define PHP_POLL_RDHUP 0x10 +#define PHP_POLL_ONESHOT 0x20 +#define PHP_POLL_ET 0x40 /* Edge-triggered */ + +/* Poll backend types */ +typedef enum { + PHP_POLL_BACKEND_AUTO = -1, + PHP_POLL_BACKEND_POLL = 0, + PHP_POLL_BACKEND_EPOLL, + PHP_POLL_BACKEND_KQUEUE, + PHP_POLL_BACKEND_EVENTPORT, + PHP_POLL_BACKEND_SELECT, + PHP_POLL_BACKEND_IOCP +} php_poll_backend_type; + +/* Error codes */ +#define PHP_POLL_OK 0 +#define PHP_POLL_ERROR -1 +#define PHP_POLL_NOMEM -2 +#define PHP_POLL_INVALID -3 +#define PHP_POLL_EXISTS -4 +#define PHP_POLL_NOTFOUND -5 +#define PHP_POLL_TIMEOUT -6 + +/* Forward declarations */ +typedef struct php_poll_ctx php_poll_ctx; +typedef struct php_poll_fd_entry php_poll_fd_entry; + +/* Poll event structure */ +typedef struct { + int fd; /* File descriptor */ + uint32_t events; /* Requested events */ + uint32_t revents; /* Returned events */ + void *data; /* User data pointer */ +} php_poll_event_t; + +/* FD entry for tracking state */ +struct php_poll_fd_entry { + int fd; + uint32_t events; + uint32_t last_revents; /* For edge-trigger simulation */ + void *data; + bool active; + bool et_armed; /* Edge-trigger state */ +}; + +/* Backend interface */ +typedef struct php_poll_backend_ops { + const char *name; + + /* Initialize backend */ + int (*init)(php_poll_ctx *ctx, int max_events); + + /* Cleanup backend */ + void (*cleanup)(php_poll_ctx *ctx); + + /* Add file descriptor */ + int (*add)(php_poll_ctx *ctx, int fd, uint32_t events, void *data); + + /* Modify file descriptor */ + int (*modify)(php_poll_ctx *ctx, int fd, uint32_t events, void *data); + + /* Remove file descriptor */ + int (*remove)(php_poll_ctx *ctx, int fd); + + /* Wait for events */ + int (*wait)(php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout); + + /* Check if backend is available */ + bool (*is_available)(void); + + /* 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; + + int max_events; + int num_fds; + bool initialized; + bool simulate_et; /* Whether to simulate edge triggering */ + + /* FD tracking for edge-trigger simulation */ + php_poll_fd_entry *fd_entries; + int fd_entries_size; + + /* Backend-specific data */ + void *backend_data; +}; + +/* Public API */ +php_poll_ctx* php_poll_create(int max_events, php_poll_backend_type preferred_backend); +void php_poll_destroy(php_poll_ctx *ctx); + +int php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data); +int php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data); +int php_poll_remove(php_poll_ctx *ctx, int fd); + +int php_poll_wait(php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout); + +const char* php_poll_backend_name(php_poll_ctx *ctx); +php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx); +bool php_poll_supports_et(php_poll_ctx *ctx); + +/* Backend registration */ +void php_poll_register_backends(void); +const php_poll_backend_ops* php_poll_get_backend_ops(php_poll_backend_type backend); + +#endif /* PHP_POLL_H */ diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c new file mode 100644 index 0000000000000..461ff4c39c8bc --- /dev/null +++ b/main/poll/poll_backend_epoll.c @@ -0,0 +1,191 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll.h" + +#ifdef HAVE_EPOLL + +#include + +typedef struct { + int epoll_fd; + struct epoll_event *events; +} 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 int epoll_backend_init(php_poll_ctx *ctx, int max_events) +{ + epoll_backend_data_t *data = calloc(1, sizeof(epoll_backend_data_t)); + if (!data) { + return PHP_POLL_NOMEM; + } + + data->epoll_fd = epoll_create1(EPOLL_CLOEXEC); + if (data->epoll_fd == -1) { + free(data); + return PHP_POLL_ERROR; + } + + data->events = calloc(max_events, sizeof(struct epoll_event)); + if (!data->events) { + close(data->epoll_fd); + free(data); + return PHP_POLL_NOMEM; + } + + ctx->backend_data = data; + return PHP_POLL_OK; +} + +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); + } + free(data->events); + free(data); + ctx->backend_data = NULL; + } +} + +static int 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) { + return (errno == EEXIST) ? PHP_POLL_EXISTS : PHP_POLL_ERROR; + } + + return PHP_POLL_OK; +} + +static int 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) { + return (errno == ENOENT) ? PHP_POLL_NOTFOUND : PHP_POLL_ERROR; + } + + return PHP_POLL_OK; +} + +static int 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) { + return (errno == ENOENT) ? PHP_POLL_NOTFOUND : PHP_POLL_ERROR; + } + + return PHP_POLL_OK; +} + +static int epoll_backend_wait( + php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) +{ + epoll_backend_data_t *backend_data = (epoll_backend_data_t *) ctx->backend_data; + + int nfds = epoll_wait(backend_data->epoll_fd, backend_data->events, max_events, timeout); + + if (nfds > 0) { + for (int i = 0; i < nfds; i++) { + events[i].fd = backend_data->events[i].data.fd; + events[i].events = 0; /* Not used in results */ + events[i].revents = epoll_events_from_native(backend_data->events[i].events); + events[i].data = backend_data->events[i].data.ptr; + } + } + + return nfds; +} + +static 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 = { .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, + .supports_et = true }; + +#endif /* HAVE_EPOLL */ diff --git a/main/poll/poll_backend_iocp.c b/main/poll/poll_backend_iocp.c new file mode 100644 index 0000000000000..a62cad87aea0c --- /dev/null +++ b/main/poll/poll_backend_iocp.c @@ -0,0 +1,243 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll.h" + +#ifdef _WIN32 + +#include +#include +#include + +typedef struct iocp_operation { + OVERLAPPED overlapped; + int fd; + uint32_t events; + void *user_data; + char buffer[1]; /* Minimal buffer for accept/recv operations */ +} iocp_operation_t; + +typedef struct { + HANDLE iocp_handle; + iocp_operation_t *operations; + int max_operations; + int operation_count; + LPFN_ACCEPTEX AcceptEx; + LPFN_CONNECTEX ConnectEx; + LPFN_GETACCEPTEXSOCKADDRS GetAcceptExSockaddrs; +} iocp_backend_data_t; + +static int iocp_backend_init(php_poll_ctx *ctx, int max_events) +{ + iocp_backend_data_t *data = calloc(1, sizeof(iocp_backend_data_t)); + if (!data) { + return PHP_POLL_NOMEM; + } + + /* Create I/O Completion Port */ + data->iocp_handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); + if (data->iocp_handle == NULL) { + free(data); + return PHP_POLL_ERROR; + } + + data->max_operations = max_events; + data->operations = calloc(max_events, sizeof(iocp_operation_t)); + + if (!data->operations) { + CloseHandle(data->iocp_handle); + free(data); + return PHP_POLL_NOMEM; + } + + /* Load Winsock extension functions */ + SOCKET dummy_socket = socket(AF_INET, SOCK_STREAM, 0); + if (dummy_socket != INVALID_SOCKET) { + GUID acceptex_guid = WSAID_ACCEPTEX; + GUID connectex_guid = WSAID_CONNECTEX; + GUID getacceptexsockaddrs_guid = WSAID_GETACCEPTEXSOCKADDRS; + DWORD bytes; + + WSAIoctl(dummy_socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &acceptex_guid, + sizeof(acceptex_guid), &data->AcceptEx, sizeof(data->AcceptEx), &bytes, NULL, NULL); + + WSAIoctl(dummy_socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &connectex_guid, + sizeof(connectex_guid), &data->ConnectEx, sizeof(data->ConnectEx), &bytes, NULL, + NULL); + + WSAIoctl(dummy_socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &getacceptexsockaddrs_guid, + sizeof(getacceptexsockaddrs_guid), &data->GetAcceptExSockaddrs, + sizeof(data->GetAcceptExSockaddrs), &bytes, NULL, NULL); + + closesocket(dummy_socket); + } + + data->operation_count = 0; + ctx->backend_data = data; + return PHP_POLL_OK; +} + +static void iocp_backend_cleanup(php_poll_ctx *ctx) +{ + iocp_backend_data_t *data = (iocp_backend_data_t *) ctx->backend_data; + if (data) { + if (data->iocp_handle != NULL) { + CloseHandle(data->iocp_handle); + } + free(data->operations); + free(data); + ctx->backend_data = NULL; + } +} + +static int iocp_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + SOCKET sock = (SOCKET) fd; + + if (backend_data->operation_count >= backend_data->max_operations) { + return PHP_POLL_NOMEM; + } + + /* Associate socket with completion port */ + HANDLE result + = CreateIoCompletionPort((HANDLE) sock, backend_data->iocp_handle, (ULONG_PTR) sock, 0); + if (result == NULL) { + return PHP_POLL_ERROR; + } + + /* Set up operation structure */ + iocp_operation_t *op = &backend_data->operations[backend_data->operation_count++]; + memset(op, 0, sizeof(iocp_operation_t)); + op->fd = fd; + op->events = events; + op->user_data = data; + + /* For read events, post a recv operation */ + if (events & PHP_POLL_READ) { + WSABUF wsabuf = { 1, op->buffer }; + DWORD flags = 0; + DWORD bytes_received; + + int result = WSARecv(sock, &wsabuf, 1, &bytes_received, &flags, &op->overlapped, NULL); + + if (result == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) { + backend_data->operation_count--; + return PHP_POLL_ERROR; + } + } + + return PHP_POLL_OK; +} + +static int iocp_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + /* For IOCP, we need to cancel existing operations and re-add */ + iocp_backend_remove(ctx, fd); + return iocp_backend_add(ctx, fd, events, data); +} + +static int iocp_backend_remove(php_poll_ctx *ctx, int fd) +{ + iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + SOCKET sock = (SOCKET) fd; + + /* Cancel all I/O operations on this socket */ + CancelIo((HANDLE) sock); + + /* Remove from our operation list */ + for (int i = 0; i < backend_data->operation_count; i++) { + if (backend_data->operations[i].fd == fd) { + /* Shift remaining operations */ + for (int j = i; j < backend_data->operation_count - 1; j++) { + backend_data->operations[j] = backend_data->operations[j + 1]; + } + backend_data->operation_count--; + break; + } + } + + return PHP_POLL_OK; +} + +static int iocp_backend_wait( + php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) +{ + iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + + DWORD bytes_transferred; + ULONG_PTR completion_key; + LPOVERLAPPED overlapped; + + BOOL result = GetQueuedCompletionStatus(backend_data->iocp_handle, &bytes_transferred, + &completion_key, &overlapped, (timeout < 0) ? INFINITE : timeout); + + if (!result && overlapped == NULL) { + /* Timeout or error */ + return (GetLastError() == WAIT_TIMEOUT) ? 0 : PHP_POLL_ERROR; + } + + if (overlapped != NULL) { + /* Find the operation that completed */ + iocp_operation_t *op = CONTAINING_RECORD(overlapped, iocp_operation_t, overlapped); + + events[0].fd = op->fd; + events[0].events = op->events; + events[0].data = op->user_data; + + if (result) { + /* Successful completion */ + if (op->events & PHP_POLL_READ) { + events[0].revents = PHP_POLL_READ; + } else if (op->events & PHP_POLL_WRITE) { + events[0].revents = PHP_POLL_WRITE; + } + } else { + /* Error completion */ + events[0].revents = PHP_POLL_ERROR; + } + + return 1; + } + + return 0; +} + +static bool iocp_backend_is_available(void) +{ + /* IOCP is available on Windows NT and later */ + OSVERSIONINFO osvi = { 0 }; + osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); + + if (GetVersionEx(&osvi)) { + return (osvi.dwPlatformId == VER_PLATFORM_WIN32_NT); + } + + return false; +} + +const php_poll_backend_ops php_poll_backend_iocp_ops = { + .name = "iocp", + .init = iocp_backend_init, + .cleanup = iocp_backend_cleanup, + .add = iocp_backend_add, + .modify = iocp_backend_modify, + .remove = iocp_backend_remove, + .wait = iocp_backend_wait, + .is_available = iocp_backend_is_available, + .supports_et + = true /* IOCP provides completion-based model which is inherently edge-triggered */ +}; + +#endif /* _WIN32 */ \ No newline at end of file diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c new file mode 100644 index 0000000000000..41efe4038391d --- /dev/null +++ b/main/poll/poll_backend_kqueue.c @@ -0,0 +1,197 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll.h" + +#ifdef HAVE_KQUEUE + +typedef struct { + int kqueue_fd; + struct kevent *events; + struct kevent *change_list; + int change_count; + int change_capacity; +} kqueue_backend_data_t; + +static int kqueue_backend_init(php_poll_ctx *ctx, int max_events) +{ + kqueue_backend_data_t *data = calloc(1, sizeof(kqueue_backend_data_t)); + if (!data) { + return PHP_POLL_NOMEM; + } + + data->kqueue_fd = kqueue(); + if (data->kqueue_fd == -1) { + free(data); + return PHP_POLL_ERROR; + } + + data->events = calloc(max_events, sizeof(struct kevent)); + data->change_list = calloc(max_events * 2, sizeof(struct kevent)); /* Read + Write */ + data->change_capacity = max_events * 2; + data->change_count = 0; + + if (!data->events || !data->change_list) { + close(data->kqueue_fd); + free(data->events); + free(data->change_list); + free(data); + return PHP_POLL_NOMEM; + } + + ctx->backend_data = data; + return PHP_POLL_OK; +} + +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); + } + free(data->events); + free(data->change_list); + free(data); + ctx->backend_data = NULL; + } +} + +static int kqueue_add_change( + kqueue_backend_data_t *data, int fd, int16_t filter, uint16_t flags, void *udata) +{ + if (data->change_count >= data->change_capacity) { + return PHP_POLL_NOMEM; + } + + struct kevent *kev = &data->change_list[data->change_count++]; + EV_SET(kev, fd, filter, flags, 0, 0, udata); + return PHP_POLL_OK; +} + +static int 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; + + uint16_t flags = EV_ADD | EV_ENABLE; + if (events & PHP_POLL_ONESHOT) { + flags |= EV_ONESHOT; + } + if (events & PHP_POLL_ET) { + flags |= EV_CLEAR; /* kqueue edge-triggering */ + } + + int result = PHP_POLL_OK; + + if (events & PHP_POLL_READ) { + result = kqueue_add_change(backend_data, fd, EVFILT_READ, flags, data); + if (result != PHP_POLL_OK) { + return result; + } + } + + if (events & PHP_POLL_WRITE) { + result = kqueue_add_change(backend_data, fd, EVFILT_WRITE, flags, data); + if (result != PHP_POLL_OK) { + return result; + } + } + + return PHP_POLL_OK; +} + +static int kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + /* For kqueue, we delete and re-add */ + kqueue_backend_remove(ctx, fd); + return kqueue_backend_add(ctx, fd, events, data); +} + +static int kqueue_backend_remove(php_poll_ctx *ctx, int fd) +{ + kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + + /* Add delete operations for both read and write filters */ + kqueue_add_change(backend_data, fd, EVFILT_READ, EV_DELETE, NULL); + kqueue_add_change(backend_data, fd, EVFILT_WRITE, EV_DELETE, NULL); + + return PHP_POLL_OK; +} + +static int kqueue_backend_wait( + php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) +{ + kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + + struct timespec ts = { 0 }, *tsp = NULL; + if (timeout >= 0) { + ts.tv_sec = timeout / 1000; + ts.tv_nsec = (timeout % 1000) * 1000000; + tsp = &ts; + } + + /* Apply pending changes and wait for events */ + int nfds = kevent(backend_data->kqueue_fd, backend_data->change_list, + backend_data->change_count, backend_data->events, max_events, tsp); + + backend_data->change_count = 0; /* Reset change list */ + + if (nfds > 0) { + for (int i = 0; i < nfds; i++) { + events[i].fd = (int) backend_data->events[i].ident; + events[i].events = 0; + events[i].data = backend_data->events[i].udata; + events[i].revents = 0; + + 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; + } + + 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; +} + +static bool kqueue_backend_is_available(void) +{ + int fd = kqueue(); + if (fd >= 0) { + close(fd); + return true; + } + return false; +} + +const php_poll_backend_ops php_poll_backend_kqueue_ops = { + .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, + .supports_et = true /* kqueue supports EV_CLEAR for edge triggering */ +}; + +#endif /* HAVE_KQUEUE */ diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c new file mode 100644 index 0000000000000..46eb7e4d0a227 --- /dev/null +++ b/main/poll/poll_backend_poll.c @@ -0,0 +1,196 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll.h" + +#ifdef HAVE_POLL + +#include +#include +#include + +typedef struct { + struct pollfd *fds; + int allocated; + int used; +} 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; + } + return events; +} + +static int poll_backend_init(php_poll_ctx *ctx, int max_events) +{ + poll_backend_data_t *data = calloc(1, sizeof(poll_backend_data_t)); + if (!data) { + return PHP_POLL_NOMEM; + } + + data->fds = calloc(max_events, sizeof(struct pollfd)); + if (!data->fds) { + free(data); + return PHP_POLL_NOMEM; + } + + data->allocated = max_events; + data->used = 0; + + /* Initialize all fds to -1 */ + for (int i = 0; i < max_events; i++) { + data->fds[i].fd = -1; + } + + ctx->backend_data = data; + return PHP_POLL_OK; +} + +static void poll_backend_cleanup(php_poll_ctx *ctx) +{ + poll_backend_data_t *data = (poll_backend_data_t *) ctx->backend_data; + if (data) { + free(data->fds); + free(data); + ctx->backend_data = NULL; + } +} + +static int poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; + + /* Find empty slot */ + for (int i = 0; i < backend_data->allocated; i++) { + if (backend_data->fds[i].fd == -1) { + backend_data->fds[i].fd = fd; + backend_data->fds[i].events = poll_events_to_native(events); + backend_data->fds[i].revents = 0; + backend_data->used++; + return PHP_POLL_OK; + } + } + + return PHP_POLL_NOMEM; +} + +static int poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; + + for (int i = 0; i < backend_data->allocated; i++) { + if (backend_data->fds[i].fd == fd) { + backend_data->fds[i].events = poll_events_to_native(events); + return PHP_POLL_OK; + } + } + + return PHP_POLL_NOTFOUND; +} + +static int poll_backend_remove(php_poll_ctx *ctx, int fd) +{ + poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; + + for (int i = 0; i < backend_data->allocated; i++) { + if (backend_data->fds[i].fd == fd) { + backend_data->fds[i].fd = -1; + backend_data->fds[i].events = 0; + backend_data->fds[i].revents = 0; + backend_data->used--; + return PHP_POLL_OK; + } + } + + return PHP_POLL_NOTFOUND; +} + +static int poll_backend_wait( + php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) +{ + poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; + + int nfds = poll(backend_data->fds, backend_data->allocated, timeout); + + if (nfds > 0) { + int event_count = 0; + for (int i = 0; i < backend_data->allocated && event_count < max_events; i++) { + if (backend_data->fds[i].fd != -1 && backend_data->fds[i].revents != 0) { + events[event_count].fd = backend_data->fds[i].fd; + events[event_count].events = backend_data->fds[i].events; + events[event_count].revents = poll_events_from_native(backend_data->fds[i].revents); + events[event_count].data = NULL; /* poll doesn't support user data directly */ + + /* Find the user data from our FD tracking */ + php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, events[event_count].fd); + if (entry) { + events[event_count].data = entry->data; + } + + backend_data->fds[i].revents = 0; /* Clear for next poll */ + event_count++; + } + } + nfds = event_count; + } + + return nfds; +} + +static bool poll_backend_is_available(void) +{ + return true; /* poll() is always available */ +} + +const php_poll_backend_ops php_poll_backend_poll_ops = { .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, + .supports_et = false }; + +#endif /* HAVE_POLL */ diff --git a/main/poll/poll_backend_port.c b/main/poll/poll_backend_port.c new file mode 100644 index 0000000000000..2195fa3342c5b --- /dev/null +++ b/main/poll/poll_backend_port.c @@ -0,0 +1,303 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll.h" + +#ifdef HAVE_PORT_CREATE + +#include +#include +#include +#include +#include + +typedef struct { + int port_fd; + port_event_t *events; + int max_events; + int active_associations; +} eventport_backend_data_t; + +/* Convert our event flags to event port flags */ +static int eventport_events_to_native(uint32_t events) +{ + int native = 0; + if (events & PHP_POLL_READ) { + native |= POLLIN; + } + if (events & PHP_POLL_WRITE) { + native |= POLLOUT; + } + if (events & PHP_POLL_ERROR) { + native |= POLLERR; + } + if (events & PHP_POLL_HUP) { + native |= POLLHUP; + } + if (events & PHP_POLL_RDHUP) { + native |= POLLHUP; /* Map RDHUP to HUP */ + } + /* Event ports provide edge-triggered semantics by default */ + 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 int eventport_backend_init(php_poll_ctx_t *ctx, int max_events) +{ + eventport_backend_data_t *data = calloc(1, sizeof(eventport_backend_data_t)); + if (!data) { + return PHP_POLL_NOMEM; + } + + /* Create event port */ + data->port_fd = port_create(); + if (data->port_fd == -1) { + free(data); + return PHP_POLL_ERROR; + } + + data->max_events = max_events; + data->active_associations = 0; + + /* Allocate event array for port_getn() */ + data->events = calloc(max_events, sizeof(port_event_t)); + if (!data->events) { + close(data->port_fd); + free(data); + return PHP_POLL_NOMEM; + } + + ctx->backend_data = data; + return PHP_POLL_OK; +} + +/* Cleanup event port backend */ +static void eventport_backend_cleanup(php_poll_ctx_t *ctx) +{ + eventport_backend_data_t *data = (eventport_backend_data_t *) ctx->backend_data; + if (data) { + if (data->port_fd >= 0) { + close(data->port_fd); + } + free(data->events); + free(data); + ctx->backend_data = NULL; + } +} + +/* Add file descriptor to event port */ +static int eventport_backend_add(php_poll_ctx_t *ctx, int fd, uint32_t events, void *user_data) +{ + eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; + + int native_events = eventport_events_to_native(events); + + /* Associate file descriptor with event port */ + if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, user_data) == -1) { + switch (errno) { + case EEXIST: + return PHP_POLL_EXISTS; + case ENOMEM: + return PHP_POLL_NOMEM; + case EBADF: + case EINVAL: + return PHP_POLL_INVALID; + default: + return PHP_POLL_ERROR; + } + } + + backend_data->active_associations++; + return PHP_POLL_OK; +} + +/* Modify file descriptor in event port */ +static int eventport_backend_modify(php_poll_ctx_t *ctx, int fd, uint32_t events, void *user_data) +{ + eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; + + /* For event ports, we need to dissociate and re-associate */ + /* Note: dissociate might fail if the fd was already fired and auto-dissociated */ + port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd); + + int native_events = eventport_events_to_native(events); + if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, user_data) == -1) { + switch (errno) { + case ENOMEM: + return PHP_POLL_NOMEM; + case EBADF: + case EINVAL: + return PHP_POLL_INVALID; + default: + return PHP_POLL_ERROR; + } + } + + return PHP_POLL_OK; +} + +/* Remove file descriptor from event port */ +static int eventport_backend_remove(php_poll_ctx_t *ctx, int fd) +{ + eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; + + if (port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd) == -1) { + switch (errno) { + case ENOENT: + return PHP_POLL_NOTFOUND; + case EBADF: + case EINVAL: + return PHP_POLL_INVALID; + default: + return PHP_POLL_ERROR; + } + } + + backend_data->active_associations--; + return PHP_POLL_OK; +} + +/* Wait for events using event port */ +static int eventport_backend_wait( + php_poll_ctx_t *ctx, php_poll_event_t *events, int max_events, int timeout) +{ + eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; + + if (backend_data->active_associations == 0) { + /* No active associations, but we still need to respect timeout */ + if (timeout > 0) { + struct timespec ts; + ts.tv_sec = timeout / 1000; + ts.tv_nsec = (timeout % 1000) * 1000000; + nanosleep(&ts, NULL); + } + return 0; + } + + /* Setup timeout structure */ + struct timespec ts = { 0 }, *tsp = NULL; + if (timeout >= 0) { + ts.tv_sec = timeout / 1000; + ts.tv_nsec = (timeout % 1000) * 1000000; + tsp = &ts; + } + + /* Retrieve events from port */ + uint_t nget = 1; /* We want to get multiple events if available */ + int result = port_getn(backend_data->port_fd, backend_data->events, + MIN(max_events, backend_data->max_events), &nget, tsp); + + if (result == -1) { + if (errno == ETIME) { + /* Timeout - this is normal */ + return 0; + } else if (errno == EINTR) { + /* Interrupted by signal */ + return 0; + } else { + /* Real error */ + return PHP_POLL_ERROR; + } + } + + int nfds = (int) nget; + + /* Process the 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) { + events[i].fd = (int) port_event->portev_object; + events[i].events = 0; /* Not used in results */ + events[i].revents = eventport_events_from_native(port_event->portev_events); + events[i].data = port_event->portev_user; + + /* Event ports automatically dissociate after firing, so we need to + re-associate if this is not a oneshot event and we want level-triggered behavior */ + php_poll_fd_entry_t *entry = php_poll_find_fd_entry(ctx, events[i].fd); + if (entry && !(entry->events & PHP_POLL_ONESHOT)) { + /* Re-associate for continued monitoring */ + int native_events = eventport_events_to_native(entry->events); + if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, events[i].fd, + native_events, entry->data) + == 0) { + /* Re-association successful */ + } else { + /* Re-association failed - might be due to fd being closed */ + backend_data->active_associations--; + } + } else { + /* Oneshot event or entry not found - reduce association count */ + backend_data->active_associations--; + } + } 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; +} + +/* Event port backend operations structure */ +const php_poll_backend_ops_t php_poll_backend_eventport_ops = { + .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, + .supports_et = true /* Event ports provide edge-triggered semantics by default */ +}; + +#endif /* HAVE_PORT_CREATE */ diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c new file mode 100644 index 0000000000000..950618932a8db --- /dev/null +++ b/main/poll/poll_backend_select.c @@ -0,0 +1,245 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll.h" + +#ifdef _WIN32 + +#include +#include + +typedef struct { + fd_set read_fds, write_fds, error_fds; + fd_set master_read_fds, master_write_fds, master_error_fds; + SOCKET *socket_list; + void **data_list; + int socket_count; + int max_sockets; +} select_backend_data_t; + +static int select_backend_init(php_poll_ctx *ctx, int max_events) +{ + select_backend_data_t *data = calloc(1, sizeof(select_backend_data_t)); + if (!data) { + return PHP_POLL_NOMEM; + } + + data->max_sockets = max_events; + data->socket_list = calloc(max_events, sizeof(SOCKET)); + data->data_list = calloc(max_events, sizeof(void *)); + + if (!data->socket_list || !data->data_list) { + free(data->socket_list); + free(data->data_list); + free(data); + return PHP_POLL_NOMEM; + } + + FD_ZERO(&data->master_read_fds); + FD_ZERO(&data->master_write_fds); + FD_ZERO(&data->master_error_fds); + data->socket_count = 0; + + ctx->backend_data = data; + return PHP_POLL_OK; +} + +static void select_backend_cleanup(php_poll_ctx *ctx) +{ + select_backend_data_t *data = (select_backend_data_t *) ctx->backend_data; + if (data) { + free(data->socket_list); + free(data->data_list); + free(data); + ctx->backend_data = NULL; + } +} + +static int select_find_socket_index(select_backend_data_t *data, SOCKET sock) +{ + for (int i = 0; i < data->socket_count; i++) { + if (data->socket_list[i] == sock) { + return i; + } + } + return -1; +} + +static int select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; + SOCKET sock = (SOCKET) fd; + + if (backend_data->socket_count >= backend_data->max_sockets) { + return PHP_POLL_NOMEM; + } + + /* Check if socket already exists */ + if (select_find_socket_index(backend_data, sock) >= 0) { + return PHP_POLL_EXISTS; + } + + /* Add socket to our tracking */ + int index = backend_data->socket_count++; + backend_data->socket_list[index] = sock; + backend_data->data_list[index] = data; + + /* Add to appropriate fd_sets */ + if (events & PHP_POLL_READ) { + FD_SET(sock, &backend_data->master_read_fds); + } + if (events & PHP_POLL_WRITE) { + FD_SET(sock, &backend_data->master_write_fds); + } + /* Always monitor for errors */ + FD_SET(sock, &backend_data->master_error_fds); + + return PHP_POLL_OK; +} + +static int select_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; + SOCKET sock = (SOCKET) fd; + + int index = select_find_socket_index(backend_data, sock); + if (index < 0) { + return PHP_POLL_NOTFOUND; + } + + /* Update user data */ + backend_data->data_list[index] = data; + + /* Remove from all sets first */ + FD_CLR(sock, &backend_data->master_read_fds); + FD_CLR(sock, &backend_data->master_write_fds); + FD_CLR(sock, &backend_data->master_error_fds); + + /* Add back based on new events */ + if (events & PHP_POLL_READ) { + FD_SET(sock, &backend_data->master_read_fds); + } + if (events & PHP_POLL_WRITE) { + FD_SET(sock, &backend_data->master_write_fds); + } + FD_SET(sock, &backend_data->master_error_fds); + + return PHP_POLL_OK; +} + +static int select_backend_remove(php_poll_ctx *ctx, int fd) +{ + select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; + SOCKET sock = (SOCKET) fd; + + int index = select_find_socket_index(backend_data, sock); + if (index < 0) { + return PHP_POLL_NOTFOUND; + } + + /* Remove from fd_sets */ + FD_CLR(sock, &backend_data->master_read_fds); + FD_CLR(sock, &backend_data->master_write_fds); + FD_CLR(sock, &backend_data->master_error_fds); + + /* Remove from socket list by shifting elements */ + for (int i = index; i < backend_data->socket_count - 1; i++) { + backend_data->socket_list[i] = backend_data->socket_list[i + 1]; + backend_data->data_list[i] = backend_data->data_list[i + 1]; + } + backend_data->socket_count--; + + return PHP_POLL_OK; +} + +static int select_backend_wait( + php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) +{ + select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; + + if (backend_data->socket_count == 0) { + /* No sockets to wait for */ + if (timeout > 0) { + Sleep(timeout); + } + return 0; + } + + /* Copy master sets */ + memcpy(&backend_data->read_fds, &backend_data->master_read_fds, sizeof(fd_set)); + memcpy(&backend_data->write_fds, &backend_data->master_write_fds, sizeof(fd_set)); + memcpy(&backend_data->error_fds, &backend_data->master_error_fds, sizeof(fd_set)); + + /* Setup timeout */ + struct timeval tv = { 0 }, *ptv = NULL; + if (timeout >= 0) { + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) * 1000; + ptv = &tv; + } + + /* Call select() */ + int result = select( + 0, &backend_data->read_fds, &backend_data->write_fds, &backend_data->error_fds, ptv); + + if (result <= 0) { + return (result == 0) ? 0 : PHP_POLL_ERROR; + } + + /* Process results */ + int event_count = 0; + for (int i = 0; i < backend_data->socket_count && event_count < max_events; i++) { + SOCKET sock = backend_data->socket_list[i]; + uint32_t revents = 0; + + if (FD_ISSET(sock, &backend_data->read_fds)) { + revents |= PHP_POLL_READ; + } + if (FD_ISSET(sock, &backend_data->write_fds)) { + revents |= PHP_POLL_WRITE; + } + if (FD_ISSET(sock, &backend_data->error_fds)) { + revents |= PHP_POLL_ERROR; + } + + if (revents != 0) { + events[event_count].fd = (int) sock; + events[event_count].events = 0; /* Not used in results */ + events[event_count].revents = revents; + events[event_count].data = backend_data->data_list[i]; + event_count++; + } + } + + return event_count; +} + +static bool select_backend_is_available(void) +{ + return true; /* select() is always available */ +} + +const php_poll_backend_ops php_poll_backend_select_ops = { + .name = "select", + .init = select_backend_init, + .cleanup = select_backend_cleanup, + .add = select_backend_add, + .modify = select_backend_modify, + .remove = select_backend_remove, + .wait = select_backend_wait, + .is_available = select_backend_is_available, + .supports_et = false /* select() doesn't support edge triggering */ +}; + +#endif _WIN32 diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c new file mode 100644 index 0000000000000..ced36b82eee5c --- /dev/null +++ b/main/poll/poll_core.c @@ -0,0 +1,353 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll.h" + +/* Backend registry */ +static const php_poll_backend_ops *registered_backends[16]; +static int num_registered_backends = 0; + +/* Forward declarations for backend ops */ +extern const php_poll_backend_ops php_poll_backend_poll_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_PORT_H +extern const php_poll_backend_ops php_poll_backend_eventport_ops; +#endif +#ifdef _WIN32 +extern const php_poll_backend_ops php_poll_backend_iocp_ops; +extern const php_poll_backend_ops php_poll_backend_select_ops; +#endif + +/* Register all available backends */ +void php_poll_register_backends(void) +{ + num_registered_backends = 0; + +#ifdef _WIN32 + /* IOCP is preferred on Windows for high performance */ + if (php_poll_backend_iocp_ops.is_available()) { + registered_backends[num_registered_backends++] = &php_poll_backend_iocp_ops; + } +#endif + +#ifdef HAVE_PORT_H + /* 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_SYS_EPOLL_H + if (php_poll_backend_epoll_ops.is_available()) { + registered_backends[num_registered_backends++] = &php_poll_backend_epoll_ops; + } +#endif + +#ifdef HAVE_SYS_EVENT_H + if (php_poll_backend_kqueue_ops.is_available()) { + registered_backends[num_registered_backends++] = &php_poll_backend_kqueue_ops; + } +#endif + +#ifdef _WIN32 + /* Windows select() as fallback before poll() */ + if (php_poll_backend_select_ops.is_available()) { + registered_backends[num_registered_backends++] = &php_poll_backend_select_ops; + } +#endif + +#ifndef _WIN32 + /* Poll is available on Unix-like systems */ + registered_backends[num_registered_backends++] = &php_poll_backend_poll_ops; +#endif +} + +/* Get backend operations */ +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] + && ((backend == PHP_POLL_BACKEND_EPOLL + && strcmp(registered_backends[i]->name, "epoll") == 0) + || (backend == PHP_POLL_BACKEND_KQUEUE + && strcmp(registered_backends[i]->name, "kqueue") == 0) + || (backend == PHP_POLL_BACKEND_EVENTPORT + && strcmp(registered_backends[i]->name, "eventport") == 0) + || (backend == PHP_POLL_BACKEND_IOCP + && strcmp(registered_backends[i]->name, "iocp") == 0) + || (backend == PHP_POLL_BACKEND_SELECT + && strcmp(registered_backends[i]->name, "select") == 0) + || (backend == PHP_POLL_BACKEND_POLL + && strcmp(registered_backends[i]->name, "poll") == 0))) { + return registered_backends[i]; + } + } + + return NULL; +} + +/* Find FD entry */ +static php_poll_fd_entry *php_poll_find_fd_entry(php_poll_ctx *ctx, int fd) +{ + for (int i = 0; i < ctx->fd_entries_size; i++) { + if (ctx->fd_entries[i].active && ctx->fd_entries[i].fd == fd) { + return &ctx->fd_entries[i]; + } + } + return NULL; +} + +/* Get or create FD entry */ +static php_poll_fd_entry *php_poll_get_fd_entry(php_poll_ctx *ctx, int fd) +{ + php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); + if (entry) + return entry; + + /* Find empty slot */ + for (int i = 0; i < ctx->fd_entries_size; i++) { + if (!ctx->fd_entries[i].active) { + ctx->fd_entries[i].fd = fd; + ctx->fd_entries[i].active = true; + ctx->fd_entries[i].last_revents = 0; + ctx->fd_entries[i].et_armed = true; + return &ctx->fd_entries[i]; + } + } + + return NULL; +} + +/* Edge-trigger simulation */ +static int php_poll_simulate_et(php_poll_ctx *ctx, php_poll_event_t *events, int nfds) +{ + if (!ctx->simulate_et) { + return nfds; /* No simulation needed */ + } + + int filtered_count = 0; + + for (int i = 0; i < nfds; i++) { + php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, events[i].fd); + if (!entry) + continue; + + uint32_t new_events = events[i].revents; + uint32_t edge_events = 0; + + /* Detect edges for each event type */ + if ((new_events & PHP_POLL_READ) && !(entry->last_revents & PHP_POLL_READ)) { + edge_events |= PHP_POLL_READ; + } + if ((new_events & PHP_POLL_WRITE) && !(entry->last_revents & PHP_POLL_WRITE)) { + edge_events |= PHP_POLL_WRITE; + } + + /* Always report error and hangup events */ + edge_events |= (new_events & (PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP)); + + entry->last_revents = new_events; + + /* Only include this event if we detected an edge or it's an error */ + if (edge_events != 0) { + if (filtered_count != i) { + events[filtered_count] = events[i]; + } + events[filtered_count].revents = edge_events; + filtered_count++; + } + } + + return filtered_count; +} + +/* Create new poll context */ +php_poll_ctx *php_poll_create(int max_events, php_poll_backend_type preferred_backend) +{ + if (max_events <= 0) + return NULL; + + php_poll_ctx *ctx = calloc(1, sizeof(php_poll_ctx)); + if (!ctx) + return NULL; + + /* Get backend operations */ + ctx->backend_ops = php_poll_get_backend_ops(preferred_backend); + if (!ctx->backend_ops) { + free(ctx); + return NULL; + } + + ctx->max_events = max_events; + ctx->backend_type = preferred_backend; + + /* Allocate FD entries for edge-trigger simulation */ + ctx->fd_entries = calloc(max_events, sizeof(php_poll_fd_entry)); + ctx->fd_entries_size = max_events; + if (!ctx->fd_entries) { + free(ctx); + return NULL; + } + + /* Initialize backend */ + if (ctx->backend_ops->init(ctx, max_events) != PHP_POLL_OK) { + free(ctx->fd_entries); + free(ctx); + return NULL; + } + + ctx->initialized = true; + ctx->simulate_et = !ctx->backend_ops->supports_et; + + return ctx; +} + +/* Destroy poll context */ +void php_poll_destroy(php_poll_ctx *ctx) +{ + if (!ctx) + return; + + if (ctx->backend_ops && ctx->backend_ops->cleanup) { + ctx->backend_ops->cleanup(ctx); + } + + free(ctx->fd_entries); + free(ctx); +} + +/* Add file descriptor */ +int php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + if (!ctx || !ctx->initialized || fd < 0) { + return PHP_POLL_INVALID; + } + + if (ctx->num_fds >= ctx->max_events) { + return PHP_POLL_NOMEM; + } + + /* Get FD entry for tracking */ + php_poll_fd_entry *entry = php_poll_get_fd_entry(ctx, fd); + if (!entry) { + return PHP_POLL_NOMEM; + } + + entry->events = events; + entry->data = data; + + /* If simulating edge triggering, convert ET events to level-triggered */ + uint32_t backend_events = events; + if (ctx->simulate_et && (events & PHP_POLL_ET)) { + backend_events &= ~PHP_POLL_ET; /* Remove ET flag for backend */ + } + + int result = ctx->backend_ops->add(ctx, fd, backend_events, data); + if (result == PHP_POLL_OK) { + ctx->num_fds++; + } else { + entry->active = false; /* Rollback */ + } + + return result; +} + +/* Modify file descriptor */ +int php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + if (!ctx || !ctx->initialized || fd < 0) { + return PHP_POLL_INVALID; + } + + php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); + if (!entry) { + return PHP_POLL_NOTFOUND; + } + + entry->events = events; + entry->data = data; + entry->et_armed = true; /* Re-arm edge triggering */ + + uint32_t backend_events = events; + if (ctx->simulate_et && (events & PHP_POLL_ET)) { + backend_events &= ~PHP_POLL_ET; + } + + return ctx->backend_ops->modify(ctx, fd, backend_events, data); +} + +/* Remove file descriptor */ +int php_poll_remove(php_poll_ctx *ctx, int fd) +{ + if (!ctx || !ctx->initialized || fd < 0) { + return PHP_POLL_INVALID; + } + + php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); + if (!entry) { + return PHP_POLL_NOTFOUND; + } + + int result = ctx->backend_ops->remove(ctx, fd); + if (result == PHP_POLL_OK) { + entry->active = false; + ctx->num_fds--; + } + + return result; +} + +/* Wait for events */ +int php_poll_wait(php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) +{ + if (!ctx || !ctx->initialized || !events || max_events <= 0) { + return PHP_POLL_INVALID; + } + + int nfds = ctx->backend_ops->wait(ctx, events, max_events, timeout); + + if (nfds > 0 && ctx->simulate_et) { + nfds = php_poll_simulate_et(ctx, events, nfds); + } + + return nfds; +} + +/* Get backend name */ +const char *php_poll_backend_name(php_poll_ctx *ctx) +{ + return ctx && ctx->backend_ops ? ctx->backend_ops->name : "unknown"; +} + +/* Get backend type */ +php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx) +{ + return ctx ? ctx->backend_type : PHP_POLL_BACKEND_AUTO; +} + +/* Check edge-triggering support */ +bool php_poll_supports_et(php_poll_ctx *ctx) +{ + return ctx && (ctx->backend_ops->supports_et || ctx->simulate_et); +} From a0d935df190c037ef268ad383425963861c5ef59 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 16 Aug 2025 11:33:00 +0200 Subject: [PATCH 02/52] Add poll mechanism build checks and fix build --- build/php.m4 | 44 +++++++++++++++++ configure.ac | 11 +++++ main/php_poll.h | 16 ++++--- main/poll/poll_backend_epoll.c | 18 +++---- main/poll/poll_backend_iocp.c | 14 +++--- main/poll/poll_backend_kqueue.c | 20 ++++---- main/poll/poll_backend_poll.c | 18 +++---- main/poll/poll_backend_port.c | 16 +++---- main/poll/poll_backend_select.c | 16 +++---- main/poll/poll_core.c | 24 +++++----- sapi/fpm/config.m4 | 84 --------------------------------- 11 files changed, 127 insertions(+), 154 deletions(-) diff --git a/build/php.m4 b/build/php.m4 index db4265c66fc67..1919880b11fb1 100644 --- a/build/php.m4 +++ b/build/php.m4 @@ -1371,6 +1371,50 @@ int main(void) { ]) ]) +AC_DEFUN([PHP_POLL_MECHANISMS], +[ + AC_MSG_CHECKING([for polling mechanisms]) + poll_mechanisms="" + + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + #include + ], [ + int fd = epoll_create(1); + return fd; + ])], [ + AC_DEFINE([HAVE_EPOLL], [1], [Define if epoll is available]) + poll_mechanisms="$poll_mechanisms epoll" + ]) + + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + #include + #include + ], [ + int kq = kqueue(); + return kq; + ])], [ + AC_DEFINE([HAVE_KQUEUE], [1], [Define if kqueue is available]) + poll_mechanisms="$poll_mechanisms kqueue" + ]) + + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + #include + ], [ + int port = port_create(); + return port; + ])], [ + AC_DEFINE([HAVE_PORT_H], [1], [Define if event ports are available]) + poll_mechanisms="$poll_mechanisms eventport" + ]) + + dnl These are always available on POSIX + AC_DEFINE([HAVE_POLL_H], [1], [Define if poll is available]) + AC_DEFINE([HAVE_SELECT], [1], [Define if select is available]) + poll_mechanisms="$poll_mechanisms poll select" + + AC_MSG_RESULT([$poll_mechanisms]) +]) + dnl ---------------------------------------------------------------------------- dnl Library/function existence and build sanity checks. dnl ---------------------------------------------------------------------------- diff --git a/configure.ac b/configure.ac index 2bd6ae26ce625..1958d7d212f45 100644 --- a/configure.ac +++ b/configure.ac @@ -426,6 +426,7 @@ AC_CHECK_HEADERS(m4_normalize([ ]) PHP_FOPENCOOKIE +PHP_POLL_MECHANISMS PHP_BROKEN_GETCWD AS_VAR_IF([GCC], [yes], [PHP_BROKEN_GCC_STRLEN_OPT]) @@ -1677,6 +1678,16 @@ PHP_ADD_SOURCES_X([main], [PHP_FASTCGI_OBJS], [no]) +PHP_ADD_SOURCES([main/poll], m4_normalize([ + poll_backend_epoll.c + poll_backend_kqueue.c + poll_backend_poll.c + poll_backend_port.c + poll_backend_select.c + poll_core.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 index eb656dcd6ab9d..5df890a145461 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -38,13 +38,13 @@ typedef enum { } php_poll_backend_type; /* Error codes */ -#define PHP_POLL_OK 0 -#define PHP_POLL_ERROR -1 -#define PHP_POLL_NOMEM -2 -#define PHP_POLL_INVALID -3 -#define PHP_POLL_EXISTS -4 -#define PHP_POLL_NOTFOUND -5 -#define PHP_POLL_TIMEOUT -6 +#define PHP_POLL_ERR_NONE 0 +#define PHP_POLL_ERR_FAIL -1 +#define PHP_POLL_ERR_NOMEM -2 +#define PHP_POLL_ERR_INVALID -3 +#define PHP_POLL_ERR_EXISTS -4 +#define PHP_POLL_ERR_NOTFOUND -5 +#define PHP_POLL_ERR_TIMEOUT -6 /* Forward declarations */ typedef struct php_poll_ctx php_poll_ctx; @@ -129,6 +129,8 @@ const char* php_poll_backend_name(php_poll_ctx *ctx); php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx); bool php_poll_supports_et(php_poll_ctx *ctx); +php_poll_fd_entry *php_poll_find_fd_entry(php_poll_ctx *ctx, int fd); + /* Backend registration */ void php_poll_register_backends(void); const php_poll_backend_ops* php_poll_get_backend_ops(php_poll_backend_type backend); diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c index 461ff4c39c8bc..055a46e62558f 100644 --- a/main/poll/poll_backend_epoll.c +++ b/main/poll/poll_backend_epoll.c @@ -75,7 +75,7 @@ static int epoll_backend_init(php_poll_ctx *ctx, int max_events) { epoll_backend_data_t *data = calloc(1, sizeof(epoll_backend_data_t)); if (!data) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } data->epoll_fd = epoll_create1(EPOLL_CLOEXEC); @@ -88,11 +88,11 @@ static int epoll_backend_init(php_poll_ctx *ctx, int max_events) if (!data->events) { close(data->epoll_fd); free(data); - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } ctx->backend_data = data; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static void epoll_backend_cleanup(php_poll_ctx *ctx) @@ -117,10 +117,10 @@ static int epoll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *d ev.data.ptr = data; if (epoll_ctl(backend_data->epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) { - return (errno == EEXIST) ? PHP_POLL_EXISTS : PHP_POLL_ERROR; + return (errno == EEXIST) ? PHP_POLL_ERR_EXISTS : PHP_POLL_ERROR; } - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int epoll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) @@ -132,10 +132,10 @@ static int epoll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void ev.data.ptr = data; if (epoll_ctl(backend_data->epoll_fd, EPOLL_CTL_MOD, fd, &ev) == -1) { - return (errno == ENOENT) ? PHP_POLL_NOTFOUND : PHP_POLL_ERROR; + return (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERROR; } - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int epoll_backend_remove(php_poll_ctx *ctx, int fd) @@ -143,10 +143,10 @@ static int 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) { - return (errno == ENOENT) ? PHP_POLL_NOTFOUND : PHP_POLL_ERROR; + return (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERROR; } - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int epoll_backend_wait( diff --git a/main/poll/poll_backend_iocp.c b/main/poll/poll_backend_iocp.c index a62cad87aea0c..775afec48306c 100644 --- a/main/poll/poll_backend_iocp.c +++ b/main/poll/poll_backend_iocp.c @@ -42,7 +42,7 @@ static int iocp_backend_init(php_poll_ctx *ctx, int max_events) { iocp_backend_data_t *data = calloc(1, sizeof(iocp_backend_data_t)); if (!data) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } /* Create I/O Completion Port */ @@ -58,7 +58,7 @@ static int iocp_backend_init(php_poll_ctx *ctx, int max_events) if (!data->operations) { CloseHandle(data->iocp_handle); free(data); - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } /* Load Winsock extension functions */ @@ -85,7 +85,7 @@ static int iocp_backend_init(php_poll_ctx *ctx, int max_events) data->operation_count = 0; ctx->backend_data = data; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static void iocp_backend_cleanup(php_poll_ctx *ctx) @@ -107,7 +107,7 @@ static int iocp_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *da SOCKET sock = (SOCKET) fd; if (backend_data->operation_count >= backend_data->max_operations) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } /* Associate socket with completion port */ @@ -138,7 +138,7 @@ static int iocp_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *da } } - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int iocp_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) @@ -168,7 +168,7 @@ static int iocp_backend_remove(php_poll_ctx *ctx, int fd) } } - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int iocp_backend_wait( @@ -240,4 +240,4 @@ const php_poll_backend_ops php_poll_backend_iocp_ops = { = true /* IOCP provides completion-based model which is inherently edge-triggered */ }; -#endif /* _WIN32 */ \ No newline at end of file +#endif /* _WIN32 */ diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 41efe4038391d..6249c1fb5937b 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -28,7 +28,7 @@ static int kqueue_backend_init(php_poll_ctx *ctx, int max_events) { kqueue_backend_data_t *data = calloc(1, sizeof(kqueue_backend_data_t)); if (!data) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } data->kqueue_fd = kqueue(); @@ -47,11 +47,11 @@ static int kqueue_backend_init(php_poll_ctx *ctx, int max_events) free(data->events); free(data->change_list); free(data); - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } ctx->backend_data = data; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static void kqueue_backend_cleanup(php_poll_ctx *ctx) @@ -72,12 +72,12 @@ static int kqueue_add_change( kqueue_backend_data_t *data, int fd, int16_t filter, uint16_t flags, void *udata) { if (data->change_count >= data->change_capacity) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } struct kevent *kev = &data->change_list[data->change_count++]; EV_SET(kev, fd, filter, flags, 0, 0, udata); - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) @@ -92,23 +92,23 @@ static int kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void * flags |= EV_CLEAR; /* kqueue edge-triggering */ } - int result = PHP_POLL_OK; + int result = PHP_POLL_ERR_NONE; if (events & PHP_POLL_READ) { result = kqueue_add_change(backend_data, fd, EVFILT_READ, flags, data); - if (result != PHP_POLL_OK) { + if (result != PHP_POLL_ERR_NONE) { return result; } } if (events & PHP_POLL_WRITE) { result = kqueue_add_change(backend_data, fd, EVFILT_WRITE, flags, data); - if (result != PHP_POLL_OK) { + if (result != PHP_POLL_ERR_NONE) { return result; } } - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) @@ -126,7 +126,7 @@ static int kqueue_backend_remove(php_poll_ctx *ctx, int fd) kqueue_add_change(backend_data, fd, EVFILT_READ, EV_DELETE, NULL); kqueue_add_change(backend_data, fd, EVFILT_WRITE, EV_DELETE, NULL); - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int kqueue_backend_wait( diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 46eb7e4d0a227..331165fa7e17e 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -66,13 +66,13 @@ static int poll_backend_init(php_poll_ctx *ctx, int max_events) { poll_backend_data_t *data = calloc(1, sizeof(poll_backend_data_t)); if (!data) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } data->fds = calloc(max_events, sizeof(struct pollfd)); if (!data->fds) { free(data); - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } data->allocated = max_events; @@ -84,7 +84,7 @@ static int poll_backend_init(php_poll_ctx *ctx, int max_events) } ctx->backend_data = data; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static void poll_backend_cleanup(php_poll_ctx *ctx) @@ -108,11 +108,11 @@ static int poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *da backend_data->fds[i].events = poll_events_to_native(events); backend_data->fds[i].revents = 0; backend_data->used++; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } } - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } static int poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) @@ -122,11 +122,11 @@ static int poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void for (int i = 0; i < backend_data->allocated; i++) { if (backend_data->fds[i].fd == fd) { backend_data->fds[i].events = poll_events_to_native(events); - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } } - return PHP_POLL_NOTFOUND; + return PHP_POLL_ERR_NOTFOUND; } static int poll_backend_remove(php_poll_ctx *ctx, int fd) @@ -139,11 +139,11 @@ static int poll_backend_remove(php_poll_ctx *ctx, int fd) backend_data->fds[i].events = 0; backend_data->fds[i].revents = 0; backend_data->used--; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } } - return PHP_POLL_NOTFOUND; + return PHP_POLL_ERR_NOTFOUND; } static int poll_backend_wait( diff --git a/main/poll/poll_backend_port.c b/main/poll/poll_backend_port.c index 2195fa3342c5b..c9097e33f3627 100644 --- a/main/poll/poll_backend_port.c +++ b/main/poll/poll_backend_port.c @@ -101,7 +101,7 @@ static int eventport_backend_init(php_poll_ctx_t *ctx, int max_events) } ctx->backend_data = data; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } /* Cleanup event port backend */ @@ -134,14 +134,14 @@ static int eventport_backend_add(php_poll_ctx_t *ctx, int fd, uint32_t events, v return PHP_POLL_NOMEM; case EBADF: case EINVAL: - return PHP_POLL_INVALID; + return PHP_POLL_ERR_INVALID; default: return PHP_POLL_ERROR; } } backend_data->active_associations++; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } /* Modify file descriptor in event port */ @@ -160,13 +160,13 @@ static int eventport_backend_modify(php_poll_ctx_t *ctx, int fd, uint32_t events return PHP_POLL_NOMEM; case EBADF: case EINVAL: - return PHP_POLL_INVALID; + return PHP_POLL_ERR_INVALID; default: return PHP_POLL_ERROR; } } - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } /* Remove file descriptor from event port */ @@ -177,17 +177,17 @@ static int eventport_backend_remove(php_poll_ctx_t *ctx, int fd) if (port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd) == -1) { switch (errno) { case ENOENT: - return PHP_POLL_NOTFOUND; + return PHP_POLL_ERR_NOTFOUND; case EBADF: case EINVAL: - return PHP_POLL_INVALID; + return PHP_POLL_ERR_INVALID; default: return PHP_POLL_ERROR; } } backend_data->active_associations--; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } /* Wait for events using event port */ diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c index 950618932a8db..2406c99aa31b3 100644 --- a/main/poll/poll_backend_select.c +++ b/main/poll/poll_backend_select.c @@ -32,7 +32,7 @@ static int select_backend_init(php_poll_ctx *ctx, int max_events) { select_backend_data_t *data = calloc(1, sizeof(select_backend_data_t)); if (!data) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } data->max_sockets = max_events; @@ -43,7 +43,7 @@ static int select_backend_init(php_poll_ctx *ctx, int max_events) free(data->socket_list); free(data->data_list); free(data); - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } FD_ZERO(&data->master_read_fds); @@ -52,7 +52,7 @@ static int select_backend_init(php_poll_ctx *ctx, int max_events) data->socket_count = 0; ctx->backend_data = data; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static void select_backend_cleanup(php_poll_ctx *ctx) @@ -82,7 +82,7 @@ static int select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void * SOCKET sock = (SOCKET) fd; if (backend_data->socket_count >= backend_data->max_sockets) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } /* Check if socket already exists */ @@ -105,7 +105,7 @@ static int select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void * /* Always monitor for errors */ FD_SET(sock, &backend_data->master_error_fds); - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int select_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) @@ -135,7 +135,7 @@ static int select_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, voi } FD_SET(sock, &backend_data->master_error_fds); - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int select_backend_remove(php_poll_ctx *ctx, int fd) @@ -160,7 +160,7 @@ static int select_backend_remove(php_poll_ctx *ctx, int fd) } backend_data->socket_count--; - return PHP_POLL_OK; + return PHP_POLL_ERR_NONE; } static int select_backend_wait( @@ -242,4 +242,4 @@ const php_poll_backend_ops php_poll_backend_select_ops = { .supports_et = false /* select() doesn't support edge triggering */ }; -#endif _WIN32 +#endif /* _WIN32 */ diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index ced36b82eee5c..de6cd53667186 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -108,7 +108,7 @@ const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backe } /* Find FD entry */ -static php_poll_fd_entry *php_poll_find_fd_entry(php_poll_ctx *ctx, int fd) +php_poll_fd_entry *php_poll_find_fd_entry(php_poll_ctx *ctx, int fd) { for (int i = 0; i < ctx->fd_entries_size; i++) { if (ctx->fd_entries[i].active && ctx->fd_entries[i].fd == fd) { @@ -211,7 +211,7 @@ php_poll_ctx *php_poll_create(int max_events, php_poll_backend_type preferred_ba } /* Initialize backend */ - if (ctx->backend_ops->init(ctx, max_events) != PHP_POLL_OK) { + if (ctx->backend_ops->init(ctx, max_events) != PHP_POLL_ERR_NONE) { free(ctx->fd_entries); free(ctx); return NULL; @@ -241,17 +241,17 @@ void php_poll_destroy(php_poll_ctx *ctx) int php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { if (!ctx || !ctx->initialized || fd < 0) { - return PHP_POLL_INVALID; + return PHP_POLL_ERR_INVALID; } if (ctx->num_fds >= ctx->max_events) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } /* Get FD entry for tracking */ php_poll_fd_entry *entry = php_poll_get_fd_entry(ctx, fd); if (!entry) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } entry->events = events; @@ -264,7 +264,7 @@ int php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) } int result = ctx->backend_ops->add(ctx, fd, backend_events, data); - if (result == PHP_POLL_OK) { + if (result == PHP_POLL_ERR_NONE) { ctx->num_fds++; } else { entry->active = false; /* Rollback */ @@ -277,12 +277,12 @@ int php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) int php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { if (!ctx || !ctx->initialized || fd < 0) { - return PHP_POLL_INVALID; + return PHP_POLL_ERR_INVALID; } php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); if (!entry) { - return PHP_POLL_NOTFOUND; + return PHP_POLL_ERR_NOTFOUND; } entry->events = events; @@ -301,16 +301,16 @@ int php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) int php_poll_remove(php_poll_ctx *ctx, int fd) { if (!ctx || !ctx->initialized || fd < 0) { - return PHP_POLL_INVALID; + return PHP_POLL_ERR_INVALID; } php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); if (!entry) { - return PHP_POLL_NOTFOUND; + return PHP_POLL_ERR_NOTFOUND; } int result = ctx->backend_ops->remove(ctx, fd); - if (result == PHP_POLL_OK) { + if (result == PHP_POLL_ERR_NONE) { entry->active = false; ctx->num_fds--; } @@ -322,7 +322,7 @@ int php_poll_remove(php_poll_ctx *ctx, int fd) int php_poll_wait(php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) { if (!ctx || !ctx->initialized || !events || max_events <= 0) { - return PHP_POLL_INVALID; + return PHP_POLL_ERR_INVALID; } int nfds = ctx->backend_ops->wait(ctx, events, max_events, timeout); diff --git a/sapi/fpm/config.m4 b/sapi/fpm/config.m4 index 4d4952eee86e7..89c53a0c4d284 100644 --- a/sapi/fpm/config.m4 +++ b/sapi/fpm/config.m4 @@ -262,100 +262,16 @@ AS_VAR_IF([php_cv_have_SO_LISTENQLEN], [yes], [Define to 1 if you have 'SO_LISTENQ*'.])]) ]) -AC_DEFUN([PHP_FPM_KQUEUE], -[AC_CACHE_CHECK([for kqueue], - [php_cv_have_kqueue], - [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([dnl - #include - #include - #include - ], [dnl - int kfd; - struct kevent k; - kfd = kqueue(); - /* 0 -> STDIN_FILENO */ - EV_SET(&k, 0, EVFILT_READ , EV_ADD | EV_CLEAR, 0, 0, NULL); - (void)kfd; - ])], - [php_cv_have_kqueue=yes], - [php_cv_have_kqueue=no])]) -AS_VAR_IF([php_cv_have_kqueue], [yes], - [AC_DEFINE([HAVE_KQUEUE], [1], - [Define to 1 if system has a working 'kqueue' function.])]) -]) - -AC_DEFUN([PHP_FPM_EPOLL], -[AC_CACHE_CHECK([for epoll], - [php_cv_have_epoll], - [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([#include ], [dnl - int epollfd; - struct epoll_event e; - - epollfd = epoll_create(1); - if (epollfd < 0) { - return 1; - } - - e.events = EPOLLIN | EPOLLET; - e.data.fd = 0; - - if (epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &e) == -1) { - return 1; - } - - e.events = 0; - if (epoll_wait(epollfd, &e, 1, 1) < 0) { - return 1; - } - ])], - [php_cv_have_epoll=yes], - [php_cv_have_epoll=no])]) -AS_VAR_IF([php_cv_have_epoll], [yes], - [AC_DEFINE([HAVE_EPOLL], [1], [Define to 1 if system has a working epoll.])]) -]) - -AC_DEFUN([PHP_FPM_SELECT], -[AC_CACHE_CHECK([for select], - [php_cv_have_select], - [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([dnl - /* According to POSIX.1-2001 */ - #include - - /* According to earlier standards */ - #include - #include - #include - ], [dnl - fd_set fds; - struct timeval t; - t.tv_sec = 0; - t.tv_usec = 42; - FD_ZERO(&fds); - /* 0 -> STDIN_FILENO */ - FD_SET(0, &fds); - select(FD_SETSIZE, &fds, NULL, NULL, &t); - ])], - [php_cv_have_select=yes], - [php_cv_have_select=no])]) -AS_VAR_IF([php_cv_have_select], [yes], - [AC_DEFINE([HAVE_SELECT], [1], - [Define to 1 if system has a working 'select' function.])]) -]) - if test "$PHP_FPM" != "no"; then PHP_FPM_CLOCK PHP_FPM_TRACE PHP_FPM_BUILTIN_ATOMIC PHP_FPM_LQ - PHP_FPM_KQUEUE - PHP_FPM_EPOLL - PHP_FPM_SELECT AC_CHECK_FUNCS([clearenv setproctitle setproctitle_fast]) AC_CHECK_HEADER([priv.h], [AC_CHECK_FUNCS([setpflags])]) AC_CHECK_HEADER([sys/times.h], [AC_CHECK_FUNCS([times])]) - AC_CHECK_HEADER([port.h], [AC_CHECK_FUNCS([port_create])]) PHP_ARG_WITH([fpm-user],, [AS_HELP_STRING([[--with-fpm-user[=USER]]], From 997ca6c4324f81ff0d8e9b8553914c869c110d1d Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 16 Aug 2025 13:35:39 +0200 Subject: [PATCH 03/52] Fix port and select backend and improve config --- build/php.m4 | 6 ++---- main/poll/poll_backend_port.c | 24 ++++++++++++------------ main/poll/poll_backend_select.c | 30 ++++++++++++------------------ main/poll/poll_core.c | 31 ++++++++++++++++--------------- sapi/fpm/fpm/events/port.c | 12 ++++++------ 5 files changed, 48 insertions(+), 55 deletions(-) diff --git a/build/php.m4 b/build/php.m4 index 1919880b11fb1..4401da03a7b7c 100644 --- a/build/php.m4 +++ b/build/php.m4 @@ -1403,13 +1403,11 @@ AC_DEFUN([PHP_POLL_MECHANISMS], int port = port_create(); return port; ])], [ - AC_DEFINE([HAVE_PORT_H], [1], [Define if event ports are available]) + AC_DEFINE([HAVE_EVENT_PORTS], [1], [Define if event ports are available]) poll_mechanisms="$poll_mechanisms eventport" ]) - dnl These are always available on POSIX - AC_DEFINE([HAVE_POLL_H], [1], [Define if poll is available]) - AC_DEFINE([HAVE_SELECT], [1], [Define if select is available]) + dnl Set poll mechanisms including poll and select that are always available poll_mechanisms="$poll_mechanisms poll select" AC_MSG_RESULT([$poll_mechanisms]) diff --git a/main/poll/poll_backend_port.c b/main/poll/poll_backend_port.c index c9097e33f3627..65655b39a12f7 100644 --- a/main/poll/poll_backend_port.c +++ b/main/poll/poll_backend_port.c @@ -14,7 +14,7 @@ #include "php_poll.h" -#ifdef HAVE_PORT_CREATE +#ifdef HAVE_EVENT_PORTS #include #include @@ -79,14 +79,14 @@ static int eventport_backend_init(php_poll_ctx_t *ctx, int max_events) { eventport_backend_data_t *data = calloc(1, sizeof(eventport_backend_data_t)); if (!data) { - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } /* Create event port */ data->port_fd = port_create(); if (data->port_fd == -1) { free(data); - return PHP_POLL_ERROR; + return PHP_POLL_ERR_FAIL; } data->max_events = max_events; @@ -97,7 +97,7 @@ static int eventport_backend_init(php_poll_ctx_t *ctx, int max_events) if (!data->events) { close(data->port_fd); free(data); - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; } ctx->backend_data = data; @@ -129,14 +129,14 @@ static int eventport_backend_add(php_poll_ctx_t *ctx, int fd, uint32_t events, v if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, user_data) == -1) { switch (errno) { case EEXIST: - return PHP_POLL_EXISTS; + return PHP_POLL_ERR_EXISTS; case ENOMEM: - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; case EBADF: case EINVAL: return PHP_POLL_ERR_INVALID; default: - return PHP_POLL_ERROR; + return PHP_POLL_ERR_FAIL; } } @@ -157,12 +157,12 @@ static int eventport_backend_modify(php_poll_ctx_t *ctx, int fd, uint32_t events if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, user_data) == -1) { switch (errno) { case ENOMEM: - return PHP_POLL_NOMEM; + return PHP_POLL_ERR_NOMEM; case EBADF: case EINVAL: return PHP_POLL_ERR_INVALID; default: - return PHP_POLL_ERROR; + return PHP_POLL_ERR_FAIL; } } @@ -182,7 +182,7 @@ static int eventport_backend_remove(php_poll_ctx_t *ctx, int fd) case EINVAL: return PHP_POLL_ERR_INVALID; default: - return PHP_POLL_ERROR; + return PHP_POLL_ERR_FAIL; } } @@ -229,7 +229,7 @@ static int eventport_backend_wait( return 0; } else { /* Real error */ - return PHP_POLL_ERROR; + return PHP_POLL_ERR_FAIL; } } @@ -300,4 +300,4 @@ const php_poll_backend_ops_t php_poll_backend_eventport_ops = { .supports_et = true /* Event ports provide edge-triggered semantics by default */ }; -#endif /* HAVE_PORT_CREATE */ +#endif /* HAVE_EVENT_PORTS */ diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c index 2406c99aa31b3..3933ca839411b 100644 --- a/main/poll/poll_backend_select.c +++ b/main/poll/poll_backend_select.c @@ -13,16 +13,12 @@ */ #include "php_poll.h" - -#ifdef _WIN32 - -#include -#include +#include "php_network.h" typedef struct { fd_set read_fds, write_fds, error_fds; fd_set master_read_fds, master_write_fds, master_error_fds; - SOCKET *socket_list; + php_socket_t *socket_list; void **data_list; int socket_count; int max_sockets; @@ -36,7 +32,7 @@ static int select_backend_init(php_poll_ctx *ctx, int max_events) } data->max_sockets = max_events; - data->socket_list = calloc(max_events, sizeof(SOCKET)); + data->socket_list = calloc(max_events, sizeof(php_socket_t)); data->data_list = calloc(max_events, sizeof(void *)); if (!data->socket_list || !data->data_list) { @@ -66,7 +62,7 @@ static void select_backend_cleanup(php_poll_ctx *ctx) } } -static int select_find_socket_index(select_backend_data_t *data, SOCKET sock) +static int select_find_socket_index(select_backend_data_t *data, php_socket_t sock) { for (int i = 0; i < data->socket_count; i++) { if (data->socket_list[i] == sock) { @@ -79,7 +75,7 @@ static int select_find_socket_index(select_backend_data_t *data, SOCKET sock) static int select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; - SOCKET sock = (SOCKET) fd; + php_socket_t sock = (php_socket_t) fd; if (backend_data->socket_count >= backend_data->max_sockets) { return PHP_POLL_ERR_NOMEM; @@ -87,7 +83,7 @@ static int select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void * /* Check if socket already exists */ if (select_find_socket_index(backend_data, sock) >= 0) { - return PHP_POLL_EXISTS; + return PHP_POLL_ERR_EXISTS; } /* Add socket to our tracking */ @@ -111,11 +107,11 @@ static int select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void * static int select_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; - SOCKET sock = (SOCKET) fd; + php_socket_t sock = (php_socket_t) fd; int index = select_find_socket_index(backend_data, sock); if (index < 0) { - return PHP_POLL_NOTFOUND; + return PHP_POLL_ERR_NOTFOUND; } /* Update user data */ @@ -141,11 +137,11 @@ static int select_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, voi static int select_backend_remove(php_poll_ctx *ctx, int fd) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; - SOCKET sock = (SOCKET) fd; + php_socket_t sock = (php_socket_t) fd; int index = select_find_socket_index(backend_data, sock); if (index < 0) { - return PHP_POLL_NOTFOUND; + return PHP_POLL_ERR_NOTFOUND; } /* Remove from fd_sets */ @@ -171,7 +167,7 @@ static int select_backend_wait( if (backend_data->socket_count == 0) { /* No sockets to wait for */ if (timeout > 0) { - Sleep(timeout); + php_sleep(timeout); } return 0; } @@ -200,7 +196,7 @@ static int select_backend_wait( /* Process results */ int event_count = 0; for (int i = 0; i < backend_data->socket_count && event_count < max_events; i++) { - SOCKET sock = backend_data->socket_list[i]; + php_socket_t sock = backend_data->socket_list[i]; uint32_t revents = 0; if (FD_ISSET(sock, &backend_data->read_fds)) { @@ -241,5 +237,3 @@ const php_poll_backend_ops php_poll_backend_select_ops = { .is_available = select_backend_is_available, .supports_et = false /* select() doesn't support edge triggering */ }; - -#endif /* _WIN32 */ diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index de6cd53667186..9b1479549af3b 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -19,20 +19,23 @@ static const php_poll_backend_ops *registered_backends[16]; static int num_registered_backends = 0; /* Forward declarations for backend ops */ + +#ifdef HAVE_POLL extern const php_poll_backend_ops php_poll_backend_poll_ops; +#endif #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_PORT_H +#ifdef HAVE_EVENT_PORTS extern const php_poll_backend_ops php_poll_backend_eventport_ops; #endif #ifdef _WIN32 extern const php_poll_backend_ops php_poll_backend_iocp_ops; -extern const php_poll_backend_ops php_poll_backend_select_ops; #endif +extern const php_poll_backend_ops php_poll_backend_select_ops; /* Register all available backends */ void php_poll_register_backends(void) @@ -46,36 +49,34 @@ void php_poll_register_backends(void) } #endif -#ifdef HAVE_PORT_H +#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_SYS_EPOLL_H - if (php_poll_backend_epoll_ops.is_available()) { - registered_backends[num_registered_backends++] = &php_poll_backend_epoll_ops; - } -#endif - -#ifdef HAVE_SYS_EVENT_H +#ifdef HAVE_KQUEUE if (php_poll_backend_kqueue_ops.is_available()) { registered_backends[num_registered_backends++] = &php_poll_backend_kqueue_ops; } #endif -#ifdef _WIN32 - /* Windows select() as fallback before poll() */ - if (php_poll_backend_select_ops.is_available()) { - registered_backends[num_registered_backends++] = &php_poll_backend_select_ops; +#ifdef HAVE_EPOLL + if (php_poll_backend_epoll_ops.is_available()) { + registered_backends[num_registered_backends++] = &php_poll_backend_epoll_ops; } #endif -#ifndef _WIN32 +#ifdef HAVE_POLL /* Poll is available on Unix-like systems */ registered_backends[num_registered_backends++] = &php_poll_backend_poll_ops; #endif + + /* select() as a fallback */ + if (php_poll_backend_select_ops.is_available()) { + registered_backends[num_registered_backends++] = &php_poll_backend_select_ops; + } } /* Get backend operations */ diff --git a/sapi/fpm/fpm/events/port.c b/sapi/fpm/fpm/events/port.c index 73cf24c82c2c2..7731b7f97f2e9 100644 --- a/sapi/fpm/fpm/events/port.c +++ b/sapi/fpm/fpm/events/port.c @@ -19,7 +19,7 @@ #include "../fpm.h" #include "../zlog.h" -#ifdef HAVE_PORT_CREATE +#ifdef HAVE_EVENT_PORTS #include #include @@ -45,19 +45,19 @@ port_event_t *events = NULL; int nevents = 0; static int pfd = -1; -#endif /* HAVE_PORT_CREATE */ +#endif /* HAVE_EVENT_PORTS */ struct fpm_event_module_s *fpm_event_port_module(void) /* {{{ */ { -#ifdef HAVE_PORT_CREATE +#ifdef HAVE_EVENT_PORTS return &port_module; #else return NULL; -#endif /* HAVE_PORT_CREATE */ +#endif /* HAVE_EVENT_PORTS */ } /* }}} */ -#ifdef HAVE_PORT_CREATE +#ifdef HAVE_EVENT_PORTS /* * Init the module @@ -196,4 +196,4 @@ static int fpm_event_port_remove(struct fpm_event_s *ev) /* {{{ */ } /* }}} */ -#endif /* HAVE_PORT_CREATE */ +#endif /* HAVE_EVENT_PORTS */ From 2b55ce218f1e09f617e82fc2c0ca4a181d46eb2c Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 16 Aug 2025 20:11:34 +0200 Subject: [PATCH 04/52] poll: restructure and refactore error handling and other bits --- configure.ac | 2 +- main/php_poll.h | 121 +++++------------- main/poll/php_poll_internal.h | 99 ++++++++++++++ main/poll/poll_backend_epoll.c | 39 +++--- ...ackend_port.c => poll_backend_eventport.c} | 67 ++++++---- main/poll/poll_backend_iocp.c | 43 ++++--- main/poll/poll_backend_kqueue.c | 38 +++--- main/poll/poll_backend_poll.c | 43 ++++--- main/poll/poll_backend_select.c | 41 +++--- main/poll/poll_core.c | 115 ++++++++++------- 10 files changed, 359 insertions(+), 249 deletions(-) create mode 100644 main/poll/php_poll_internal.h rename main/poll/{poll_backend_port.c => poll_backend_eventport.c} (83%) diff --git a/configure.ac b/configure.ac index 1958d7d212f45..f06c242b16474 100644 --- a/configure.ac +++ b/configure.ac @@ -1680,9 +1680,9 @@ PHP_ADD_SOURCES_X([main], PHP_ADD_SOURCES([main/poll], m4_normalize([ poll_backend_epoll.c + poll_backend_eventport.c poll_backend_kqueue.c poll_backend_poll.c - poll_backend_port.c poll_backend_select.c poll_core.c ]), diff --git a/main/php_poll.h b/main/php_poll.h index 5df890a145461..20c3fe3885149 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -17,14 +17,16 @@ #include "php.h" +/* clang-format off */ + /* Event types */ -#define PHP_POLL_READ 0x01 -#define PHP_POLL_WRITE 0x02 -#define PHP_POLL_ERROR 0x04 -#define PHP_POLL_HUP 0x08 -#define PHP_POLL_RDHUP 0x10 -#define PHP_POLL_ONESHOT 0x20 -#define PHP_POLL_ET 0x40 /* Edge-triggered */ +#define PHP_POLL_READ 0x01 +#define PHP_POLL_WRITE 0x02 +#define PHP_POLL_ERROR 0x04 +#define PHP_POLL_HUP 0x08 +#define PHP_POLL_RDHUP 0x10 +#define PHP_POLL_ONESHOT 0x20 +#define PHP_POLL_ET 0x40 /* Edge-triggered */ /* Poll backend types */ typedef enum { @@ -37,102 +39,43 @@ typedef enum { PHP_POLL_BACKEND_IOCP } php_poll_backend_type; -/* Error codes */ -#define PHP_POLL_ERR_NONE 0 -#define PHP_POLL_ERR_FAIL -1 -#define PHP_POLL_ERR_NOMEM -2 -#define PHP_POLL_ERR_INVALID -3 -#define PHP_POLL_ERR_EXISTS -4 -#define PHP_POLL_ERR_NOTFOUND -5 -#define PHP_POLL_ERR_TIMEOUT -6 +/* Result codes */ +typedef enum { + PHP_POLL_ERR_NONE, + PHP_POLL_ERR_SYSTEM, + PHP_POLL_ERR_NOMEM, + PHP_POLL_ERR_INVALID, + PHP_POLL_ERR_EXISTS, + PHP_POLL_ERR_NOTFOUND, + PHP_POLL_ERR_TIMEOUT, +} php_poll_error; + +/* clang-format on */ /* Forward declarations */ typedef struct php_poll_ctx php_poll_ctx; typedef struct php_poll_fd_entry php_poll_fd_entry; - -/* Poll event structure */ -typedef struct { - int fd; /* File descriptor */ - uint32_t events; /* Requested events */ - uint32_t revents; /* Returned events */ - void *data; /* User data pointer */ -} php_poll_event_t; - -/* FD entry for tracking state */ -struct php_poll_fd_entry { - int fd; - uint32_t events; - uint32_t last_revents; /* For edge-trigger simulation */ - void *data; - bool active; - bool et_armed; /* Edge-trigger state */ -}; - -/* Backend interface */ -typedef struct php_poll_backend_ops { - const char *name; - - /* Initialize backend */ - int (*init)(php_poll_ctx *ctx, int max_events); - - /* Cleanup backend */ - void (*cleanup)(php_poll_ctx *ctx); - - /* Add file descriptor */ - int (*add)(php_poll_ctx *ctx, int fd, uint32_t events, void *data); - - /* Modify file descriptor */ - int (*modify)(php_poll_ctx *ctx, int fd, uint32_t events, void *data); - - /* Remove file descriptor */ - int (*remove)(php_poll_ctx *ctx, int fd); - - /* Wait for events */ - int (*wait)(php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout); - - /* Check if backend is available */ - bool (*is_available)(void); - - /* 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; - - int max_events; - int num_fds; - bool initialized; - bool simulate_et; /* Whether to simulate edge triggering */ - - /* FD tracking for edge-trigger simulation */ - php_poll_fd_entry *fd_entries; - int fd_entries_size; - - /* Backend-specific data */ - void *backend_data; -}; +typedef struct php_poll_backend_ops php_poll_backend_ops; +typedef struct php_poll_event php_poll_event; /* Public API */ -php_poll_ctx* php_poll_create(int max_events, php_poll_backend_type preferred_backend); +php_poll_ctx *php_poll_create(int max_events, php_poll_backend_type preferred_backend); void php_poll_destroy(php_poll_ctx *ctx); -int php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data); -int php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data); -int php_poll_remove(php_poll_ctx *ctx, int fd); +zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data); +zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data); +zend_result php_poll_remove(php_poll_ctx *ctx, int fd); -int php_poll_wait(php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout); +int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout); -const char* php_poll_backend_name(php_poll_ctx *ctx); +const char *php_poll_backend_name(php_poll_ctx *ctx); php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx); bool php_poll_supports_et(php_poll_ctx *ctx); -php_poll_fd_entry *php_poll_find_fd_entry(php_poll_ctx *ctx, int fd); - /* Backend registration */ void php_poll_register_backends(void); -const php_poll_backend_ops* php_poll_get_backend_ops(php_poll_backend_type backend); +const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backend); + +php_poll_error php_poll_get_error(php_poll_ctx *ctx); #endif /* PHP_POLL_H */ diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h new file mode 100644 index 0000000000000..6f4954f38c792 --- /dev/null +++ b/main/poll/php_poll_internal.h @@ -0,0 +1,99 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll.h" + +/* 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 */ +}; + +/* FD entry for tracking state */ +struct php_poll_fd_entry { + int fd; + uint32_t events; + uint32_t last_revents; /* For edge-trigger simulation */ + void *data; + bool active; + bool et_armed; /* Edge-trigger state */ +}; + +/* 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, int max_events); + + /* Cleanup backend */ + void (*cleanup)(php_poll_ctx *ctx); + + /* Add file descriptor */ + zend_result (*add)(php_poll_ctx *ctx, int fd, uint32_t events, void *data); + + /* Modify file descriptor */ + zend_result (*modify)(php_poll_ctx *ctx, int fd, uint32_t events, void *data); + + /* Remove file descriptor */ + zend_result (*remove)(php_poll_ctx *ctx, int fd); + + /* Wait for events */ + int (*wait)(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout); + + /* Check if backend is available */ + bool (*is_available)(void); + + /* 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; + + int max_events; + int num_fds; + bool initialized; + /* Whether to simulate edge triggering */ + bool simulate_et; + + /* FD tracking for edge-trigger simulation */ + php_poll_fd_entry *fd_entries; + int fd_entries_size; + + /* Last error */ + php_poll_error last_error; + + /* Backend-specific data */ + void *backend_data; +}; + +php_poll_fd_entry *php_poll_find_fd_entry(php_poll_ctx *ctx, int fd); + +static inline void php_poll_set_error(php_poll_ctx *ctx, php_poll_error error) +{ + ctx->last_error = error; +} + +static inline void php_poll_set_system_error_if_not_set(php_poll_ctx *ctx) +{ + if (ctx->last_error == PHP_POLL_ERR_NONE) { + ctx->last_error = PHP_POLL_ERR_SYSTEM; + } +} diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c index 055a46e62558f..1b6a3d47d99fa 100644 --- a/main/poll/poll_backend_epoll.c +++ b/main/poll/poll_backend_epoll.c @@ -12,7 +12,7 @@ +----------------------------------------------------------------------+ */ -#include "php_poll.h" +#include "php_poll_internal.h" #ifdef HAVE_EPOLL @@ -71,28 +71,31 @@ static uint32_t epoll_events_from_native(uint32_t native) return events; } -static int epoll_backend_init(php_poll_ctx *ctx, int max_events) +static zend_result epoll_backend_init(php_poll_ctx *ctx, int max_events) { epoll_backend_data_t *data = calloc(1, sizeof(epoll_backend_data_t)); if (!data) { - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } data->epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (data->epoll_fd == -1) { free(data); - return PHP_POLL_ERROR; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } data->events = calloc(max_events, sizeof(struct epoll_event)); if (!data->events) { close(data->epoll_fd); free(data); - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } ctx->backend_data = data; - return PHP_POLL_ERR_NONE; + return SUCCESS; } static void epoll_backend_cleanup(php_poll_ctx *ctx) @@ -117,10 +120,11 @@ static int epoll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *d ev.data.ptr = data; if (epoll_ctl(backend_data->epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) { - return (errno == EEXIST) ? PHP_POLL_ERR_EXISTS : PHP_POLL_ERROR; + php_poll_set_error(ctx, (errno == EEXIST) ? PHP_POLL_ERR_EXISTS : PHP_POLL_ERR_SYSTEM); + return FAILURE; } - return PHP_POLL_ERR_NONE; + return SUCCESS; } static int epoll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) @@ -132,10 +136,11 @@ static int epoll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void ev.data.ptr = data; if (epoll_ctl(backend_data->epoll_fd, EPOLL_CTL_MOD, fd, &ev) == -1) { - return (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERROR; + php_poll_set_error(ctx, (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERR_SYSTEM); + return FAILURE; } - return PHP_POLL_ERR_NONE; + return SUCCESS; } static int epoll_backend_remove(php_poll_ctx *ctx, int fd) @@ -143,14 +148,15 @@ static int 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) { - return (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERROR; + php_poll_set_error(ctx, (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERR_SYSTEM); + return FAILURE; } - return PHP_POLL_ERR_NONE; + return SUCCESS; } static int epoll_backend_wait( - php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) + php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { epoll_backend_data_t *backend_data = (epoll_backend_data_t *) ctx->backend_data; @@ -178,7 +184,9 @@ static bool epoll_backend_is_available(void) return false; } -const php_poll_backend_ops php_poll_backend_epoll_ops = { .name = "epoll", +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, @@ -186,6 +194,7 @@ const php_poll_backend_ops php_poll_backend_epoll_ops = { .name = "epoll", .remove = epoll_backend_remove, .wait = epoll_backend_wait, .is_available = epoll_backend_is_available, - .supports_et = true }; + .supports_et = true, +}; #endif /* HAVE_EPOLL */ diff --git a/main/poll/poll_backend_port.c b/main/poll/poll_backend_eventport.c similarity index 83% rename from main/poll/poll_backend_port.c rename to main/poll/poll_backend_eventport.c index 65655b39a12f7..e6e5cf20f46ba 100644 --- a/main/poll/poll_backend_port.c +++ b/main/poll/poll_backend_eventport.c @@ -12,7 +12,7 @@ +----------------------------------------------------------------------+ */ -#include "php_poll.h" +#include "php_poll_internal.h" #ifdef HAVE_EVENT_PORTS @@ -75,18 +75,20 @@ static uint32_t eventport_events_from_native(int native) } /* Initialize event port backend */ -static int eventport_backend_init(php_poll_ctx_t *ctx, int max_events) +static zend_result eventport_backend_init(php_poll_ctx_t *ctx, int max_events) { eventport_backend_data_t *data = calloc(1, sizeof(eventport_backend_data_t)); if (!data) { - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } /* Create event port */ data->port_fd = port_create(); if (data->port_fd == -1) { free(data); - return PHP_POLL_ERR_FAIL; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } data->max_events = max_events; @@ -97,11 +99,12 @@ static int eventport_backend_init(php_poll_ctx_t *ctx, int max_events) if (!data->events) { close(data->port_fd); free(data); - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } ctx->backend_data = data; - return PHP_POLL_ERR_NONE; + return SUCCESS; } /* Cleanup event port backend */ @@ -119,7 +122,8 @@ static void eventport_backend_cleanup(php_poll_ctx_t *ctx) } /* Add file descriptor to event port */ -static int eventport_backend_add(php_poll_ctx_t *ctx, int fd, uint32_t events, void *user_data) +static zend_result eventport_backend_add( + php_poll_ctx_t *ctx, int fd, uint32_t events, void *user_data) { eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; @@ -129,23 +133,29 @@ static int eventport_backend_add(php_poll_ctx_t *ctx, int fd, uint32_t events, v if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, user_data) == -1) { switch (errno) { case EEXIST: - return PHP_POLL_ERR_EXISTS; + php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); + break; case ENOMEM: - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + break; case EBADF: case EINVAL: - return PHP_POLL_ERR_INVALID; + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; default: - return PHP_POLL_ERR_FAIL; + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; } + return FAILURE; } backend_data->active_associations++; - return PHP_POLL_ERR_NONE; + return SUCCESS; } /* Modify file descriptor in event port */ -static int eventport_backend_modify(php_poll_ctx_t *ctx, int fd, uint32_t events, void *user_data) +static zend_result eventport_backend_modify( + php_poll_ctx_t *ctx, int fd, uint32_t events, void *user_data) { eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; @@ -157,42 +167,50 @@ static int eventport_backend_modify(php_poll_ctx_t *ctx, int fd, uint32_t events if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, user_data) == -1) { switch (errno) { case ENOMEM: - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + break; case EBADF: case EINVAL: - return PHP_POLL_ERR_INVALID; + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; default: - return PHP_POLL_ERR_FAIL; + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; } + return FAILURE; } - return PHP_POLL_ERR_NONE; + return SUCCESS; } /* Remove file descriptor from event port */ -static int eventport_backend_remove(php_poll_ctx_t *ctx, int fd) +static zend_result eventport_backend_remove(php_poll_ctx_t *ctx, int fd) { eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; if (port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd) == -1) { switch (errno) { case ENOENT: - return PHP_POLL_ERR_NOTFOUND; + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + break; case EBADF: case EINVAL: - return PHP_POLL_ERR_INVALID; + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; default: - return PHP_POLL_ERR_FAIL; + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; } + return FAILURE; } backend_data->active_associations--; - return PHP_POLL_ERR_NONE; + return SUCCESS; } /* Wait for events using event port */ static int eventport_backend_wait( - php_poll_ctx_t *ctx, php_poll_event_t *events, int max_events, int timeout) + php_poll_ctx_t *ctx, php_poll_event *events, int max_events, int timeout) { eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; @@ -229,7 +247,7 @@ static int eventport_backend_wait( return 0; } else { /* Real error */ - return PHP_POLL_ERR_FAIL; + return -1; } } @@ -289,6 +307,7 @@ static bool eventport_backend_is_available(void) /* Event port backend operations structure */ const php_poll_backend_ops_t php_poll_backend_eventport_ops = { + .type = PHP_POLL_BACKEND_EVENTPORT, .name = "eventport", .init = eventport_backend_init, .cleanup = eventport_backend_cleanup, diff --git a/main/poll/poll_backend_iocp.c b/main/poll/poll_backend_iocp.c index 775afec48306c..4f0a551625645 100644 --- a/main/poll/poll_backend_iocp.c +++ b/main/poll/poll_backend_iocp.c @@ -12,7 +12,7 @@ +----------------------------------------------------------------------+ */ -#include "php_poll.h" +#include "php_poll_internal.h" #ifdef _WIN32 @@ -38,18 +38,20 @@ typedef struct { LPFN_GETACCEPTEXSOCKADDRS GetAcceptExSockaddrs; } iocp_backend_data_t; -static int iocp_backend_init(php_poll_ctx *ctx, int max_events) +static zend_result iocp_backend_init(php_poll_ctx *ctx, int max_events) { iocp_backend_data_t *data = calloc(1, sizeof(iocp_backend_data_t)); if (!data) { - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } /* Create I/O Completion Port */ data->iocp_handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); if (data->iocp_handle == NULL) { free(data); - return PHP_POLL_ERROR; + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; } data->max_operations = max_events; @@ -58,7 +60,8 @@ static int iocp_backend_init(php_poll_ctx *ctx, int max_events) if (!data->operations) { CloseHandle(data->iocp_handle); free(data); - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } /* Load Winsock extension functions */ @@ -85,7 +88,7 @@ static int iocp_backend_init(php_poll_ctx *ctx, int max_events) data->operation_count = 0; ctx->backend_data = data; - return PHP_POLL_ERR_NONE; + return SUCCESS; } static void iocp_backend_cleanup(php_poll_ctx *ctx) @@ -101,20 +104,22 @@ static void iocp_backend_cleanup(php_poll_ctx *ctx) } } -static int iocp_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +static zend_result iocp_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; SOCKET sock = (SOCKET) fd; if (backend_data->operation_count >= backend_data->max_operations) { - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } /* Associate socket with completion port */ HANDLE result = CreateIoCompletionPort((HANDLE) sock, backend_data->iocp_handle, (ULONG_PTR) sock, 0); if (result == NULL) { - return PHP_POLL_ERROR; + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; } /* Set up operation structure */ @@ -134,21 +139,22 @@ static int iocp_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *da if (result == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) { backend_data->operation_count--; - return PHP_POLL_ERROR; + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; } } - return PHP_POLL_ERR_NONE; + return SUCCESS; } -static int iocp_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +static zend_result iocp_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { /* For IOCP, we need to cancel existing operations and re-add */ iocp_backend_remove(ctx, fd); return iocp_backend_add(ctx, fd, events, data); } -static int iocp_backend_remove(php_poll_ctx *ctx, int fd) +static zend_result iocp_backend_remove(php_poll_ctx *ctx, int fd) { iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; SOCKET sock = (SOCKET) fd; @@ -168,11 +174,10 @@ static int iocp_backend_remove(php_poll_ctx *ctx, int fd) } } - return PHP_POLL_ERR_NONE; + return SUCCESS; } -static int iocp_backend_wait( - php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) +static int iocp_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; @@ -185,7 +190,7 @@ static int iocp_backend_wait( if (!result && overlapped == NULL) { /* Timeout or error */ - return (GetLastError() == WAIT_TIMEOUT) ? 0 : PHP_POLL_ERROR; + return (GetLastError() == WAIT_TIMEOUT) ? 0 : -1; } if (overlapped != NULL) { @@ -228,6 +233,7 @@ static bool iocp_backend_is_available(void) } const php_poll_backend_ops php_poll_backend_iocp_ops = { + .type = PHP_POLL_BACKEND_IOCP, .name = "iocp", .init = iocp_backend_init, .cleanup = iocp_backend_cleanup, @@ -236,8 +242,7 @@ const php_poll_backend_ops php_poll_backend_iocp_ops = { .remove = iocp_backend_remove, .wait = iocp_backend_wait, .is_available = iocp_backend_is_available, - .supports_et - = true /* IOCP provides completion-based model which is inherently edge-triggered */ + .supports_et = true /* IOCP provides completion-based model which is edge-triggered */ }; #endif /* _WIN32 */ diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 6249c1fb5937b..431066c77da41 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -12,7 +12,7 @@ +----------------------------------------------------------------------+ */ -#include "php_poll.h" +#include "php_poll_internal.h" #ifdef HAVE_KQUEUE @@ -24,7 +24,7 @@ typedef struct { int change_capacity; } kqueue_backend_data_t; -static int kqueue_backend_init(php_poll_ctx *ctx, int max_events) +static zend_result kqueue_backend_init(php_poll_ctx *ctx, int max_events) { kqueue_backend_data_t *data = calloc(1, sizeof(kqueue_backend_data_t)); if (!data) { @@ -34,7 +34,8 @@ static int kqueue_backend_init(php_poll_ctx *ctx, int max_events) data->kqueue_fd = kqueue(); if (data->kqueue_fd == -1) { free(data); - return PHP_POLL_ERROR; + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; } data->events = calloc(max_events, sizeof(struct kevent)); @@ -47,11 +48,12 @@ static int kqueue_backend_init(php_poll_ctx *ctx, int max_events) free(data->events); free(data->change_list); free(data); - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } ctx->backend_data = data; - return PHP_POLL_ERR_NONE; + return SUCCESS; } static void kqueue_backend_cleanup(php_poll_ctx *ctx) @@ -68,19 +70,20 @@ static void kqueue_backend_cleanup(php_poll_ctx *ctx) } } -static int kqueue_add_change( +static zend_result kqueue_add_change( kqueue_backend_data_t *data, int fd, int16_t filter, uint16_t flags, void *udata) { if (data->change_count >= data->change_capacity) { - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } struct kevent *kev = &data->change_list[data->change_count++]; EV_SET(kev, fd, filter, flags, 0, 0, udata); - return PHP_POLL_ERR_NONE; + return SUCCESS; } -static int kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +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; @@ -92,33 +95,33 @@ static int kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void * flags |= EV_CLEAR; /* kqueue edge-triggering */ } - int result = PHP_POLL_ERR_NONE; + int result = SUCCESS; if (events & PHP_POLL_READ) { result = kqueue_add_change(backend_data, fd, EVFILT_READ, flags, data); - if (result != PHP_POLL_ERR_NONE) { + if (result != SUCCESS) { return result; } } if (events & PHP_POLL_WRITE) { result = kqueue_add_change(backend_data, fd, EVFILT_WRITE, flags, data); - if (result != PHP_POLL_ERR_NONE) { + if (result != SUCCESS) { return result; } } - return PHP_POLL_ERR_NONE; + return SUCCESS; } -static int kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { /* For kqueue, we delete and re-add */ kqueue_backend_remove(ctx, fd); return kqueue_backend_add(ctx, fd, events, data); } -static int kqueue_backend_remove(php_poll_ctx *ctx, int fd) +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; @@ -126,11 +129,11 @@ static int kqueue_backend_remove(php_poll_ctx *ctx, int fd) kqueue_add_change(backend_data, fd, EVFILT_READ, EV_DELETE, NULL); kqueue_add_change(backend_data, fd, EVFILT_WRITE, EV_DELETE, NULL); - return PHP_POLL_ERR_NONE; + return SUCCESS; } static int kqueue_backend_wait( - php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) + php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; @@ -183,6 +186,7 @@ static bool kqueue_backend_is_available(void) } 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, diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 331165fa7e17e..a867b2af9750d 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -12,7 +12,7 @@ +----------------------------------------------------------------------+ */ -#include "php_poll.h" +#include "php_poll_internal.h" #ifdef HAVE_POLL @@ -62,17 +62,19 @@ static uint32_t poll_events_from_native(uint32_t native) return events; } -static int poll_backend_init(php_poll_ctx *ctx, int max_events) +static zend_result poll_backend_init(php_poll_ctx *ctx, int max_events) { poll_backend_data_t *data = calloc(1, sizeof(poll_backend_data_t)); if (!data) { - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } data->fds = calloc(max_events, sizeof(struct pollfd)); if (!data->fds) { free(data); - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } data->allocated = max_events; @@ -84,7 +86,7 @@ static int poll_backend_init(php_poll_ctx *ctx, int max_events) } ctx->backend_data = data; - return PHP_POLL_ERR_NONE; + return SUCCESS; } static void poll_backend_cleanup(php_poll_ctx *ctx) @@ -97,7 +99,7 @@ static void poll_backend_cleanup(php_poll_ctx *ctx) } } -static int poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +static zend_result poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; @@ -108,28 +110,30 @@ static int poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *da backend_data->fds[i].events = poll_events_to_native(events); backend_data->fds[i].revents = 0; backend_data->used++; - return PHP_POLL_ERR_NONE; + return SUCCESS; } } - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } -static int poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +static zend_result poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; for (int i = 0; i < backend_data->allocated; i++) { if (backend_data->fds[i].fd == fd) { backend_data->fds[i].events = poll_events_to_native(events); - return PHP_POLL_ERR_NONE; + return SUCCESS; } } - return PHP_POLL_ERR_NOTFOUND; + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; } -static int poll_backend_remove(php_poll_ctx *ctx, int fd) +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; @@ -139,15 +143,15 @@ static int poll_backend_remove(php_poll_ctx *ctx, int fd) backend_data->fds[i].events = 0; backend_data->fds[i].revents = 0; backend_data->used--; - return PHP_POLL_ERR_NONE; + return SUCCESS; } } - return PHP_POLL_ERR_NOTFOUND; + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; } -static int poll_backend_wait( - php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) +static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; @@ -183,7 +187,9 @@ static bool poll_backend_is_available(void) return true; /* poll() is always available */ } -const php_poll_backend_ops php_poll_backend_poll_ops = { .name = "poll", +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, @@ -191,6 +197,7 @@ const php_poll_backend_ops php_poll_backend_poll_ops = { .name = "poll", .remove = poll_backend_remove, .wait = poll_backend_wait, .is_available = poll_backend_is_available, - .supports_et = false }; + .supports_et = false, +}; #endif /* HAVE_POLL */ diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c index 3933ca839411b..a3c735c3afb7e 100644 --- a/main/poll/poll_backend_select.c +++ b/main/poll/poll_backend_select.c @@ -12,7 +12,7 @@ +----------------------------------------------------------------------+ */ -#include "php_poll.h" +#include "php_poll_internal.h" #include "php_network.h" typedef struct { @@ -24,11 +24,12 @@ typedef struct { int max_sockets; } select_backend_data_t; -static int select_backend_init(php_poll_ctx *ctx, int max_events) +static zend_result select_backend_init(php_poll_ctx *ctx, int max_events) { select_backend_data_t *data = calloc(1, sizeof(select_backend_data_t)); if (!data) { - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } data->max_sockets = max_events; @@ -39,7 +40,8 @@ static int select_backend_init(php_poll_ctx *ctx, int max_events) free(data->socket_list); free(data->data_list); free(data); - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } FD_ZERO(&data->master_read_fds); @@ -48,7 +50,7 @@ static int select_backend_init(php_poll_ctx *ctx, int max_events) data->socket_count = 0; ctx->backend_data = data; - return PHP_POLL_ERR_NONE; + return SUCCESS; } static void select_backend_cleanup(php_poll_ctx *ctx) @@ -72,18 +74,20 @@ static int select_find_socket_index(select_backend_data_t *data, php_socket_t so return -1; } -static int select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +static zend_result select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; if (backend_data->socket_count >= backend_data->max_sockets) { - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } /* Check if socket already exists */ if (select_find_socket_index(backend_data, sock) >= 0) { - return PHP_POLL_ERR_EXISTS; + php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); + return FAILURE; } /* Add socket to our tracking */ @@ -101,17 +105,18 @@ static int select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void * /* Always monitor for errors */ FD_SET(sock, &backend_data->master_error_fds); - return PHP_POLL_ERR_NONE; + return SUCCESS; } -static int select_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +static zend_result select_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; int index = select_find_socket_index(backend_data, sock); if (index < 0) { - return PHP_POLL_ERR_NOTFOUND; + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; } /* Update user data */ @@ -131,17 +136,18 @@ static int select_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, voi } FD_SET(sock, &backend_data->master_error_fds); - return PHP_POLL_ERR_NONE; + return SUCCESS; } -static int select_backend_remove(php_poll_ctx *ctx, int fd) +static zend_result select_backend_remove(php_poll_ctx *ctx, int fd) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; int index = select_find_socket_index(backend_data, sock); if (index < 0) { - return PHP_POLL_ERR_NOTFOUND; + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; } /* Remove from fd_sets */ @@ -156,11 +162,11 @@ static int select_backend_remove(php_poll_ctx *ctx, int fd) } backend_data->socket_count--; - return PHP_POLL_ERR_NONE; + return SUCCESS; } static int select_backend_wait( - php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) + php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; @@ -190,7 +196,7 @@ static int select_backend_wait( 0, &backend_data->read_fds, &backend_data->write_fds, &backend_data->error_fds, ptv); if (result <= 0) { - return (result == 0) ? 0 : PHP_POLL_ERROR; + return (result == 0) ? 0 : -1; } /* Process results */ @@ -227,6 +233,7 @@ static bool select_backend_is_available(void) } const php_poll_backend_ops php_poll_backend_select_ops = { + .type = PHP_POLL_BACKEND_SELECT, .name = "select", .init = select_backend_init, .cleanup = select_backend_cleanup, diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 9b1479549af3b..6d6abae711e6b 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -12,7 +12,7 @@ +----------------------------------------------------------------------+ */ -#include "php_poll.h" +#include "php_poll_internal.h" /* Backend registry */ static const php_poll_backend_ops *registered_backends[16]; @@ -88,19 +88,7 @@ const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backe } for (int i = 0; i < num_registered_backends; i++) { - if (registered_backends[i] - && ((backend == PHP_POLL_BACKEND_EPOLL - && strcmp(registered_backends[i]->name, "epoll") == 0) - || (backend == PHP_POLL_BACKEND_KQUEUE - && strcmp(registered_backends[i]->name, "kqueue") == 0) - || (backend == PHP_POLL_BACKEND_EVENTPORT - && strcmp(registered_backends[i]->name, "eventport") == 0) - || (backend == PHP_POLL_BACKEND_IOCP - && strcmp(registered_backends[i]->name, "iocp") == 0) - || (backend == PHP_POLL_BACKEND_SELECT - && strcmp(registered_backends[i]->name, "select") == 0) - || (backend == PHP_POLL_BACKEND_POLL - && strcmp(registered_backends[i]->name, "poll") == 0))) { + if (registered_backends[i] && registered_backends[i]->type == backend) { return registered_backends[i]; } } @@ -123,8 +111,9 @@ php_poll_fd_entry *php_poll_find_fd_entry(php_poll_ctx *ctx, int fd) static php_poll_fd_entry *php_poll_get_fd_entry(php_poll_ctx *ctx, int fd) { php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); - if (entry) + if (entry) { return entry; + } /* Find empty slot */ for (int i = 0; i < ctx->fd_entries_size; i++) { @@ -141,7 +130,7 @@ static php_poll_fd_entry *php_poll_get_fd_entry(php_poll_ctx *ctx, int fd) } /* Edge-trigger simulation */ -static int php_poll_simulate_et(php_poll_ctx *ctx, php_poll_event_t *events, int nfds) +static int php_poll_simulate_et(php_poll_ctx *ctx, php_poll_event *events, int nfds) { if (!ctx->simulate_et) { return nfds; /* No simulation needed */ @@ -151,8 +140,9 @@ static int php_poll_simulate_et(php_poll_ctx *ctx, php_poll_event_t *events, int for (int i = 0; i < nfds; i++) { php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, events[i].fd); - if (!entry) + if (!entry) { continue; + } uint32_t new_events = events[i].revents; uint32_t edge_events = 0; @@ -186,12 +176,14 @@ static int php_poll_simulate_et(php_poll_ctx *ctx, php_poll_event_t *events, int /* Create new poll context */ php_poll_ctx *php_poll_create(int max_events, php_poll_backend_type preferred_backend) { - if (max_events <= 0) + if (max_events <= 0) { return NULL; + } php_poll_ctx *ctx = calloc(1, sizeof(php_poll_ctx)); - if (!ctx) + if (!ctx) { return NULL; + } /* Get backend operations */ ctx->backend_ops = php_poll_get_backend_ops(preferred_backend); @@ -212,7 +204,7 @@ php_poll_ctx *php_poll_create(int max_events, php_poll_backend_type preferred_ba } /* Initialize backend */ - if (ctx->backend_ops->init(ctx, max_events) != PHP_POLL_ERR_NONE) { + if (ctx->backend_ops->init(ctx, max_events) != SUCCESS) { free(ctx->fd_entries); free(ctx); return NULL; @@ -227,8 +219,9 @@ php_poll_ctx *php_poll_create(int max_events, php_poll_backend_type preferred_ba /* Destroy poll context */ void php_poll_destroy(php_poll_ctx *ctx) { - if (!ctx) + if (!ctx) { return; + } if (ctx->backend_ops && ctx->backend_ops->cleanup) { ctx->backend_ops->cleanup(ctx); @@ -239,20 +232,23 @@ void php_poll_destroy(php_poll_ctx *ctx) } /* Add file descriptor */ -int php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { - if (!ctx || !ctx->initialized || fd < 0) { - return PHP_POLL_ERR_INVALID; + if (UNEXPECTED(!ctx || !ctx->initialized || fd < 0)) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + return FAILURE; } - if (ctx->num_fds >= ctx->max_events) { - return PHP_POLL_ERR_NOMEM; + if (UNEXPECTED(ctx->num_fds >= ctx->max_events)) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } /* Get FD entry for tracking */ php_poll_fd_entry *entry = php_poll_get_fd_entry(ctx, fd); if (!entry) { - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } entry->events = events; @@ -264,26 +260,29 @@ int php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) backend_events &= ~PHP_POLL_ET; /* Remove ET flag for backend */ } - int result = ctx->backend_ops->add(ctx, fd, backend_events, data); - if (result == PHP_POLL_ERR_NONE) { + if (EXPECTED(ctx->backend_ops->add(ctx, fd, backend_events, data) == SUCCESS)) { ctx->num_fds++; - } else { - entry->active = false; /* Rollback */ + return SUCCESS; } - return result; + entry->active = false; /* Rollback */ + php_poll_set_system_error_if_not_set(ctx); + + return FAILURE; } /* Modify file descriptor */ -int php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { - if (!ctx || !ctx->initialized || fd < 0) { - return PHP_POLL_ERR_INVALID; + if (UNEXPECTED(!ctx || !ctx->initialized || fd < 0)) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + return FAILURE; } php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); - if (!entry) { - return PHP_POLL_ERR_NOTFOUND; + if (UNEXPECTED(!entry)) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; } entry->events = events; @@ -295,35 +294,43 @@ int php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) backend_events &= ~PHP_POLL_ET; } - return ctx->backend_ops->modify(ctx, fd, backend_events, data); + if (EXPECTED(ctx->backend_ops->modify(ctx, fd, backend_events, data) == SUCCESS)) { + return SUCCESS; + } + + php_poll_set_system_error_if_not_set(ctx); + return FAILURE; } /* Remove file descriptor */ -int php_poll_remove(php_poll_ctx *ctx, int fd) +zend_result php_poll_remove(php_poll_ctx *ctx, int fd) { - if (!ctx || !ctx->initialized || fd < 0) { - return PHP_POLL_ERR_INVALID; + if (UNEXPECTED(!ctx || !ctx->initialized || fd < 0)) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + return FAILURE; } php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); - if (!entry) { - return PHP_POLL_ERR_NOTFOUND; + if (UNEXPECTED(!entry)) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; } - int result = ctx->backend_ops->remove(ctx, fd); - if (result == PHP_POLL_ERR_NONE) { + if (EXPECTED(ctx->backend_ops->remove(ctx, fd) == SUCCESS)) { entry->active = false; ctx->num_fds--; } - return result; + php_poll_set_system_error_if_not_set(ctx); + return FAILURE; } /* Wait for events */ -int php_poll_wait(php_poll_ctx *ctx, php_poll_event_t *events, int max_events, int timeout) +int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { - if (!ctx || !ctx->initialized || !events || max_events <= 0) { - return PHP_POLL_ERR_INVALID; + if (UNEXPECTED(!ctx || !ctx->initialized || !events || max_events <= 0)) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + return -1; } int nfds = ctx->backend_ops->wait(ctx, events, max_events, timeout); @@ -332,6 +339,10 @@ int php_poll_wait(php_poll_ctx *ctx, php_poll_event_t *events, int max_events, i nfds = php_poll_simulate_et(ctx, events, nfds); } + if (UNEXPECTED(nfds < 0)) { + php_poll_set_system_error_if_not_set(ctx); + } + return nfds; } @@ -352,3 +363,9 @@ bool php_poll_supports_et(php_poll_ctx *ctx) { return ctx && (ctx->backend_ops->supports_et || ctx->simulate_et); } + +/* Error retrieval */ +php_poll_error php_poll_get_error(php_poll_ctx *ctx) +{ + return ctx ? ctx->last_error : PHP_POLL_ERR_INVALID; +} From 5c37babfe9a5b8354780a03b26fadef964a46242 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 16 Aug 2025 20:43:18 +0200 Subject: [PATCH 05/52] poll: use zend memory allocator --- main/php_poll.h | 3 ++- main/poll/php_poll_internal.h | 1 + main/poll/poll_backend_epoll.c | 12 ++++++------ main/poll/poll_backend_eventport.c | 12 ++++++------ main/poll/poll_backend_iocp.c | 12 ++++++------ main/poll/poll_backend_kqueue.c | 20 ++++++++++---------- main/poll/poll_backend_poll.c | 10 +++++----- main/poll/poll_backend_select.c | 18 +++++++++--------- main/poll/poll_core.c | 8 +++++--- 9 files changed, 50 insertions(+), 46 deletions(-) diff --git a/main/php_poll.h b/main/php_poll.h index 20c3fe3885149..6a03689ec37ed 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -59,7 +59,8 @@ typedef struct php_poll_backend_ops php_poll_backend_ops; typedef struct php_poll_event php_poll_event; /* Public API */ -php_poll_ctx *php_poll_create(int max_events, php_poll_backend_type preferred_backend); +php_poll_ctx *php_poll_create( + int max_events, php_poll_backend_type preferred_backend, bool persistent); void php_poll_destroy(php_poll_ctx *ctx); zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data); diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h index 6f4954f38c792..aee6c1e20fb2c 100644 --- a/main/poll/php_poll_internal.h +++ b/main/poll/php_poll_internal.h @@ -70,6 +70,7 @@ struct php_poll_ctx { int max_events; int num_fds; bool initialized; + bool persistent; /* Whether to simulate edge triggering */ bool simulate_et; diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c index 1b6a3d47d99fa..bce3e502b3710 100644 --- a/main/poll/poll_backend_epoll.c +++ b/main/poll/poll_backend_epoll.c @@ -73,7 +73,7 @@ static uint32_t epoll_events_from_native(uint32_t native) static zend_result epoll_backend_init(php_poll_ctx *ctx, int max_events) { - epoll_backend_data_t *data = calloc(1, sizeof(epoll_backend_data_t)); + epoll_backend_data_t *data = pecalloc(1, sizeof(epoll_backend_data_t), ctx->persistent); if (!data) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; @@ -81,15 +81,15 @@ static zend_result epoll_backend_init(php_poll_ctx *ctx, int max_events) data->epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (data->epoll_fd == -1) { - free(data); + pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } - data->events = calloc(max_events, sizeof(struct epoll_event)); + data->events = pecalloc(max_events, sizeof(struct epoll_event), ctx->persistent); if (!data->events) { close(data->epoll_fd); - free(data); + pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } @@ -105,8 +105,8 @@ static void epoll_backend_cleanup(php_poll_ctx *ctx) if (data->epoll_fd >= 0) { close(data->epoll_fd); } - free(data->events); - free(data); + pefree(data->events, ctx->persistent); + pefree(data, ctx->persistent); ctx->backend_data = NULL; } } diff --git a/main/poll/poll_backend_eventport.c b/main/poll/poll_backend_eventport.c index e6e5cf20f46ba..c5293bb058998 100644 --- a/main/poll/poll_backend_eventport.c +++ b/main/poll/poll_backend_eventport.c @@ -77,7 +77,7 @@ static uint32_t eventport_events_from_native(int native) /* Initialize event port backend */ static zend_result eventport_backend_init(php_poll_ctx_t *ctx, int max_events) { - eventport_backend_data_t *data = calloc(1, sizeof(eventport_backend_data_t)); + eventport_backend_data_t *data = pecalloc(1, sizeof(eventport_backend_data_t), ctx->persistent); if (!data) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; @@ -86,7 +86,7 @@ static zend_result eventport_backend_init(php_poll_ctx_t *ctx, int max_events) /* Create event port */ data->port_fd = port_create(); if (data->port_fd == -1) { - free(data); + pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } @@ -95,10 +95,10 @@ static zend_result eventport_backend_init(php_poll_ctx_t *ctx, int max_events) data->active_associations = 0; /* Allocate event array for port_getn() */ - data->events = calloc(max_events, sizeof(port_event_t)); + data->events = pecalloc(max_events, sizeof(port_event_t), ctx->persistent); if (!data->events) { close(data->port_fd); - free(data); + pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } @@ -115,8 +115,8 @@ static void eventport_backend_cleanup(php_poll_ctx_t *ctx) if (data->port_fd >= 0) { close(data->port_fd); } - free(data->events); - free(data); + pefree(data->events, ctx->persistent); + pefree(data, ctx->persistent); ctx->backend_data = NULL; } } diff --git a/main/poll/poll_backend_iocp.c b/main/poll/poll_backend_iocp.c index 4f0a551625645..640d8c7a26880 100644 --- a/main/poll/poll_backend_iocp.c +++ b/main/poll/poll_backend_iocp.c @@ -40,7 +40,7 @@ typedef struct { static zend_result iocp_backend_init(php_poll_ctx *ctx, int max_events) { - iocp_backend_data_t *data = calloc(1, sizeof(iocp_backend_data_t)); + iocp_backend_data_t *data = pecalloc(1, sizeof(iocp_backend_data_t), ctx->persistent); if (!data) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; @@ -49,17 +49,17 @@ static zend_result iocp_backend_init(php_poll_ctx *ctx, int max_events) /* Create I/O Completion Port */ data->iocp_handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); if (data->iocp_handle == NULL) { - free(data); + pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); return FAILURE; } data->max_operations = max_events; - data->operations = calloc(max_events, sizeof(iocp_operation_t)); + data->operations = pecalloc(max_events, sizeof(iocp_operation_t), ctx->persistent); if (!data->operations) { CloseHandle(data->iocp_handle); - free(data); + pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } @@ -98,8 +98,8 @@ static void iocp_backend_cleanup(php_poll_ctx *ctx) if (data->iocp_handle != NULL) { CloseHandle(data->iocp_handle); } - free(data->operations); - free(data); + pefree(data->operations, ctx->persistent); + pefree(data, ctx->persistent); ctx->backend_data = NULL; } } diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 431066c77da41..11179f714b6ac 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -26,28 +26,28 @@ typedef struct { static zend_result kqueue_backend_init(php_poll_ctx *ctx, int max_events) { - kqueue_backend_data_t *data = calloc(1, sizeof(kqueue_backend_data_t)); + kqueue_backend_data_t *data = pecalloc(1, sizeof(kqueue_backend_data_t), ctx->persistent); if (!data) { return PHP_POLL_ERR_NOMEM; } data->kqueue_fd = kqueue(); if (data->kqueue_fd == -1) { - free(data); + pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); return FAILURE; } - data->events = calloc(max_events, sizeof(struct kevent)); - data->change_list = calloc(max_events * 2, sizeof(struct kevent)); /* Read + Write */ + data->events = pecalloc(max_events, sizeof(struct kevent), ctx->persistent); + data->change_list = pecalloc(max_events * 2, sizeof(struct kevent)); /* Read + Write */ data->change_capacity = max_events * 2; data->change_count = 0; if (!data->events || !data->change_list) { close(data->kqueue_fd); - free(data->events); - free(data->change_list); - free(data); + pefree(data->events, ctx->persistent); + pefree(data->change_list, ctx->persistent); + pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } @@ -63,9 +63,9 @@ static void kqueue_backend_cleanup(php_poll_ctx *ctx) if (data->kqueue_fd >= 0) { close(data->kqueue_fd); } - free(data->events); - free(data->change_list); - free(data); + pefree(data->events, ctx->persistent); + pefree(data->change_list), ctx->persistent; + pefree(data, ctx->persistent); ctx->backend_data = NULL; } } diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index a867b2af9750d..88a32817dd82c 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -64,15 +64,15 @@ static uint32_t poll_events_from_native(uint32_t native) static zend_result poll_backend_init(php_poll_ctx *ctx, int max_events) { - poll_backend_data_t *data = calloc(1, sizeof(poll_backend_data_t)); + poll_backend_data_t *data = pecalloc(1, sizeof(poll_backend_data_t), ctx->persistent); if (!data) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } - data->fds = calloc(max_events, sizeof(struct pollfd)); + data->fds = pecalloc(max_events, sizeof(struct pollfd), ctx->persistent); if (!data->fds) { - free(data); + pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } @@ -93,8 +93,8 @@ static void poll_backend_cleanup(php_poll_ctx *ctx) { poll_backend_data_t *data = (poll_backend_data_t *) ctx->backend_data; if (data) { - free(data->fds); - free(data); + pefree(data->fds, ctx->persistent); + pefree(data, ctx->persistent); ctx->backend_data = NULL; } } diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c index a3c735c3afb7e..d43dd7b376813 100644 --- a/main/poll/poll_backend_select.c +++ b/main/poll/poll_backend_select.c @@ -26,20 +26,20 @@ typedef struct { static zend_result select_backend_init(php_poll_ctx *ctx, int max_events) { - select_backend_data_t *data = calloc(1, sizeof(select_backend_data_t)); + select_backend_data_t *data = pecalloc(1, sizeof(select_backend_data_t), ctx->persistent); if (!data) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } data->max_sockets = max_events; - data->socket_list = calloc(max_events, sizeof(php_socket_t)); - data->data_list = calloc(max_events, sizeof(void *)); + data->socket_list = pecalloc(max_events, sizeof(php_socket_t), ctx->persistent); + data->data_list = pecalloc(max_events, sizeof(void *), ctx->persistent); if (!data->socket_list || !data->data_list) { - free(data->socket_list); - free(data->data_list); - free(data); + pefree(data->socket_list, ctx->persistent); + pefree(data->data_list, ctx->persistent); + pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } @@ -57,9 +57,9 @@ static void select_backend_cleanup(php_poll_ctx *ctx) { select_backend_data_t *data = (select_backend_data_t *) ctx->backend_data; if (data) { - free(data->socket_list); - free(data->data_list); - free(data); + pefree(data->socket_list, ctx->persistent); + pefree(data->data_list, ctx->persistent); + pefree(data, ctx->persistent); ctx->backend_data = NULL; } } diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 6d6abae711e6b..5e68a291d3999 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -174,16 +174,18 @@ static int php_poll_simulate_et(php_poll_ctx *ctx, php_poll_event *events, int n } /* Create new poll context */ -php_poll_ctx *php_poll_create(int max_events, php_poll_backend_type preferred_backend) +php_poll_ctx *php_poll_create( + int max_events, php_poll_backend_type preferred_backend, bool persistent) { if (max_events <= 0) { return NULL; } - php_poll_ctx *ctx = calloc(1, sizeof(php_poll_ctx)); + php_poll_ctx *ctx = pecalloc(1, sizeof(php_poll_ctx), persistent); if (!ctx) { return NULL; } + ctx->persistent = persistent; /* Get backend operations */ ctx->backend_ops = php_poll_get_backend_ops(preferred_backend); @@ -196,7 +198,7 @@ php_poll_ctx *php_poll_create(int max_events, php_poll_backend_type preferred_ba ctx->backend_type = preferred_backend; /* Allocate FD entries for edge-trigger simulation */ - ctx->fd_entries = calloc(max_events, sizeof(php_poll_fd_entry)); + ctx->fd_entries = pecalloc(max_events, sizeof(php_poll_fd_entry), persistent); ctx->fd_entries_size = max_events; if (!ctx->fd_entries) { free(ctx); From 6811f99be8e3a0d6b690a710874cc13ee4584b8a Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 16 Aug 2025 20:50:19 +0200 Subject: [PATCH 06/52] poll: split init from create --- main/php_poll.h | 2 ++ main/poll/poll_core.c | 24 ++++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/main/php_poll.h b/main/php_poll.h index 6a03689ec37ed..40e48181417b5 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -61,6 +61,8 @@ typedef struct php_poll_event php_poll_event; /* Public API */ php_poll_ctx *php_poll_create( int max_events, php_poll_backend_type preferred_backend, bool persistent); + +zend_result php_poll_init(php_poll_ctx *ctx); void php_poll_destroy(php_poll_ctx *ctx); zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data); diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 5e68a291d3999..faeb0059fdb46 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -196,6 +196,7 @@ php_poll_ctx *php_poll_create( ctx->max_events = max_events; ctx->backend_type = preferred_backend; + ctx->simulate_et = !ctx->backend_ops->supports_et; /* Allocate FD entries for edge-trigger simulation */ ctx->fd_entries = pecalloc(max_events, sizeof(php_poll_fd_entry), persistent); @@ -205,17 +206,24 @@ php_poll_ctx *php_poll_create( return NULL; } - /* Initialize backend */ - if (ctx->backend_ops->init(ctx, max_events) != SUCCESS) { - free(ctx->fd_entries); - free(ctx); - return NULL; + return ctx; +} + +/* Initialize poll context */ +zend_result php_poll_init(php_poll_ctx *ctx) +{ + if (UNEXPECTED(ctx->initialized)) { + return SUCCESS; } - ctx->initialized = true; - ctx->simulate_et = !ctx->backend_ops->supports_et; + /* Initialize backend */ + if (EXPECTED(ctx->backend_ops->init(ctx, ctx->max_events) == SUCCESS)) { + ctx->initialized = true; + return SUCCESS; + } - return ctx; + php_poll_set_system_error_if_not_set(ctx); + return FAILURE; } /* Destroy poll context */ From 16489ef13a890bd3dd7d5c8b0bb0c683b2f8d7fe Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 16 Aug 2025 20:51:55 +0200 Subject: [PATCH 07/52] poll: use pefree instead of free everywhere --- main/poll/poll_core.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index faeb0059fdb46..7c78056111694 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -190,7 +190,7 @@ php_poll_ctx *php_poll_create( /* Get backend operations */ ctx->backend_ops = php_poll_get_backend_ops(preferred_backend); if (!ctx->backend_ops) { - free(ctx); + pefree(ctx, persistent); return NULL; } @@ -202,7 +202,7 @@ php_poll_ctx *php_poll_create( ctx->fd_entries = pecalloc(max_events, sizeof(php_poll_fd_entry), persistent); ctx->fd_entries_size = max_events; if (!ctx->fd_entries) { - free(ctx); + pefree(ctx, persistent); return NULL; } @@ -237,8 +237,8 @@ void php_poll_destroy(php_poll_ctx *ctx) ctx->backend_ops->cleanup(ctx); } - free(ctx->fd_entries); - free(ctx); + pefree(ctx->fd_entries, ctx->persistent); + pefree(ctx, ctx->persistent); } /* Add file descriptor */ From bdeeb30c746261c765dce526b4b33f886dd74898 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 16 Aug 2025 21:02:37 +0200 Subject: [PATCH 08/52] poll: declare PHPAPI --- main/php_poll.h | 26 ++++++++++++-------------- main/poll/php_poll_internal.h | 3 +++ main/poll/poll_core.c | 24 ++++++++++++------------ 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/main/php_poll.h b/main/php_poll.h index 40e48181417b5..b0e2aba90ba54 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -59,26 +59,24 @@ typedef struct php_poll_backend_ops php_poll_backend_ops; typedef struct php_poll_event php_poll_event; /* Public API */ -php_poll_ctx *php_poll_create( +PHPAPI php_poll_ctx *php_poll_create( int max_events, php_poll_backend_type preferred_backend, bool persistent); -zend_result php_poll_init(php_poll_ctx *ctx); -void php_poll_destroy(php_poll_ctx *ctx); +PHPAPI zend_result php_poll_init(php_poll_ctx *ctx); +PHPAPI void php_poll_destroy(php_poll_ctx *ctx); -zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data); -zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data); -zend_result php_poll_remove(php_poll_ctx *ctx, int fd); +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); -int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout); +PHPAPI int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout); -const char *php_poll_backend_name(php_poll_ctx *ctx); -php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx); -bool php_poll_supports_et(php_poll_ctx *ctx); +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); /* Backend registration */ -void php_poll_register_backends(void); -const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backend); - -php_poll_error php_poll_get_error(php_poll_ctx *ctx); +PHPAPI void php_poll_register_backends(void); #endif /* PHP_POLL_H */ diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h index aee6c1e20fb2c..417ecb4da87d2 100644 --- a/main/poll/php_poll_internal.h +++ b/main/poll/php_poll_internal.h @@ -85,6 +85,9 @@ struct php_poll_ctx { void *backend_data; }; + +const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backend); + php_poll_fd_entry *php_poll_find_fd_entry(php_poll_ctx *ctx, int fd); static inline void php_poll_set_error(php_poll_ctx *ctx, php_poll_error error) diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 7c78056111694..7b72ccd1ccaf5 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -38,7 +38,7 @@ extern const php_poll_backend_ops php_poll_backend_iocp_ops; extern const php_poll_backend_ops php_poll_backend_select_ops; /* Register all available backends */ -void php_poll_register_backends(void) +PHPAPI void php_poll_register_backends(void) { num_registered_backends = 0; @@ -174,7 +174,7 @@ static int php_poll_simulate_et(php_poll_ctx *ctx, php_poll_event *events, int n } /* Create new poll context */ -php_poll_ctx *php_poll_create( +PHPAPI php_poll_ctx *php_poll_create( int max_events, php_poll_backend_type preferred_backend, bool persistent) { if (max_events <= 0) { @@ -210,7 +210,7 @@ php_poll_ctx *php_poll_create( } /* Initialize poll context */ -zend_result php_poll_init(php_poll_ctx *ctx) +PHPAPI zend_result php_poll_init(php_poll_ctx *ctx) { if (UNEXPECTED(ctx->initialized)) { return SUCCESS; @@ -227,7 +227,7 @@ zend_result php_poll_init(php_poll_ctx *ctx) } /* Destroy poll context */ -void php_poll_destroy(php_poll_ctx *ctx) +PHPAPI void php_poll_destroy(php_poll_ctx *ctx) { if (!ctx) { return; @@ -242,7 +242,7 @@ void php_poll_destroy(php_poll_ctx *ctx) } /* Add file descriptor */ -zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +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); @@ -282,7 +282,7 @@ zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) } /* Modify file descriptor */ -zend_result php_poll_modify(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) { if (UNEXPECTED(!ctx || !ctx->initialized || fd < 0)) { php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); @@ -313,7 +313,7 @@ zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *da } /* Remove file descriptor */ -zend_result php_poll_remove(php_poll_ctx *ctx, int fd) +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); @@ -336,7 +336,7 @@ zend_result php_poll_remove(php_poll_ctx *ctx, int fd) } /* Wait for events */ -int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) +PHPAPI int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { if (UNEXPECTED(!ctx || !ctx->initialized || !events || max_events <= 0)) { php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); @@ -357,25 +357,25 @@ int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int } /* Get backend name */ -const char *php_poll_backend_name(php_poll_ctx *ctx) +PHPAPI const char *php_poll_backend_name(php_poll_ctx *ctx) { return ctx && ctx->backend_ops ? ctx->backend_ops->name : "unknown"; } /* Get backend type */ -php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx) +PHPAPI php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx) { return ctx ? ctx->backend_type : PHP_POLL_BACKEND_AUTO; } /* Check edge-triggering support */ -bool php_poll_supports_et(php_poll_ctx *ctx) +PHPAPI bool php_poll_supports_et(php_poll_ctx *ctx) { return ctx && (ctx->backend_ops->supports_et || ctx->simulate_et); } /* Error retrieval */ -php_poll_error php_poll_get_error(php_poll_ctx *ctx) +PHPAPI php_poll_error php_poll_get_error(php_poll_ctx *ctx) { return ctx ? ctx->last_error : PHP_POLL_ERR_INVALID; } From 9ce08cb1cd2524ed46bb304cf32853694673d0ea Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 18 Aug 2025 16:27:50 +0200 Subject: [PATCH 09/52] poll: fix kqueue build --- main/poll/poll_backend_kqueue.c | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 11179f714b6ac..86c079574d3ff 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -16,6 +16,10 @@ #ifdef HAVE_KQUEUE +#include +#include +#include + typedef struct { int kqueue_fd; struct kevent *events; @@ -28,7 +32,8 @@ static zend_result kqueue_backend_init(php_poll_ctx *ctx, int max_events) { kqueue_backend_data_t *data = pecalloc(1, sizeof(kqueue_backend_data_t), ctx->persistent); if (!data) { - return PHP_POLL_ERR_NOMEM; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } data->kqueue_fd = kqueue(); @@ -39,7 +44,7 @@ static zend_result kqueue_backend_init(php_poll_ctx *ctx, int max_events) } data->events = pecalloc(max_events, sizeof(struct kevent), ctx->persistent); - data->change_list = pecalloc(max_events * 2, sizeof(struct kevent)); /* Read + Write */ + data->change_list = pecalloc(max_events * 2, sizeof(struct kevent), ctx->persistent); /* Read + Write */ data->change_capacity = max_events * 2; data->change_count = 0; @@ -64,14 +69,15 @@ static void kqueue_backend_cleanup(php_poll_ctx *ctx) close(data->kqueue_fd); } pefree(data->events, ctx->persistent); - pefree(data->change_list), ctx->persistent; + pefree(data->change_list, ctx->persistent); pefree(data, ctx->persistent); ctx->backend_data = NULL; } } static zend_result kqueue_add_change( - kqueue_backend_data_t *data, int fd, int16_t filter, uint16_t flags, void *udata) + php_poll_ctx *ctx, kqueue_backend_data_t *data, int fd, int16_t filter, uint16_t flags, + void *udata) { if (data->change_count >= data->change_capacity) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); @@ -98,14 +104,14 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events int result = SUCCESS; if (events & PHP_POLL_READ) { - result = kqueue_add_change(backend_data, fd, EVFILT_READ, flags, data); + result = kqueue_add_change(ctx, backend_data, fd, EVFILT_READ, flags, data); if (result != SUCCESS) { return result; } } if (events & PHP_POLL_WRITE) { - result = kqueue_add_change(backend_data, fd, EVFILT_WRITE, flags, data); + result = kqueue_add_change(ctx, backend_data, fd, EVFILT_WRITE, flags, data); if (result != SUCCESS) { return result; } @@ -114,24 +120,24 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events return SUCCESS; } -static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) -{ - /* For kqueue, we delete and re-add */ - kqueue_backend_remove(ctx, fd); - return kqueue_backend_add(ctx, fd, events, data); -} - 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; /* Add delete operations for both read and write filters */ - kqueue_add_change(backend_data, fd, EVFILT_READ, EV_DELETE, NULL); - kqueue_add_change(backend_data, fd, EVFILT_WRITE, EV_DELETE, NULL); + kqueue_add_change(ctx, backend_data, fd, EVFILT_READ, EV_DELETE, NULL); + kqueue_add_change(ctx, backend_data, fd, EVFILT_WRITE, EV_DELETE, NULL); return SUCCESS; } +static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +{ + /* For kqueue, we delete and re-add */ + kqueue_backend_remove(ctx, fd); + return kqueue_backend_add(ctx, fd, events, data); +} + static int kqueue_backend_wait( php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { From d2d8978eeb547f6076fe98d7c9f2b01995fee13c Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 19 Aug 2025 15:56:02 +0200 Subject: [PATCH 10/52] Add initial streams polling API --- ext/standard/basic_functions.c | 1 + ext/standard/basic_functions.h | 1 + ext/standard/config.m4 | 1 + ext/standard/stream_poll.c | 398 +++++++++++++++++++++++++++++ ext/standard/stream_poll.stub.php | 113 ++++++++ ext/standard/stream_poll_arginfo.h | 113 ++++++++ main/php_poll.h | 8 + main/poll/php_poll_internal.h | 8 - 8 files changed, 635 insertions(+), 8 deletions(-) create mode 100644 ext/standard/stream_poll.c create mode 100644 ext/standard/stream_poll.stub.php create mode 100644 ext/standard/stream_poll_arginfo.h diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index ee148ff6c9226..dd8498480b28e 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -303,6 +303,7 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */ BASIC_MINIT_SUBMODULE(browscap) BASIC_MINIT_SUBMODULE(standard_filters) BASIC_MINIT_SUBMODULE(user_filters) + BASIC_MINIT_SUBMODULE(stream_poll) BASIC_MINIT_SUBMODULE(password) BASIC_MINIT_SUBMODULE(image) BASIC_MINIT_SUBMODULE(url) diff --git a/ext/standard/basic_functions.h b/ext/standard/basic_functions.h index bad6fcf4e645e..9a12b4a2a3fc6 100644 --- a/ext/standard/basic_functions.h +++ b/ext/standard/basic_functions.h @@ -42,6 +42,7 @@ PHP_MINFO_FUNCTION(basic); ZEND_API void php_get_highlight_struct(zend_syntax_highlighter_ini *syntax_highlighter_ini); +PHP_MINIT_FUNCTION(stream_poll); PHP_MINIT_FUNCTION(user_filters); PHP_RSHUTDOWN_FUNCTION(user_filters); PHP_RSHUTDOWN_FUNCTION(browscap); diff --git a/ext/standard/config.m4 b/ext/standard/config.m4 index ef6b3c5a01018..78f57ebbea149 100644 --- a/ext/standard/config.m4 +++ b/ext/standard/config.m4 @@ -437,6 +437,7 @@ PHP_NEW_EXTENSION([standard], m4_normalize([ scanf.c sha1.c soundex.c + stream_poll.c streamsfuncs.c string.c strnatcmp.c diff --git a/ext/standard/stream_poll.c b/ext/standard/stream_poll.c new file mode 100644 index 0000000000000..39ec4e90cf2f9 --- /dev/null +++ b/ext/standard/stream_poll.c @@ -0,0 +1,398 @@ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "php_network.h" +#include "php_poll.h" +#include "stream_poll_arginfo.h" +#include "zend_exceptions.h" + +static zend_class_entry *stream_poll_context_class_entry; +static zend_class_entry *stream_poll_event_class_entry; +static zend_class_entry *stream_poll_exception_class_entry; +static zend_object_handlers php_stream_poll_context_object_handlers; + +/* Internal structure to hold stream data */ +typedef struct { + php_stream *stream; + zval data; +} php_stream_poll_entry; + +/* Object wrapper for userspace */ +typedef struct { + php_poll_ctx *ctx; + HashTable *stream_map; /* Maps fd -> php_stream_poll_entry */ + int max_events; + zend_object std; +} php_stream_poll_context_object; + +#define PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZOBJ php_stream_poll_context_object_from_zend_object +#define PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(_zv) php_stream_poll_context_object_from_zend_object(Z_OBJ_P(_zv)) + +static inline php_stream_poll_context_object *php_stream_poll_context_object_from_zend_object(zend_object *obj) +{ + return (php_stream_poll_context_object *)((char *)(obj) - XtOffsetOf(php_stream_poll_context_object, std)); +} + +static void stream_map_entry_dtor(zval *zv) +{ + php_stream_poll_entry *entry = Z_PTR_P(zv); + if (entry) { + zval_ptr_dtor(&entry->data); + efree(entry); + } +} + +static void php_stream_poll_context_free_object_storage(zend_object *obj) +{ + php_stream_poll_context_object *intern = PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZOBJ(obj); + + if (intern->ctx) { + php_poll_destroy(intern->ctx); + } + if (intern->stream_map) { + zend_hash_destroy(intern->stream_map); + efree(intern->stream_map); + } + zend_object_std_dtor(&intern->std); +} + +static inline zend_object *php_stream_poll_context_create_object_ex(zend_class_entry *ce, php_poll_ctx **ctx) +{ + php_stream_poll_context_object *intern = zend_object_alloc(sizeof(php_stream_poll_context_object), ce); + + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + + intern->ctx = NULL; + intern->stream_map = NULL; + intern->max_events = 0; + *ctx = NULL; + + return &intern->std; +} + +static zend_object *php_stream_poll_context_create_object(zend_class_entry *ce) +{ + php_poll_ctx *ctx; + return php_stream_poll_context_create_object_ex(ce, &ctx); +} + +/* Create a new stream polling context */ +PHP_FUNCTION(stream_poll_create) +{ + zend_long max_events_long = 1024, backend_long = PHP_POLL_BACKEND_AUTO; + bool max_events_is_null = 1; + php_poll_ctx *poll_ctx; + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_LONG_OR_NULL(max_events_long, max_events_is_null) + Z_PARAM_LONG(backend_long) + ZEND_PARSE_PARAMETERS_END(); + + if (!max_events_is_null && max_events_long <= 0) { + zend_throw_exception(stream_poll_exception_class_entry, "max_events must be positive", 0); + RETURN_THROWS(); + } + + int max_events = max_events_is_null ? 1024 : (int)max_events_long; + php_poll_backend_type backend = (php_poll_backend_type)backend_long; + + poll_ctx = php_poll_create(max_events, backend, false); + if (!poll_ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Failed to create polling context", 0); + RETURN_THROWS(); + } + + if (php_poll_init(poll_ctx) != SUCCESS) { + php_poll_destroy(poll_ctx); + zend_throw_exception(stream_poll_exception_class_entry, "Failed to initialize polling context", 0); + RETURN_THROWS(); + } + + /* Create object */ + object_init_ex(return_value, stream_poll_context_class_entry); + php_stream_poll_context_object *intern = PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(return_value); + + intern->ctx = poll_ctx; + intern->max_events = max_events; + intern->stream_map = emalloc(sizeof(HashTable)); + zend_hash_init(intern->stream_map, 8, NULL, stream_map_entry_dtor, 0); +} + +static php_stream_poll_context_object *get_stream_poll_context_object(zval *obj) +{ + if (Z_TYPE_P(obj) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(obj), stream_poll_context_class_entry)) { + return NULL; + } + return PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(obj); +} + +/* Add a stream to the polling context */ +PHP_FUNCTION(stream_poll_add) +{ + zval *zpoll_ctx, *zdata = NULL; + zend_long events; + php_stream *stream; + php_stream_poll_context_object *context; + php_stream_poll_entry *entry; + php_socket_t fd; + + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + PHP_Z_PARAM_STREAM(stream) + Z_PARAM_LONG(events) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(zdata) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + /* Get file descriptor from stream */ + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + (void*)&fd, 1) != SUCCESS || fd == -1) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); + RETURN_THROWS(); + } + + /* Check if already exists */ + if (zend_hash_index_exists(context->stream_map, (zend_ulong)fd)) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream already added", 0); + RETURN_THROWS(); + } + + /* Create entry */ + entry = emalloc(sizeof(php_stream_poll_entry)); + entry->stream = stream; + if (zdata) { + ZVAL_COPY(&entry->data, zdata); + } else { + ZVAL_NULL(&entry->data); + } + + /* Add to internal poll context */ + if (php_poll_add(context->ctx, (int)fd, (uint32_t)events, entry) != SUCCESS) { + zval_ptr_dtor(&entry->data); + efree(entry); + zend_throw_exception(stream_poll_exception_class_entry, "Failed to add stream to poll", 0); + RETURN_THROWS(); + } + + /* Add to our mapping */ + zend_hash_index_add_ptr(context->stream_map, (zend_ulong)fd, entry); +} + +/* Modify events for a stream in the polling context */ +PHP_FUNCTION(stream_poll_modify) +{ + zval *zpoll_ctx, *zdata = NULL; + zend_long events; + php_stream *stream; + php_stream_poll_context_object *context; + php_stream_poll_entry *entry; + php_socket_t fd; + + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + PHP_Z_PARAM_STREAM(stream) + Z_PARAM_LONG(events) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(zdata) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + /* Get file descriptor from stream */ + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + (void*)&fd, 1) != SUCCESS || fd == -1) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); + RETURN_THROWS(); + } + + /* Find existing entry */ + entry = zend_hash_index_find_ptr(context->stream_map, (zend_ulong)fd); + if (!entry) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream not found", 0); + RETURN_THROWS(); + } + + /* Update data if provided */ + if (zdata) { + zval_ptr_dtor(&entry->data); + ZVAL_COPY(&entry->data, zdata); + } + + /* Modify in poll context */ + if (php_poll_modify(context->ctx, (int)fd, (uint32_t)events, entry) != SUCCESS) { + zend_throw_exception(stream_poll_exception_class_entry, "Failed to modify stream in poll", 0); + RETURN_THROWS(); + } +} + +/* Remove a stream from the polling context */ +PHP_FUNCTION(stream_poll_remove) +{ + zval *zpoll_ctx; + php_stream *stream; + php_stream_poll_context_object *context; + php_socket_t fd; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + PHP_Z_PARAM_STREAM(stream) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + /* Get file descriptor from stream */ + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + (void*)&fd, 1) != SUCCESS || fd == -1) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); + RETURN_THROWS(); + } + + /* Remove from poll context */ + if (php_poll_remove(context->ctx, (int)fd) != SUCCESS) { + zend_throw_exception(stream_poll_exception_class_entry, "Failed to remove stream from poll", 0); + RETURN_THROWS(); + } + + /* Remove from our mapping */ + zend_hash_index_del(context->stream_map, (zend_ulong)fd); +} + +/* Wait for events on streams in the polling context */ +PHP_FUNCTION(stream_poll_wait) +{ + zval *zpoll_ctx; + zend_long timeout = -1; + php_stream_poll_context_object *context; + php_poll_event *events; + int num_events, i; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(timeout) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + events = emalloc(sizeof(php_poll_event) * context->max_events); + + num_events = php_poll_wait(context->ctx, events, context->max_events, (int)timeout); + + if (num_events < 0) { + efree(events); + zend_throw_exception(stream_poll_exception_class_entry, "Poll wait failed", 0); + RETURN_THROWS(); + } + + array_init(return_value); + + for (i = 0; i < num_events; i++) { + php_stream_poll_entry *entry = (php_stream_poll_entry*)events[i].data; + zval event_obj; + + object_init_ex(&event_obj, stream_poll_event_class_entry); + + /* Set stream property */ + zval stream_zval; + php_stream_to_zval(entry->stream, &stream_zval); + zend_update_property(stream_poll_event_class_entry, Z_OBJ(event_obj), + ZEND_STRL("stream"), &stream_zval); + zval_ptr_dtor(&stream_zval); + + /* Set events property */ + zend_update_property_long(stream_poll_event_class_entry, Z_OBJ(event_obj), + ZEND_STRL("events"), events[i].events); + + /* Set data property */ + zend_update_property(stream_poll_event_class_entry, Z_OBJ(event_obj), + ZEND_STRL("data"), &entry->data); + + add_next_index_zval(return_value, &event_obj); + } + + efree(events); +} + +/* Get the backend name for the polling context */ +PHP_FUNCTION(stream_poll_backend_name) +{ + zval *zpoll_ctx; + php_stream_poll_context_object *context; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + const char *backend_name = php_poll_backend_name(context->ctx); + RETURN_STRING(backend_name); +} + +PHP_METHOD(StreamPollContext, __construct) +{ + zend_throw_error(NULL, "Cannot directly construct StreamPollContext, use stream_poll_create() instead"); +} + +/* Initialize the stream poll classes - add to PHP_MINIT_FUNCTION */ +PHP_MINIT_FUNCTION(stream_poll) +{ + /* Register symbols */ + register_stream_poll_symbols(module_number); + + /* Register classes */ + stream_poll_context_class_entry = register_class_StreamPollContext(); + stream_poll_context_class_entry->create_object = php_stream_poll_context_create_object; + stream_poll_context_class_entry->default_object_handlers = &php_stream_poll_context_object_handlers; + + stream_poll_event_class_entry = register_class_StreamPollEvent(); + stream_poll_exception_class_entry = register_class_StreamPollException(zend_ce_exception); + + /* Set up object handlers */ + memcpy(&php_stream_poll_context_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + php_stream_poll_context_object_handlers.offset = XtOffsetOf(php_stream_poll_context_object, std); + php_stream_poll_context_object_handlers.free_obj = php_stream_poll_context_free_object_storage; + + /* Register poll backends */ + php_poll_register_backends(); + + return SUCCESS; +} diff --git a/ext/standard/stream_poll.stub.php b/ext/standard/stream_poll.stub.php new file mode 100644 index 0000000000000..0086ea0c3d7d7 --- /dev/null +++ b/ext/standard/stream_poll.stub.php @@ -0,0 +1,113 @@ + Date: Tue, 19 Aug 2025 21:51:05 +0200 Subject: [PATCH 11/52] poll: add missing private constructor --- ext/standard/stream_poll.stub.php | 1 + ext/standard/stream_poll_arginfo.h | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ext/standard/stream_poll.stub.php b/ext/standard/stream_poll.stub.php index 0086ea0c3d7d7..a26d49154f7d8 100644 --- a/ext/standard/stream_poll.stub.php +++ b/ext/standard/stream_poll.stub.php @@ -76,6 +76,7 @@ final class StreamPollContext { + private final function __construct() {} } final class StreamPollEvent diff --git a/ext/standard/stream_poll_arginfo.h b/ext/standard/stream_poll_arginfo.h index d82ba098095a3..9e4a1fce7f1c9 100644 --- a/ext/standard/stream_poll_arginfo.h +++ b/ext/standard/stream_poll_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: a067ddaa03e0ae4f1d2dd2ac3715fd31bf86ad28 */ + * Stub hash: acb9c32baff24c8f6ad08dd2290ce0170fb9b956 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_stream_poll_create, 0, 0, StreamPollContext, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, max_events, IS_LONG, 1, "null") @@ -29,12 +29,16 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_backend_name, 0, 1, ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_StreamPollContext___construct, 0, 0, 0) +ZEND_END_ARG_INFO() + ZEND_FUNCTION(stream_poll_create); ZEND_FUNCTION(stream_poll_add); ZEND_FUNCTION(stream_poll_modify); ZEND_FUNCTION(stream_poll_remove); ZEND_FUNCTION(stream_poll_wait); ZEND_FUNCTION(stream_poll_backend_name); +ZEND_METHOD(StreamPollContext, __construct); static const zend_function_entry ext_functions[] = { ZEND_FE(stream_poll_create, arginfo_stream_poll_create) @@ -46,6 +50,11 @@ static const zend_function_entry ext_functions[] = { ZEND_FE_END }; +static const zend_function_entry class_StreamPollContext_methods[] = { + ZEND_ME(StreamPollContext, __construct, arginfo_class_StreamPollContext___construct, ZEND_ACC_PRIVATE|ZEND_ACC_FINAL) + ZEND_FE_END +}; + static void register_stream_poll_symbols(int module_number) { REGISTER_LONG_CONSTANT("STREAM_POLL_READ", PHP_POLL_READ, CONST_PERSISTENT); @@ -68,7 +77,7 @@ static zend_class_entry *register_class_StreamPollContext(void) { zend_class_entry ce, *class_entry; - INIT_CLASS_ENTRY(ce, "StreamPollContext", NULL); + INIT_CLASS_ENTRY(ce, "StreamPollContext", class_StreamPollContext_methods); class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); return class_entry; From 209ca2b0965c0ae3033b165af77743290cc353de Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 19 Aug 2025 22:46:30 +0200 Subject: [PATCH 12/52] poll: add initial test --- .../tests/streams/stream_poll_basic.phpt | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 ext/standard/tests/streams/stream_poll_basic.phpt diff --git a/ext/standard/tests/streams/stream_poll_basic.phpt b/ext/standard/tests/streams/stream_poll_basic.phpt new file mode 100644 index 0000000000000..e5cb14ba0fa02 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic.phpt @@ -0,0 +1,193 @@ +--TEST-- +Stream polling basic functionality +--SKIPIF-- + +--FILE-- +getMessage() . "\n"; +} + +// Test 5: Create socket pair for testing +echo "\n=== Test 5: Socket pair operations ===\n"; +$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); +if ($sockets === false) { + echo "SKIP: Cannot create socket pair\n"; + exit(0); +} + +list($socket1, $socket2) = $sockets; + +// Test 6: Add streams to poll +echo "\n=== Test 6: Add streams ===\n"; +try { + stream_poll_add($poll_ctx, $socket1, STREAM_POLL_READ, "socket1_data"); + stream_poll_add($poll_ctx, $socket2, STREAM_POLL_WRITE, "socket2_data"); + echo "OK: Added streams successfully\n"; +} catch (StreamPollException $e) { + echo "ERROR: " . $e->getMessage() . "\n"; +} + +// Test 7: Try to add same stream again (should fail) +echo "\n=== Test 7: Duplicate add (should fail) ===\n"; +try { + stream_poll_add($poll_ctx, $socket1, STREAM_POLL_READ); + echo "ERROR: Should not allow duplicate add\n"; +} catch (StreamPollException $e) { + echo "OK: " . $e->getMessage() . "\n"; +} + +// Test 8: Modify stream events +echo "\n=== Test 8: Modify stream ===\n"; +try { + stream_poll_modify($poll_ctx, $socket1, STREAM_POLL_READ | STREAM_POLL_WRITE, "modified_data"); + echo "OK: Modified stream successfully\n"; +} catch (StreamPollException $e) { + echo "ERROR: " . $e->getMessage() . "\n"; +} + +// Test 9: Poll with timeout (should timeout immediately) +echo "\n=== Test 9: Poll with timeout ===\n"; +try { + $events = stream_poll_wait($poll_ctx, 0); + var_dump(is_array($events)); + echo "Events count: " . count($events) . "\n"; + + // Check if socket2 is ready for writing (it should be) + foreach ($events as $event) { + echo "Event: "; + var_dump($event instanceof StreamPollEvent); + if ($event instanceof StreamPollEvent) { + echo "Events: " . $event->events . ", Data: " . $event->data . "\n"; + } + } +} catch (StreamPollException $e) { + echo "ERROR: " . $e->getMessage() . "\n"; +} + +// Test 10: Write data and poll for read +echo "\n=== Test 10: Write and poll ===\n"; +fwrite($socket2, "test data"); +try { + $events = stream_poll_wait($poll_ctx, 100); // 100ms timeout + echo "Events after write: " . count($events) . "\n"; + + foreach ($events as $event) { + if ($event instanceof StreamPollEvent) { + if ($event->events & STREAM_POLL_READ) { + echo "Read event detected on: " . $event->data . "\n"; + // Read the data + $data = fread($event->stream, 1024); + echo "Read data: '$data'\n"; + } + if ($event->events & STREAM_POLL_WRITE) { + echo "Write event detected on: " . $event->data . "\n"; + } + } + } +} catch (StreamPollException $e) { + echo "ERROR: " . $e->getMessage() . "\n"; +} + +// Test 11: Remove stream +echo "\n=== Test 11: Remove stream ===\n"; +try { + stream_poll_remove($poll_ctx, $socket1); + echo "OK: Removed stream successfully\n"; +} catch (StreamPollException $e) { + echo "ERROR: " . $e->getMessage() . "\n"; +} + +// Test 12: Try to remove same stream again (should fail) +echo "\n=== Test 12: Remove non-existent stream ===\n"; +try { + stream_poll_remove($poll_ctx, $socket1); + echo "ERROR: Should not allow removing non-existent stream\n"; +} catch (StreamPollException $e) { + echo "OK: " . $e->getMessage() . "\n"; +} + +// Test 13: Try to modify removed stream (should fail) +echo "\n=== Test 13: Modify removed stream ===\n"; +try { + stream_poll_modify($poll_ctx, $socket1, STREAM_POLL_READ); + echo "ERROR: Should not allow modifying removed stream\n"; +} catch (StreamPollException $e) { + echo "OK: " . $e->getMessage() . "\n"; +} + +// Clean up +fclose($socket1); +fclose($socket2); + +echo "\n=== Test completed ===\n"; + +?> +--EXPECT-- +=== Test 1: Create polling context === +bool(true) + +=== Test 2: Create with parameters === +bool(true) + +=== Test 3: Backend name === +bool(true) +Backend: %s + +=== Test 4: Cannot construct directly === +OK: Cannot directly construct StreamPollContext, use stream_poll_create() instead + +=== Test 5: Socket pair operations === + +=== Test 6: Add streams === +OK: Added streams successfully + +=== Test 7: Duplicate add (should fail) === +OK: Stream already added + +=== Test 8: Modify stream === +OK: Modified stream successfully + +=== Test 9: Poll with timeout === +bool(true) +Events count: %d + +=== Test 10: Write and poll === +Events after write: %d + +=== Test 11: Remove stream === +OK: Removed stream successfully + +=== Test 12: Remove non-existent stream === +OK: Failed to remove stream from poll + +=== Test 13: Modify removed stream === +OK: Stream not found + +=== Test completed === \ No newline at end of file From c3fd060a7b1cd403539c8dd99c33e01475c8af6b Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Tue, 19 Aug 2025 22:47:01 +0200 Subject: [PATCH 13/52] poll: reorganize stubs so functions are added --- ext/standard/basic_functions.stub.php | 15 +++++++++ ext/standard/basic_functions_arginfo.h | 42 ++++++++++++++++++++++- ext/standard/stream_poll.stub.php | 15 --------- ext/standard/stream_poll_arginfo.h | 46 +------------------------- 4 files changed, 57 insertions(+), 61 deletions(-) diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 541415a5ab918..3284ba9839587 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -3386,6 +3386,21 @@ function soundex(string $string): string {} /* streamsfuncs.c */ +function stream_poll_create(?int $max_events = null, int $backend = STREAM_POLL_BACKEND_AUTO): StreamPollContext {} + +/** @param resource $stream */ +function stream_poll_add(StreamPollContext $poll_ctx, $stream, int $events, mixed $data = null): void {} + +/** @param resource $stream */ +function stream_poll_modify(StreamPollContext $poll_ctx, $stream, int $events, mixed $data = null): void {} + +/** @param resource $stream */ +function stream_poll_remove(StreamPollContext $poll_ctx, $stream): void {} + +function stream_poll_wait(StreamPollContext $poll_ctx, int $timeout = -1): array {} + +function stream_poll_backend_name(StreamPollContext $poll_ctx): string {} + function stream_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, ?int $microseconds = null): int|false {} /** diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index ba9d1710137cc..45fa2fb596833 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: deb4ea96dd130d8a0174678095c30a61e118bd60 */ + * Stub hash: bfee6d05a55da26dfe15a427a76479857723862a */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -1810,6 +1810,34 @@ ZEND_END_ARG_INFO() #define arginfo_soundex arginfo_base64_encode +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_stream_poll_create, 0, 0, StreamPollContext, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, max_events, IS_LONG, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, backend, IS_LONG, 0, "STREAM_POLL_BACKEND_AUTO") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_add, 0, 3, IS_VOID, 0) + ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) + ZEND_ARG_INFO(0, stream) + ZEND_ARG_TYPE_INFO(0, events, IS_LONG, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_MIXED, 0, "null") +ZEND_END_ARG_INFO() + +#define arginfo_stream_poll_modify arginfo_stream_poll_add + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_remove, 0, 2, IS_VOID, 0) + ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) + ZEND_ARG_INFO(0, stream) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_wait, 0, 1, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout, IS_LONG, 0, "-1") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_backend_name, 0, 1, IS_STRING, 0) + ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_stream_select, 0, 4, MAY_BE_LONG|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(1, read, IS_ARRAY, 1) ZEND_ARG_TYPE_INFO(1, write, IS_ARRAY, 1) @@ -2780,6 +2808,12 @@ ZEND_FUNCTION(proc_get_status); ZEND_FUNCTION(quoted_printable_decode); ZEND_FUNCTION(quoted_printable_encode); ZEND_FUNCTION(soundex); +ZEND_FUNCTION(stream_poll_create); +ZEND_FUNCTION(stream_poll_add); +ZEND_FUNCTION(stream_poll_modify); +ZEND_FUNCTION(stream_poll_remove); +ZEND_FUNCTION(stream_poll_wait); +ZEND_FUNCTION(stream_poll_backend_name); ZEND_FUNCTION(stream_select); ZEND_FUNCTION(stream_context_create); ZEND_FUNCTION(stream_context_set_params); @@ -3384,6 +3418,12 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY("quoted_printable_decode", zif_quoted_printable_decode, arginfo_quoted_printable_decode, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("quoted_printable_encode", zif_quoted_printable_encode, arginfo_quoted_printable_encode, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_FE(soundex, arginfo_soundex) + ZEND_FE(stream_poll_create, arginfo_stream_poll_create) + ZEND_FE(stream_poll_add, arginfo_stream_poll_add) + ZEND_FE(stream_poll_modify, arginfo_stream_poll_modify) + ZEND_FE(stream_poll_remove, arginfo_stream_poll_remove) + ZEND_FE(stream_poll_wait, arginfo_stream_poll_wait) + ZEND_FE(stream_poll_backend_name, arginfo_stream_poll_backend_name) ZEND_FE(stream_select, arginfo_stream_select) ZEND_FE(stream_context_create, arginfo_stream_context_create) ZEND_FE(stream_context_set_params, arginfo_stream_context_set_params) diff --git a/ext/standard/stream_poll.stub.php b/ext/standard/stream_poll.stub.php index a26d49154f7d8..cd47668ae9c7b 100644 --- a/ext/standard/stream_poll.stub.php +++ b/ext/standard/stream_poll.stub.php @@ -97,18 +97,3 @@ final class StreamPollEvent class StreamPollException extends Exception { } - -function stream_poll_create(?int $max_events = null, int $backend = STREAM_POLL_BACKEND_AUTO): StreamPollContext {} - -/** @param resource $stream */ -function stream_poll_add(StreamPollContext $poll_ctx, $stream, int $events, mixed $data = null): void {} - -/** @param resource $stream */ -function stream_poll_modify(StreamPollContext $poll_ctx, $stream, int $events, mixed $data = null): void {} - -/** @param resource $stream */ -function stream_poll_remove(StreamPollContext $poll_ctx, $stream): void {} - -function stream_poll_wait(StreamPollContext $poll_ctx, int $timeout = -1): array {} - -function stream_poll_backend_name(StreamPollContext $poll_ctx): string {} diff --git a/ext/standard/stream_poll_arginfo.h b/ext/standard/stream_poll_arginfo.h index 9e4a1fce7f1c9..d556adfbc3c22 100644 --- a/ext/standard/stream_poll_arginfo.h +++ b/ext/standard/stream_poll_arginfo.h @@ -1,55 +1,11 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: acb9c32baff24c8f6ad08dd2290ce0170fb9b956 */ - -ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_stream_poll_create, 0, 0, StreamPollContext, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, max_events, IS_LONG, 1, "null") - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, backend, IS_LONG, 0, "STREAM_POLL_BACKEND_AUTO") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_add, 0, 3, IS_VOID, 0) - ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) - ZEND_ARG_INFO(0, stream) - ZEND_ARG_TYPE_INFO(0, events, IS_LONG, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_MIXED, 0, "null") -ZEND_END_ARG_INFO() - -#define arginfo_stream_poll_modify arginfo_stream_poll_add - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_remove, 0, 2, IS_VOID, 0) - ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) - ZEND_ARG_INFO(0, stream) -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_wait, 0, 1, IS_ARRAY, 0) - ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout, IS_LONG, 0, "-1") -ZEND_END_ARG_INFO() - -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_backend_name, 0, 1, IS_STRING, 0) - ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) -ZEND_END_ARG_INFO() + * Stub hash: adcf3f025228764c97ec58f18c44b14ead8d2467 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_StreamPollContext___construct, 0, 0, 0) ZEND_END_ARG_INFO() -ZEND_FUNCTION(stream_poll_create); -ZEND_FUNCTION(stream_poll_add); -ZEND_FUNCTION(stream_poll_modify); -ZEND_FUNCTION(stream_poll_remove); -ZEND_FUNCTION(stream_poll_wait); -ZEND_FUNCTION(stream_poll_backend_name); ZEND_METHOD(StreamPollContext, __construct); -static const zend_function_entry ext_functions[] = { - ZEND_FE(stream_poll_create, arginfo_stream_poll_create) - ZEND_FE(stream_poll_add, arginfo_stream_poll_add) - ZEND_FE(stream_poll_modify, arginfo_stream_poll_modify) - ZEND_FE(stream_poll_remove, arginfo_stream_poll_remove) - ZEND_FE(stream_poll_wait, arginfo_stream_poll_wait) - ZEND_FE(stream_poll_backend_name, arginfo_stream_poll_backend_name) - ZEND_FE_END -}; - static const zend_function_entry class_StreamPollContext_methods[] = { ZEND_ME(StreamPollContext, __construct, arginfo_class_StreamPollContext___construct, ZEND_ACC_PRIVATE|ZEND_ACC_FINAL) ZEND_FE_END From c567f5a0832b26be3c9a598243ac197be00c2f07 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 20 Aug 2025 15:05:06 +0200 Subject: [PATCH 14/52] poll: do not dtor stream event property --- ext/standard/stream_poll.c | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/standard/stream_poll.c b/ext/standard/stream_poll.c index 39ec4e90cf2f9..2d734611adadf 100644 --- a/ext/standard/stream_poll.c +++ b/ext/standard/stream_poll.c @@ -331,7 +331,6 @@ PHP_FUNCTION(stream_poll_wait) php_stream_to_zval(entry->stream, &stream_zval); zend_update_property(stream_poll_event_class_entry, Z_OBJ(event_obj), ZEND_STRL("stream"), &stream_zval); - zval_ptr_dtor(&stream_zval); /* Set events property */ zend_update_property_long(stream_poll_event_class_entry, Z_OBJ(event_obj), From ca728dc76115db7682dc7bab7469b1602c3296e9 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 20 Aug 2025 16:40:34 +0200 Subject: [PATCH 15/52] poll: split the basic test to multiple eparated scenarios --- .../tests/streams/stream_poll_basic.phpt | 193 ------------------ .../stream_poll_basic_sock_add_only.phpt | 31 +++ .../streams/stream_poll_basic_sock_read.phpt | 44 ++++ .../streams/stream_poll_basic_sock_write.phpt | 48 +++++ .../stream_poll_basic_sock_write_read.phpt | 69 +++++++ .../stream_poll_basic_wait_no_add.phpt | 19 ++ 6 files changed, 211 insertions(+), 193 deletions(-) delete mode 100644 ext/standard/tests/streams/stream_poll_basic.phpt create mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt create mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_read.phpt create mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_write.phpt create mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt create mode 100644 ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt diff --git a/ext/standard/tests/streams/stream_poll_basic.phpt b/ext/standard/tests/streams/stream_poll_basic.phpt deleted file mode 100644 index e5cb14ba0fa02..0000000000000 --- a/ext/standard/tests/streams/stream_poll_basic.phpt +++ /dev/null @@ -1,193 +0,0 @@ ---TEST-- -Stream polling basic functionality ---SKIPIF-- - ---FILE-- -getMessage() . "\n"; -} - -// Test 5: Create socket pair for testing -echo "\n=== Test 5: Socket pair operations ===\n"; -$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); -if ($sockets === false) { - echo "SKIP: Cannot create socket pair\n"; - exit(0); -} - -list($socket1, $socket2) = $sockets; - -// Test 6: Add streams to poll -echo "\n=== Test 6: Add streams ===\n"; -try { - stream_poll_add($poll_ctx, $socket1, STREAM_POLL_READ, "socket1_data"); - stream_poll_add($poll_ctx, $socket2, STREAM_POLL_WRITE, "socket2_data"); - echo "OK: Added streams successfully\n"; -} catch (StreamPollException $e) { - echo "ERROR: " . $e->getMessage() . "\n"; -} - -// Test 7: Try to add same stream again (should fail) -echo "\n=== Test 7: Duplicate add (should fail) ===\n"; -try { - stream_poll_add($poll_ctx, $socket1, STREAM_POLL_READ); - echo "ERROR: Should not allow duplicate add\n"; -} catch (StreamPollException $e) { - echo "OK: " . $e->getMessage() . "\n"; -} - -// Test 8: Modify stream events -echo "\n=== Test 8: Modify stream ===\n"; -try { - stream_poll_modify($poll_ctx, $socket1, STREAM_POLL_READ | STREAM_POLL_WRITE, "modified_data"); - echo "OK: Modified stream successfully\n"; -} catch (StreamPollException $e) { - echo "ERROR: " . $e->getMessage() . "\n"; -} - -// Test 9: Poll with timeout (should timeout immediately) -echo "\n=== Test 9: Poll with timeout ===\n"; -try { - $events = stream_poll_wait($poll_ctx, 0); - var_dump(is_array($events)); - echo "Events count: " . count($events) . "\n"; - - // Check if socket2 is ready for writing (it should be) - foreach ($events as $event) { - echo "Event: "; - var_dump($event instanceof StreamPollEvent); - if ($event instanceof StreamPollEvent) { - echo "Events: " . $event->events . ", Data: " . $event->data . "\n"; - } - } -} catch (StreamPollException $e) { - echo "ERROR: " . $e->getMessage() . "\n"; -} - -// Test 10: Write data and poll for read -echo "\n=== Test 10: Write and poll ===\n"; -fwrite($socket2, "test data"); -try { - $events = stream_poll_wait($poll_ctx, 100); // 100ms timeout - echo "Events after write: " . count($events) . "\n"; - - foreach ($events as $event) { - if ($event instanceof StreamPollEvent) { - if ($event->events & STREAM_POLL_READ) { - echo "Read event detected on: " . $event->data . "\n"; - // Read the data - $data = fread($event->stream, 1024); - echo "Read data: '$data'\n"; - } - if ($event->events & STREAM_POLL_WRITE) { - echo "Write event detected on: " . $event->data . "\n"; - } - } - } -} catch (StreamPollException $e) { - echo "ERROR: " . $e->getMessage() . "\n"; -} - -// Test 11: Remove stream -echo "\n=== Test 11: Remove stream ===\n"; -try { - stream_poll_remove($poll_ctx, $socket1); - echo "OK: Removed stream successfully\n"; -} catch (StreamPollException $e) { - echo "ERROR: " . $e->getMessage() . "\n"; -} - -// Test 12: Try to remove same stream again (should fail) -echo "\n=== Test 12: Remove non-existent stream ===\n"; -try { - stream_poll_remove($poll_ctx, $socket1); - echo "ERROR: Should not allow removing non-existent stream\n"; -} catch (StreamPollException $e) { - echo "OK: " . $e->getMessage() . "\n"; -} - -// Test 13: Try to modify removed stream (should fail) -echo "\n=== Test 13: Modify removed stream ===\n"; -try { - stream_poll_modify($poll_ctx, $socket1, STREAM_POLL_READ); - echo "ERROR: Should not allow modifying removed stream\n"; -} catch (StreamPollException $e) { - echo "OK: " . $e->getMessage() . "\n"; -} - -// Clean up -fclose($socket1); -fclose($socket2); - -echo "\n=== Test completed ===\n"; - -?> ---EXPECT-- -=== Test 1: Create polling context === -bool(true) - -=== Test 2: Create with parameters === -bool(true) - -=== Test 3: Backend name === -bool(true) -Backend: %s - -=== Test 4: Cannot construct directly === -OK: Cannot directly construct StreamPollContext, use stream_poll_create() instead - -=== Test 5: Socket pair operations === - -=== Test 6: Add streams === -OK: Added streams successfully - -=== Test 7: Duplicate add (should fail) === -OK: Stream already added - -=== Test 8: Modify stream === -OK: Modified stream successfully - -=== Test 9: Poll with timeout === -bool(true) -Events count: %d - -=== Test 10: Write and poll === -Events after write: %d - -=== Test 11: Remove stream === -OK: Removed stream successfully - -=== Test 12: Remove non-existent stream === -OK: Failed to remove stream from poll - -=== Test 13: Modify removed stream === -OK: Stream not found - -=== Test completed === \ No newline at end of file diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt new file mode 100644 index 0000000000000..026cf197f5ff2 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt @@ -0,0 +1,31 @@ +--TEST-- +Stream polling basic functionality - only add +--SKIPIF-- + +--FILE-- + +--EXPECT-- +object(StreamPollContext)#1 (0) { +} diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt new file mode 100644 index 0000000000000..47b9166630f10 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt @@ -0,0 +1,44 @@ +--TEST-- +Stream polling basic functionality - socket write +--SKIPIF-- + +--FILE-- +events . ", Data: " . $event->data . "\n"; + } +} + +// Clean up +fclose($socket1); +fclose($socket2); + +?> +--EXPECT-- +bool(true) +Events count: 1 +Event: bool(true) +Events: 2, Data: socket_data diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt new file mode 100644 index 0000000000000..89cfebcfabf5a --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt @@ -0,0 +1,48 @@ +--TEST-- +Stream polling basic functionality - socket write / read +--SKIPIF-- + +--FILE-- +events & STREAM_POLL_READ) { + echo "Read event detected on: " . $event->data . "\n"; + // Read the data + $data = fread($event->stream, 1024); + echo "Read data: '$data'\n"; + } + if ($event->events & STREAM_POLL_WRITE) { + echo "Write event detected on: " . $event->data . "\n"; + } +} + +// Clean up +fclose($socket1); +fclose($socket2); + + +?> +--EXPECT-- +Events after write: 1 +Read event detected on: socket_data +Read data: 'test data' diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt new file mode 100644 index 0000000000000..7900d629ff924 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt @@ -0,0 +1,69 @@ +--TEST-- +Stream polling basic functionality - socket write / read +--SKIPIF-- + +--FILE-- +events . ", Data: " . $event->data . "\n"; + } +} + +fwrite($socket2, "test data"); +$events = stream_poll_wait($poll_ctx, 100); // 100ms timeout +echo "Events after write: " . count($events) . "\n"; + +foreach ($events as $event) { + echo "Event: "; + if ($event instanceof StreamPollEvent) { + if ($event->events & STREAM_POLL_READ) { + echo "Read event detected on: " . $event->data . "\n"; + // Read the data + $data = fread($event->stream, 1024); + echo "Events: " . $event->events . ", Read data: '$data'\n"; + } + if ($event->events & STREAM_POLL_WRITE) { + echo "Write event detected on: " . $event->data . "\n"; + echo "Events: " . $event->events . "\n"; + } + } +} + +// Clean up +fclose($socket1); +fclose($socket2); + + +?> +--EXPECT-- +bool(true) +Events count: 1 +Event: Events: 2, Data: socket2_data +Events after write: 2 +Event: Write event detected on: socket2_data +Events: 2 +Event: Read event detected on: socket1_data +Events: 1, Read data: 'test data' diff --git a/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt b/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt new file mode 100644 index 0000000000000..25dd2f79c2420 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt @@ -0,0 +1,19 @@ +--TEST-- +Stream polling basic functionality - only wait +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +Events count: 0 From ec3b3baad1e01ad983333a9bb33d19bf90e97eb5 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 20 Aug 2025 16:41:12 +0200 Subject: [PATCH 16/52] poll: fix returned events --- ext/standard/stream_poll.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/standard/stream_poll.c b/ext/standard/stream_poll.c index 2d734611adadf..b045dfbcc1d46 100644 --- a/ext/standard/stream_poll.c +++ b/ext/standard/stream_poll.c @@ -334,7 +334,7 @@ PHP_FUNCTION(stream_poll_wait) /* Set events property */ zend_update_property_long(stream_poll_event_class_entry, Z_OBJ(event_obj), - ZEND_STRL("events"), events[i].events); + ZEND_STRL("events"), events[i].revents); /* Set data property */ zend_update_property(stream_poll_event_class_entry, Z_OBJ(event_obj), From 6ea0819917219b80a58d46e9da783fbe5c0ddf4f Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 20 Aug 2025 20:36:23 +0200 Subject: [PATCH 17/52] poll: rewrite kevent to track changed events --- main/poll/poll_backend_kqueue.c | 102 +++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 86c079574d3ff..cd1c15144c2f4 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -19,13 +19,13 @@ #include #include #include - typedef struct { int kqueue_fd; struct kevent *events; struct kevent *change_list; int change_count; int change_capacity; + int events_capacity; } kqueue_backend_data_t; static zend_result kqueue_backend_init(php_poll_ctx *ctx, int max_events) @@ -43,9 +43,13 @@ static zend_result kqueue_backend_init(php_poll_ctx *ctx, int max_events) return FAILURE; } - data->events = pecalloc(max_events, sizeof(struct kevent), ctx->persistent); - data->change_list = pecalloc(max_events * 2, sizeof(struct kevent), ctx->persistent); /* Read + Write */ - data->change_capacity = max_events * 2; + int initial_events = (max_events > 0) ? max_events : 64; + int initial_changes = max_events * 2; + + data->events = pecalloc(initial_events, sizeof(struct kevent), ctx->persistent); + data->change_list = pecalloc(initial_changes, sizeof(struct kevent), ctx->persistent); + data->events_capacity = initial_events; + data->change_capacity = initial_changes; data->change_count = 0; if (!data->events || !data->change_list) { @@ -75,15 +79,53 @@ static void kqueue_backend_cleanup(php_poll_ctx *ctx) } } -static zend_result kqueue_add_change( - php_poll_ctx *ctx, kqueue_backend_data_t *data, int fd, int16_t filter, uint16_t flags, - void *udata) +/* Helper function to find existing change for same (ident, filter) */ +static int find_existing_change(kqueue_backend_data_t *data, int fd, int16_t filter) { - if (data->change_count >= data->change_capacity) { + for (int i = 0; i < data->change_count; i++) { + if (data->change_list[i].ident == (uintptr_t) fd && data->change_list[i].filter == filter) { + return i; + } + } + return -1; +} + +/* Helper function to grow change list capacity */ +static zend_result grow_change_list(php_poll_ctx *ctx, kqueue_backend_data_t *data) +{ + int new_capacity = data->change_capacity * 2; + struct kevent *new_list + = perealloc(data->change_list, new_capacity * sizeof(struct kevent), ctx->persistent); + if (!new_list) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } + data->change_list = new_list; + data->change_capacity = new_capacity; + return SUCCESS; +} + +static zend_result kqueue_add_change(php_poll_ctx *ctx, kqueue_backend_data_t *data, int fd, + int16_t filter, uint16_t flags, void *udata) +{ + int existing_idx = find_existing_change(data, fd, filter); + + if (existing_idx >= 0) { + /* Update existing change */ + struct kevent *existing = &data->change_list[existing_idx]; + EV_SET(existing, fd, filter, flags, 0, 0, udata); + return SUCCESS; + } + + if (data->change_count >= data->change_capacity) { + zend_result result = grow_change_list(ctx, data); + if (result != SUCCESS) { + return result; + } + } + + /* Set new change */ struct kevent *kev = &data->change_list[data->change_count++]; EV_SET(kev, fd, filter, flags, 0, 0, udata); return SUCCESS; @@ -98,10 +140,10 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events flags |= EV_ONESHOT; } if (events & PHP_POLL_ET) { - flags |= EV_CLEAR; /* kqueue edge-triggering */ + flags |= EV_CLEAR; } - int result = SUCCESS; + zend_result result = SUCCESS; if (events & PHP_POLL_READ) { result = kqueue_add_change(ctx, backend_data, fd, EVFILT_READ, flags, data); @@ -124,17 +166,35 @@ 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; - /* Add delete operations for both read and write filters */ - kqueue_add_change(ctx, backend_data, fd, EVFILT_READ, EV_DELETE, NULL); - kqueue_add_change(ctx, backend_data, fd, EVFILT_WRITE, EV_DELETE, NULL); + /* Delete both read and write filters */ + zend_result result = kqueue_add_change(ctx, backend_data, fd, EVFILT_READ, EV_DELETE, NULL); + if (result != SUCCESS) { + return result; + } + result = kqueue_add_change(ctx, backend_data, fd, EVFILT_WRITE, EV_DELETE, NULL); + if (result != SUCCESS) { + return result; + } return SUCCESS; } static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { - /* For kqueue, we delete and re-add */ - kqueue_backend_remove(ctx, fd); + kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + + /* For epoll-consistent behavior: completely replace the event mask */ + zend_result result = kqueue_add_change(ctx, backend_data, fd, EVFILT_READ, EV_DELETE, NULL); + if (result != SUCCESS) { + return result; + } + + result = kqueue_add_change(ctx, backend_data, fd, EVFILT_WRITE, EV_DELETE, NULL); + if (result != SUCCESS) { + return result; + } + + /* Add back the requested events (coalescing will handle DELETE -> ADD optimization) */ return kqueue_backend_add(ctx, fd, events, data); } @@ -143,6 +203,18 @@ static int kqueue_backend_wait( { kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + if (max_events > backend_data->events_capacity) { + int new_capacity = max_events; + struct kevent *new_events = perealloc( + backend_data->events, new_capacity * sizeof(struct kevent), ctx->persistent); + if (!new_events) { + max_events = backend_data->events_capacity; + } else { + backend_data->events = new_events; + backend_data->events_capacity = new_capacity; + } + } + struct timespec ts = { 0 }, *tsp = NULL; if (timeout >= 0) { ts.tv_sec = timeout / 1000; From 3e1029593ceefe52d0d7f15f5b0ff094bc3aac7e Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 20 Aug 2025 21:15:28 +0200 Subject: [PATCH 18/52] poll: refactore kevent to get rid of long term batching --- main/poll/poll_backend_kqueue.c | 165 ++++++++++++++------------------ 1 file changed, 70 insertions(+), 95 deletions(-) diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index cd1c15144c2f4..f8a4e2d6f7c31 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -22,9 +22,6 @@ typedef struct { int kqueue_fd; struct kevent *events; - struct kevent *change_list; - int change_count; - int change_capacity; int events_capacity; } kqueue_backend_data_t; @@ -43,19 +40,14 @@ static zend_result kqueue_backend_init(php_poll_ctx *ctx, int max_events) return FAILURE; } + /* Use reasonable default if max_events is 0 */ int initial_events = (max_events > 0) ? max_events : 64; - int initial_changes = max_events * 2; data->events = pecalloc(initial_events, sizeof(struct kevent), ctx->persistent); - data->change_list = pecalloc(initial_changes, sizeof(struct kevent), ctx->persistent); data->events_capacity = initial_events; - data->change_capacity = initial_changes; - data->change_count = 0; - if (!data->events || !data->change_list) { + if (!data->events) { close(data->kqueue_fd); - pefree(data->events, ctx->persistent); - pefree(data->change_list, ctx->persistent); pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; @@ -73,68 +65,18 @@ static void kqueue_backend_cleanup(php_poll_ctx *ctx) close(data->kqueue_fd); } pefree(data->events, ctx->persistent); - pefree(data->change_list, ctx->persistent); pefree(data, ctx->persistent); ctx->backend_data = NULL; } } -/* Helper function to find existing change for same (ident, filter) */ -static int find_existing_change(kqueue_backend_data_t *data, int fd, int16_t filter) -{ - for (int i = 0; i < data->change_count; i++) { - if (data->change_list[i].ident == (uintptr_t) fd && data->change_list[i].filter == filter) { - return i; - } - } - return -1; -} - -/* Helper function to grow change list capacity */ -static zend_result grow_change_list(php_poll_ctx *ctx, kqueue_backend_data_t *data) -{ - int new_capacity = data->change_capacity * 2; - struct kevent *new_list - = perealloc(data->change_list, new_capacity * sizeof(struct kevent), ctx->persistent); - if (!new_list) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); - return FAILURE; - } - - data->change_list = new_list; - data->change_capacity = new_capacity; - return SUCCESS; -} - -static zend_result kqueue_add_change(php_poll_ctx *ctx, kqueue_backend_data_t *data, int fd, - int16_t filter, uint16_t flags, void *udata) -{ - int existing_idx = find_existing_change(data, fd, filter); - - if (existing_idx >= 0) { - /* Update existing change */ - struct kevent *existing = &data->change_list[existing_idx]; - EV_SET(existing, fd, filter, flags, 0, 0, udata); - return SUCCESS; - } - - if (data->change_count >= data->change_capacity) { - zend_result result = grow_change_list(ctx, data); - if (result != SUCCESS) { - return result; - } - } - - /* Set new change */ - struct kevent *kev = &data->change_list[data->change_count++]; - EV_SET(kev, fd, filter, flags, 0, 0, udata); - return SUCCESS; -} - static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + struct kevent changes[2]; /* Max 2 changes: read + write */ + int change_count = 0; + uint16_t flags = EV_ADD | EV_ENABLE; if (events & PHP_POLL_ONESHOT) { flags |= EV_ONESHOT; @@ -143,19 +85,21 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events flags |= EV_CLEAR; } - zend_result result = SUCCESS; - if (events & PHP_POLL_READ) { - result = kqueue_add_change(ctx, backend_data, fd, EVFILT_READ, flags, data); - if (result != SUCCESS) { - return result; - } + EV_SET(&changes[change_count], fd, EVFILT_READ, flags, 0, 0, data); + change_count++; } if (events & PHP_POLL_WRITE) { - result = kqueue_add_change(ctx, backend_data, fd, EVFILT_WRITE, flags, data); - if (result != SUCCESS) { - return result; + 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_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; } } @@ -166,15 +110,11 @@ 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; - /* Delete both read and write filters */ - zend_result result = kqueue_add_change(ctx, backend_data, fd, EVFILT_READ, EV_DELETE, NULL); - if (result != SUCCESS) { - return result; - } - result = kqueue_add_change(ctx, backend_data, fd, EVFILT_WRITE, EV_DELETE, NULL); - if (result != SUCCESS) { - return result; - } + struct kevent changes[2]; + EV_SET(&changes[0], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); + EV_SET(&changes[1], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); + + kevent(backend_data->kqueue_fd, changes, 2, NULL, 0, NULL); return SUCCESS; } @@ -183,19 +123,57 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve { kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; - /* For epoll-consistent behavior: completely replace the event mask */ - zend_result result = kqueue_add_change(ctx, backend_data, fd, EVFILT_READ, EV_DELETE, NULL); - if (result != SUCCESS) { - return result; + struct kevent deletes[2]; + struct kevent adds[2]; + int delete_count = 0; + int add_count = 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; } - result = kqueue_add_change(ctx, backend_data, fd, EVFILT_WRITE, EV_DELETE, NULL); - if (result != SUCCESS) { - return result; + /* It needs to reset read and write so it works in the same way as epoll */ + 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++; } - /* Add back the requested events (coalescing will handle DELETE -> ADD optimization) */ - return kqueue_backend_add(ctx, fd, events, data); + /* Prepare add operations */ + 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++; + } + + if (delete_count > 0) { + int result = kevent(backend_data->kqueue_fd, deletes, delete_count, NULL, 0, NULL); + /* ENOENT is not an issue here because we try to delete only if it is there */ + if (result == -1 && errno != ENOENT) { + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; + } + } + + if (add_count > 0) { + int result = kevent(backend_data->kqueue_fd, adds, add_count, NULL, 0, NULL); + if (result == -1) { + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return FAILURE; + } + } + + return SUCCESS; } static int kqueue_backend_wait( @@ -203,6 +181,7 @@ static int kqueue_backend_wait( { kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + /* Grow events array if needed */ if (max_events > backend_data->events_capacity) { int new_capacity = max_events; struct kevent *new_events = perealloc( @@ -222,11 +201,7 @@ static int kqueue_backend_wait( tsp = &ts; } - /* Apply pending changes and wait for events */ - int nfds = kevent(backend_data->kqueue_fd, backend_data->change_list, - backend_data->change_count, backend_data->events, max_events, tsp); - - backend_data->change_count = 0; /* Reset change list */ + int nfds = kevent(backend_data->kqueue_fd, NULL, 0, backend_data->events, max_events, tsp); if (nfds > 0) { for (int i = 0; i < nfds; i++) { From 76845bccac6018daa19ff9de5ccc408a00305d30 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 20 Aug 2025 21:16:09 +0200 Subject: [PATCH 19/52] poll: add modify and write test --- .../stream_poll_basic_sock_modify_write.phpt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt new file mode 100644 index 0000000000000..a3c74ebb7c418 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt @@ -0,0 +1,45 @@ +--TEST-- +Stream polling basic functionality +--SKIPIF-- + +--FILE-- +events . ", Data: " . $event->data . "\n"; + } +} + +// Clean up +fclose($socket1); +fclose($socket2); + +?> +--EXPECT-- +bool(true) +Events count: 1 +Event: bool(true) +Events: 2, Data: modified_data From 5cbfc0ff547228d0a876f40e21fbd182820d234a Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 21 Aug 2025 15:14:10 +0200 Subject: [PATCH 20/52] poll: optimize and refactore all apis to better handle max_events --- ext/standard/basic_functions.stub.php | 4 +- ext/standard/basic_functions_arginfo.h | 4 +- ext/standard/stream_poll.c | 589 +++++++++++++------------ main/php_poll.h | 5 +- main/poll/php_poll_internal.h | 30 +- main/poll/poll_backend_epoll.c | 28 +- main/poll/poll_backend_eventport.c | 233 ++++++++-- main/poll/poll_backend_iocp.c | 266 ++++++++--- main/poll/poll_backend_kqueue.c | 105 +++-- main/poll/poll_backend_poll.c | 328 ++++++++++++-- main/poll/poll_backend_select.c | 284 +++++++++--- main/poll/poll_core.c | 181 ++------ 12 files changed, 1339 insertions(+), 718 deletions(-) diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 3284ba9839587..c2dcee282e5bf 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -3386,7 +3386,7 @@ function soundex(string $string): string {} /* streamsfuncs.c */ -function stream_poll_create(?int $max_events = null, int $backend = STREAM_POLL_BACKEND_AUTO): StreamPollContext {} +function stream_poll_create(int $backend = STREAM_POLL_BACKEND_AUTO): StreamPollContext {} /** @param resource $stream */ function stream_poll_add(StreamPollContext $poll_ctx, $stream, int $events, mixed $data = null): void {} @@ -3397,7 +3397,7 @@ function stream_poll_modify(StreamPollContext $poll_ctx, $stream, int $events, m /** @param resource $stream */ function stream_poll_remove(StreamPollContext $poll_ctx, $stream): void {} -function stream_poll_wait(StreamPollContext $poll_ctx, int $timeout = -1): array {} +function stream_poll_wait(StreamPollContext $poll_ctx, int $timeout = -1, int $max_events = -1): array {} function stream_poll_backend_name(StreamPollContext $poll_ctx): string {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 45fa2fb596833..f7919d52a444d 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: bfee6d05a55da26dfe15a427a76479857723862a */ + * Stub hash: e8c56e78a608a3dbdd13010e6f98f9a24426c2fb */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -1811,7 +1811,6 @@ ZEND_END_ARG_INFO() #define arginfo_soundex arginfo_base64_encode ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_stream_poll_create, 0, 0, StreamPollContext, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, max_events, IS_LONG, 1, "null") ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, backend, IS_LONG, 0, "STREAM_POLL_BACKEND_AUTO") ZEND_END_ARG_INFO() @@ -1832,6 +1831,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_wait, 0, 1, IS_ARRAY, 0) ZEND_ARG_OBJ_INFO(0, poll_ctx, StreamPollContext, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, timeout, IS_LONG, 0, "-1") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, max_events, IS_LONG, 0, "-1") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_backend_name, 0, 1, IS_STRING, 0) diff --git a/ext/standard/stream_poll.c b/ext/standard/stream_poll.c index b045dfbcc1d46..646afd8bb98bf 100644 --- a/ext/standard/stream_poll.c +++ b/ext/standard/stream_poll.c @@ -27,348 +27,360 @@ static zend_object_handlers php_stream_poll_context_object_handlers; /* Internal structure to hold stream data */ typedef struct { - php_stream *stream; - zval data; + php_stream *stream; + zval data; } php_stream_poll_entry; /* Object wrapper for userspace */ typedef struct { - php_poll_ctx *ctx; - HashTable *stream_map; /* Maps fd -> php_stream_poll_entry */ - int max_events; - zend_object std; + php_poll_ctx *ctx; + HashTable *stream_map; /* Maps fd -> php_stream_poll_entry */ + zend_object std; } php_stream_poll_context_object; #define PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZOBJ php_stream_poll_context_object_from_zend_object -#define PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(_zv) php_stream_poll_context_object_from_zend_object(Z_OBJ_P(_zv)) +#define PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(_zv) \ + php_stream_poll_context_object_from_zend_object(Z_OBJ_P(_zv)) -static inline php_stream_poll_context_object *php_stream_poll_context_object_from_zend_object(zend_object *obj) +static inline php_stream_poll_context_object *php_stream_poll_context_object_from_zend_object( + zend_object *obj) { - return (php_stream_poll_context_object *)((char *)(obj) - XtOffsetOf(php_stream_poll_context_object, std)); + return (php_stream_poll_context_object *) ((char *) (obj) -XtOffsetOf( + php_stream_poll_context_object, std)); } static void stream_map_entry_dtor(zval *zv) { - php_stream_poll_entry *entry = Z_PTR_P(zv); - if (entry) { - zval_ptr_dtor(&entry->data); - efree(entry); - } + php_stream_poll_entry *entry = Z_PTR_P(zv); + if (entry) { + zval_ptr_dtor(&entry->data); + efree(entry); + } } static void php_stream_poll_context_free_object_storage(zend_object *obj) { - php_stream_poll_context_object *intern = PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZOBJ(obj); - - if (intern->ctx) { - php_poll_destroy(intern->ctx); - } - if (intern->stream_map) { - zend_hash_destroy(intern->stream_map); - efree(intern->stream_map); - } - zend_object_std_dtor(&intern->std); + php_stream_poll_context_object *intern = PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZOBJ(obj); + + if (intern->ctx) { + php_poll_destroy(intern->ctx); + } + if (intern->stream_map) { + zend_hash_destroy(intern->stream_map); + efree(intern->stream_map); + } + zend_object_std_dtor(&intern->std); } -static inline zend_object *php_stream_poll_context_create_object_ex(zend_class_entry *ce, php_poll_ctx **ctx) +static inline zend_object *php_stream_poll_context_create_object_ex( + zend_class_entry *ce, php_poll_ctx **ctx) { - php_stream_poll_context_object *intern = zend_object_alloc(sizeof(php_stream_poll_context_object), ce); + php_stream_poll_context_object *intern + = zend_object_alloc(sizeof(php_stream_poll_context_object), ce); - zend_object_std_init(&intern->std, ce); - object_properties_init(&intern->std, ce); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); - intern->ctx = NULL; - intern->stream_map = NULL; - intern->max_events = 0; - *ctx = NULL; + intern->ctx = NULL; + intern->stream_map = NULL; + *ctx = NULL; - return &intern->std; + return &intern->std; } static zend_object *php_stream_poll_context_create_object(zend_class_entry *ce) { - php_poll_ctx *ctx; - return php_stream_poll_context_create_object_ex(ce, &ctx); + php_poll_ctx *ctx; + return php_stream_poll_context_create_object_ex(ce, &ctx); } /* Create a new stream polling context */ PHP_FUNCTION(stream_poll_create) { - zend_long max_events_long = 1024, backend_long = PHP_POLL_BACKEND_AUTO; - bool max_events_is_null = 1; - php_poll_ctx *poll_ctx; - - ZEND_PARSE_PARAMETERS_START(0, 2) - Z_PARAM_OPTIONAL - Z_PARAM_LONG_OR_NULL(max_events_long, max_events_is_null) - Z_PARAM_LONG(backend_long) - ZEND_PARSE_PARAMETERS_END(); - - if (!max_events_is_null && max_events_long <= 0) { - zend_throw_exception(stream_poll_exception_class_entry, "max_events must be positive", 0); - RETURN_THROWS(); - } - - int max_events = max_events_is_null ? 1024 : (int)max_events_long; - php_poll_backend_type backend = (php_poll_backend_type)backend_long; - - poll_ctx = php_poll_create(max_events, backend, false); - if (!poll_ctx) { - zend_throw_exception(stream_poll_exception_class_entry, "Failed to create polling context", 0); - RETURN_THROWS(); - } - - if (php_poll_init(poll_ctx) != SUCCESS) { - php_poll_destroy(poll_ctx); - zend_throw_exception(stream_poll_exception_class_entry, "Failed to initialize polling context", 0); - RETURN_THROWS(); - } - - /* Create object */ - object_init_ex(return_value, stream_poll_context_class_entry); - php_stream_poll_context_object *intern = PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(return_value); - - intern->ctx = poll_ctx; - intern->max_events = max_events; - intern->stream_map = emalloc(sizeof(HashTable)); - zend_hash_init(intern->stream_map, 8, NULL, stream_map_entry_dtor, 0); + zend_long backend_long = PHP_POLL_BACKEND_AUTO; + php_poll_ctx *poll_ctx; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(backend_long) + ZEND_PARSE_PARAMETERS_END(); + + php_poll_backend_type backend = (php_poll_backend_type) backend_long; + + poll_ctx = php_poll_create(backend, false); + if (!poll_ctx) { + zend_throw_exception( + stream_poll_exception_class_entry, "Failed to create polling context", 0); + RETURN_THROWS(); + } + + if (php_poll_init(poll_ctx) != SUCCESS) { + php_poll_destroy(poll_ctx); + zend_throw_exception( + stream_poll_exception_class_entry, "Failed to initialize polling context", 0); + RETURN_THROWS(); + } + + /* Create object */ + object_init_ex(return_value, stream_poll_context_class_entry); + php_stream_poll_context_object *intern = PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(return_value); + + intern->ctx = poll_ctx; + intern->stream_map = emalloc(sizeof(HashTable)); + zend_hash_init(intern->stream_map, 8, NULL, stream_map_entry_dtor, 0); } static php_stream_poll_context_object *get_stream_poll_context_object(zval *obj) { - if (Z_TYPE_P(obj) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(obj), stream_poll_context_class_entry)) { - return NULL; - } - return PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(obj); + if (Z_TYPE_P(obj) != IS_OBJECT + || !instanceof_function(Z_OBJCE_P(obj), stream_poll_context_class_entry)) { + return NULL; + } + return PHP_STREAM_POLL_CONTEXT_OBJ_FROM_ZV(obj); } /* Add a stream to the polling context */ PHP_FUNCTION(stream_poll_add) { - zval *zpoll_ctx, *zdata = NULL; - zend_long events; - php_stream *stream; - php_stream_poll_context_object *context; - php_stream_poll_entry *entry; - php_socket_t fd; - - ZEND_PARSE_PARAMETERS_START(3, 4) - Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) - PHP_Z_PARAM_STREAM(stream) - Z_PARAM_LONG(events) - Z_PARAM_OPTIONAL - Z_PARAM_ZVAL(zdata) - ZEND_PARSE_PARAMETERS_END(); - - context = get_stream_poll_context_object(zpoll_ctx); - if (!context || !context->ctx) { - zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); - RETURN_THROWS(); - } - - /* Get file descriptor from stream */ - if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, - (void*)&fd, 1) != SUCCESS || fd == -1) { - zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); - RETURN_THROWS(); - } - - /* Check if already exists */ - if (zend_hash_index_exists(context->stream_map, (zend_ulong)fd)) { - zend_throw_exception(stream_poll_exception_class_entry, "Stream already added", 0); - RETURN_THROWS(); - } - - /* Create entry */ - entry = emalloc(sizeof(php_stream_poll_entry)); - entry->stream = stream; - if (zdata) { - ZVAL_COPY(&entry->data, zdata); - } else { - ZVAL_NULL(&entry->data); - } - - /* Add to internal poll context */ - if (php_poll_add(context->ctx, (int)fd, (uint32_t)events, entry) != SUCCESS) { - zval_ptr_dtor(&entry->data); - efree(entry); - zend_throw_exception(stream_poll_exception_class_entry, "Failed to add stream to poll", 0); - RETURN_THROWS(); - } - - /* Add to our mapping */ - zend_hash_index_add_ptr(context->stream_map, (zend_ulong)fd, entry); + zval *zpoll_ctx, *zdata = NULL; + zend_long events; + php_stream *stream; + php_stream_poll_context_object *context; + php_stream_poll_entry *entry; + php_socket_t fd; + + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + PHP_Z_PARAM_STREAM(stream) + Z_PARAM_LONG(events) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(zdata) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + /* Get file descriptor from stream */ + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + (void *) &fd, + 1) != SUCCESS + || fd == -1) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); + RETURN_THROWS(); + } + + /* Check if already exists */ + if (zend_hash_index_exists(context->stream_map, (zend_ulong) fd)) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream already added", 0); + RETURN_THROWS(); + } + + /* Create entry */ + entry = emalloc(sizeof(php_stream_poll_entry)); + entry->stream = stream; + if (zdata) { + ZVAL_COPY(&entry->data, zdata); + } else { + ZVAL_NULL(&entry->data); + } + + /* Add to internal poll context */ + if (php_poll_add(context->ctx, (int) fd, (uint32_t) events, entry) != SUCCESS) { + zval_ptr_dtor(&entry->data); + efree(entry); + zend_throw_exception(stream_poll_exception_class_entry, "Failed to add stream to poll", 0); + RETURN_THROWS(); + } + + /* Add to our mapping */ + zend_hash_index_add_ptr(context->stream_map, (zend_ulong) fd, entry); } /* Modify events for a stream in the polling context */ PHP_FUNCTION(stream_poll_modify) { - zval *zpoll_ctx, *zdata = NULL; - zend_long events; - php_stream *stream; - php_stream_poll_context_object *context; - php_stream_poll_entry *entry; - php_socket_t fd; - - ZEND_PARSE_PARAMETERS_START(3, 4) - Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) - PHP_Z_PARAM_STREAM(stream) - Z_PARAM_LONG(events) - Z_PARAM_OPTIONAL - Z_PARAM_ZVAL(zdata) - ZEND_PARSE_PARAMETERS_END(); - - context = get_stream_poll_context_object(zpoll_ctx); - if (!context || !context->ctx) { - zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); - RETURN_THROWS(); - } - - /* Get file descriptor from stream */ - if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, - (void*)&fd, 1) != SUCCESS || fd == -1) { - zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); - RETURN_THROWS(); - } - - /* Find existing entry */ - entry = zend_hash_index_find_ptr(context->stream_map, (zend_ulong)fd); - if (!entry) { - zend_throw_exception(stream_poll_exception_class_entry, "Stream not found", 0); - RETURN_THROWS(); - } - - /* Update data if provided */ - if (zdata) { - zval_ptr_dtor(&entry->data); - ZVAL_COPY(&entry->data, zdata); - } - - /* Modify in poll context */ - if (php_poll_modify(context->ctx, (int)fd, (uint32_t)events, entry) != SUCCESS) { - zend_throw_exception(stream_poll_exception_class_entry, "Failed to modify stream in poll", 0); - RETURN_THROWS(); - } + zval *zpoll_ctx, *zdata = NULL; + zend_long events; + php_stream *stream; + php_stream_poll_context_object *context; + php_stream_poll_entry *entry; + php_socket_t fd; + + ZEND_PARSE_PARAMETERS_START(3, 4) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + PHP_Z_PARAM_STREAM(stream) + Z_PARAM_LONG(events) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(zdata) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + /* Get file descriptor from stream */ + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + (void *) &fd, + 1) != SUCCESS + || fd == -1) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); + RETURN_THROWS(); + } + + /* Find existing entry */ + entry = zend_hash_index_find_ptr(context->stream_map, (zend_ulong) fd); + if (!entry) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream not found", 0); + RETURN_THROWS(); + } + + /* Update data if provided */ + if (zdata) { + zval_ptr_dtor(&entry->data); + ZVAL_COPY(&entry->data, zdata); + } + + /* Modify in poll context */ + if (php_poll_modify(context->ctx, (int) fd, (uint32_t) events, entry) != SUCCESS) { + zend_throw_exception( + stream_poll_exception_class_entry, "Failed to modify stream in poll", 0); + RETURN_THROWS(); + } } /* Remove a stream from the polling context */ PHP_FUNCTION(stream_poll_remove) { - zval *zpoll_ctx; - php_stream *stream; - php_stream_poll_context_object *context; - php_socket_t fd; - - ZEND_PARSE_PARAMETERS_START(2, 2) - Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) - PHP_Z_PARAM_STREAM(stream) - ZEND_PARSE_PARAMETERS_END(); - - context = get_stream_poll_context_object(zpoll_ctx); - if (!context || !context->ctx) { - zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); - RETURN_THROWS(); - } - - /* Get file descriptor from stream */ - if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, - (void*)&fd, 1) != SUCCESS || fd == -1) { - zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); - RETURN_THROWS(); - } - - /* Remove from poll context */ - if (php_poll_remove(context->ctx, (int)fd) != SUCCESS) { - zend_throw_exception(stream_poll_exception_class_entry, "Failed to remove stream from poll", 0); - RETURN_THROWS(); - } - - /* Remove from our mapping */ - zend_hash_index_del(context->stream_map, (zend_ulong)fd); + zval *zpoll_ctx; + php_stream *stream; + php_stream_poll_context_object *context; + php_socket_t fd; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + PHP_Z_PARAM_STREAM(stream) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + /* Get file descriptor from stream */ + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + (void *) &fd, + 1) != SUCCESS + || fd == -1) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream cannot be polled", 0); + RETURN_THROWS(); + } + + /* Remove from poll context */ + if (php_poll_remove(context->ctx, (int) fd) != SUCCESS) { + zend_throw_exception( + stream_poll_exception_class_entry, "Failed to remove stream from poll", 0); + RETURN_THROWS(); + } + + /* Remove from our mapping */ + zend_hash_index_del(context->stream_map, (zend_ulong) fd); } /* Wait for events on streams in the polling context */ PHP_FUNCTION(stream_poll_wait) { - zval *zpoll_ctx; - zend_long timeout = -1; - php_stream_poll_context_object *context; - php_poll_event *events; - int num_events, i; - - ZEND_PARSE_PARAMETERS_START(1, 2) - Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) - Z_PARAM_OPTIONAL - Z_PARAM_LONG(timeout) - ZEND_PARSE_PARAMETERS_END(); - - context = get_stream_poll_context_object(zpoll_ctx); - if (!context || !context->ctx) { - zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); - RETURN_THROWS(); - } - - events = emalloc(sizeof(php_poll_event) * context->max_events); - - num_events = php_poll_wait(context->ctx, events, context->max_events, (int)timeout); - - if (num_events < 0) { - efree(events); - zend_throw_exception(stream_poll_exception_class_entry, "Poll wait failed", 0); - RETURN_THROWS(); - } - - array_init(return_value); - - for (i = 0; i < num_events; i++) { - php_stream_poll_entry *entry = (php_stream_poll_entry*)events[i].data; - zval event_obj; - - object_init_ex(&event_obj, stream_poll_event_class_entry); - - /* Set stream property */ - zval stream_zval; - php_stream_to_zval(entry->stream, &stream_zval); - zend_update_property(stream_poll_event_class_entry, Z_OBJ(event_obj), - ZEND_STRL("stream"), &stream_zval); - - /* Set events property */ - zend_update_property_long(stream_poll_event_class_entry, Z_OBJ(event_obj), - ZEND_STRL("events"), events[i].revents); - - /* Set data property */ - zend_update_property(stream_poll_event_class_entry, Z_OBJ(event_obj), - ZEND_STRL("data"), &entry->data); - - add_next_index_zval(return_value, &event_obj); - } - - efree(events); + zval *zpoll_ctx; + zend_long timeout = -1; + zend_long max_events = -1; + php_stream_poll_context_object *context; + php_poll_event *events; + int num_events, i; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(timeout) + Z_PARAM_LONG(max_events) + ZEND_PARSE_PARAMETERS_END(); + + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } + + if (max_events <= 0) { + // TODO: get some recommended value from polling api basend on number of added events + max_events = 1024; + } + events = emalloc(sizeof(php_poll_event) * max_events); + + num_events = php_poll_wait(context->ctx, events, max_events, (int) timeout); + + if (num_events < 0) { + efree(events); + zend_throw_exception(stream_poll_exception_class_entry, "Poll wait failed", 0); + RETURN_THROWS(); + } + + array_init(return_value); + + for (i = 0; i < num_events; i++) { + php_stream_poll_entry *entry = (php_stream_poll_entry *) events[i].data; + zval event_obj; + + object_init_ex(&event_obj, stream_poll_event_class_entry); + + /* Set stream property */ + zval stream_zval; + php_stream_to_zval(entry->stream, &stream_zval); + zend_update_property( + stream_poll_event_class_entry, Z_OBJ(event_obj), ZEND_STRL("stream"), &stream_zval); + + /* Set events property */ + zend_update_property_long(stream_poll_event_class_entry, Z_OBJ(event_obj), + ZEND_STRL("events"), events[i].revents); + + /* Set data property */ + zend_update_property( + stream_poll_event_class_entry, Z_OBJ(event_obj), ZEND_STRL("data"), &entry->data); + + add_next_index_zval(return_value, &event_obj); + } + + efree(events); } /* Get the backend name for the polling context */ PHP_FUNCTION(stream_poll_backend_name) { - zval *zpoll_ctx; - php_stream_poll_context_object *context; + zval *zpoll_ctx; + php_stream_poll_context_object *context; - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) - ZEND_PARSE_PARAMETERS_END(); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(zpoll_ctx, stream_poll_context_class_entry) + ZEND_PARSE_PARAMETERS_END(); - context = get_stream_poll_context_object(zpoll_ctx); - if (!context || !context->ctx) { - zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); - RETURN_THROWS(); - } + context = get_stream_poll_context_object(zpoll_ctx); + if (!context || !context->ctx) { + zend_throw_exception(stream_poll_exception_class_entry, "Invalid polling context", 0); + RETURN_THROWS(); + } - const char *backend_name = php_poll_backend_name(context->ctx); - RETURN_STRING(backend_name); + const char *backend_name = php_poll_backend_name(context->ctx); + RETURN_STRING(backend_name); } PHP_METHOD(StreamPollContext, __construct) { - zend_throw_error(NULL, "Cannot directly construct StreamPollContext, use stream_poll_create() instead"); + zend_throw_error( + NULL, "Cannot directly construct StreamPollContext, use stream_poll_create() instead"); } /* Initialize the stream poll classes - add to PHP_MINIT_FUNCTION */ @@ -377,21 +389,24 @@ PHP_MINIT_FUNCTION(stream_poll) /* Register symbols */ register_stream_poll_symbols(module_number); - /* Register classes */ - stream_poll_context_class_entry = register_class_StreamPollContext(); - stream_poll_context_class_entry->create_object = php_stream_poll_context_create_object; - stream_poll_context_class_entry->default_object_handlers = &php_stream_poll_context_object_handlers; + /* Register classes */ + stream_poll_context_class_entry = register_class_StreamPollContext(); + stream_poll_context_class_entry->create_object = php_stream_poll_context_create_object; + stream_poll_context_class_entry->default_object_handlers + = &php_stream_poll_context_object_handlers; - stream_poll_event_class_entry = register_class_StreamPollEvent(); - stream_poll_exception_class_entry = register_class_StreamPollException(zend_ce_exception); + stream_poll_event_class_entry = register_class_StreamPollEvent(); + stream_poll_exception_class_entry = register_class_StreamPollException(zend_ce_exception); - /* Set up object handlers */ - memcpy(&php_stream_poll_context_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); - php_stream_poll_context_object_handlers.offset = XtOffsetOf(php_stream_poll_context_object, std); - php_stream_poll_context_object_handlers.free_obj = php_stream_poll_context_free_object_storage; + /* Set up object handlers */ + memcpy(&php_stream_poll_context_object_handlers, &std_object_handlers, + sizeof(zend_object_handlers)); + php_stream_poll_context_object_handlers.offset + = XtOffsetOf(php_stream_poll_context_object, std); + php_stream_poll_context_object_handlers.free_obj = php_stream_poll_context_free_object_storage; - /* Register poll backends */ - php_poll_register_backends(); + /* Register poll backends */ + php_poll_register_backends(); - return SUCCESS; + return SUCCESS; } diff --git a/main/php_poll.h b/main/php_poll.h index 4bcd26a622e50..b371ca04c2979 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -62,14 +62,13 @@ struct php_poll_event { /* Forward declarations */ typedef struct php_poll_ctx php_poll_ctx; -typedef struct php_poll_fd_entry php_poll_fd_entry; typedef struct php_poll_backend_ops php_poll_backend_ops; typedef struct php_poll_event php_poll_event; /* Public API */ -PHPAPI php_poll_ctx *php_poll_create( - int max_events, php_poll_backend_type preferred_backend, bool persistent); +PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, bool persistent); +PHPAPI zend_result php_poll_set_max_events_hint(php_poll_ctx *ctx, int max_events); PHPAPI zend_result php_poll_init(php_poll_ctx *ctx); PHPAPI void php_poll_destroy(php_poll_ctx *ctx); diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h index 9ff58d85a8447..eb5b7598fbc20 100644 --- a/main/poll/php_poll_internal.h +++ b/main/poll/php_poll_internal.h @@ -12,17 +12,10 @@ +----------------------------------------------------------------------+ */ -#include "php_poll.h" +#ifndef PHP_POLL_INTERNAL_H +#define PHP_POLL_INTERNAL_H -/* FD entry for tracking state */ -struct php_poll_fd_entry { - int fd; - uint32_t events; - uint32_t last_revents; /* For edge-trigger simulation */ - void *data; - bool active; - bool et_armed; /* Edge-trigger state */ -}; +#include "php_poll.h" /* Backend interface */ typedef struct php_poll_backend_ops { @@ -30,7 +23,7 @@ typedef struct php_poll_backend_ops { const char *name; /* Initialize backend */ - zend_result (*init)(php_poll_ctx *ctx, int max_events); + zend_result (*init)(php_poll_ctx *ctx); /* Cleanup backend */ void (*cleanup)(php_poll_ctx *ctx); @@ -59,16 +52,11 @@ struct php_poll_ctx { const php_poll_backend_ops *backend_ops; php_poll_backend_type backend_type; - int max_events; - int num_fds; bool initialized; bool persistent; - /* Whether to simulate edge triggering */ - bool simulate_et; - /* FD tracking for edge-trigger simulation */ - php_poll_fd_entry *fd_entries; - int fd_entries_size; + /* Optional capacity hint for backends */ + int max_events_hint; /* Last error */ php_poll_error last_error; @@ -77,11 +65,9 @@ struct php_poll_ctx { void *backend_data; }; - +/* Internal functions */ const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backend); -php_poll_fd_entry *php_poll_find_fd_entry(php_poll_ctx *ctx, int fd); - static inline void php_poll_set_error(php_poll_ctx *ctx, php_poll_error error) { ctx->last_error = error; @@ -93,3 +79,5 @@ static inline void php_poll_set_system_error_if_not_set(php_poll_ctx *ctx) ctx->last_error = PHP_POLL_ERR_SYSTEM; } } + +#endif /* PHP_POLL_INTERNAL_H */ diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c index bce3e502b3710..11408d58c6da4 100644 --- a/main/poll/poll_backend_epoll.c +++ b/main/poll/poll_backend_epoll.c @@ -21,6 +21,7 @@ typedef struct { int epoll_fd; struct epoll_event *events; + int events_capacity; } epoll_backend_data_t; static uint32_t epoll_events_to_native(uint32_t events) @@ -71,7 +72,7 @@ static uint32_t epoll_events_from_native(uint32_t native) return events; } -static zend_result epoll_backend_init(php_poll_ctx *ctx, int max_events) +static zend_result epoll_backend_init(php_poll_ctx *ctx) { epoll_backend_data_t *data = pecalloc(1, sizeof(epoll_backend_data_t), ctx->persistent); if (!data) { @@ -82,17 +83,20 @@ static zend_result epoll_backend_init(php_poll_ctx *ctx, int max_events) data->epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (data->epoll_fd == -1) { pefree(data, ctx->persistent); - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); return FAILURE; } - data->events = pecalloc(max_events, sizeof(struct epoll_event), ctx->persistent); + /* 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 = pecalloc(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; @@ -111,7 +115,7 @@ static void epoll_backend_cleanup(php_poll_ctx *ctx) } } -static int epoll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +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; @@ -127,7 +131,7 @@ static int epoll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *d return SUCCESS; } -static int epoll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +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; @@ -143,7 +147,7 @@ static int epoll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void return SUCCESS; } -static int epoll_backend_remove(php_poll_ctx *ctx, int fd) +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; @@ -160,6 +164,18 @@ static int epoll_backend_wait( { 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 = perealloc( + backend_data->events, max_events * sizeof(struct epoll_event), ctx->persistent); + if (!new_events) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return -1; + } + backend_data->events = new_events; + backend_data->events_capacity = max_events; + } + int nfds = epoll_wait(backend_data->epoll_fd, backend_data->events, max_events, timeout); if (nfds > 0) { diff --git a/main/poll/poll_backend_eventport.c b/main/poll/poll_backend_eventport.c index c5293bb058998..9ff2fc8a44965 100644 --- a/main/poll/poll_backend_eventport.c +++ b/main/poll/poll_backend_eventport.c @@ -22,11 +22,23 @@ #include #include +typedef struct { + int fd; + uint32_t events; + void *data; + bool active; +} eventport_fd_entry; + typedef struct { int port_fd; port_event_t *events; - int max_events; + int events_capacity; int active_associations; + + /* FD tracking for re-association */ + eventport_fd_entry *fd_entries; + int fd_entries_capacity; + int fd_count; } eventport_backend_data_t; /* Convert our event flags to event port flags */ @@ -48,7 +60,6 @@ static int eventport_events_to_native(uint32_t events) if (events & PHP_POLL_RDHUP) { native |= POLLHUP; /* Map RDHUP to HUP */ } - /* Event ports provide edge-triggered semantics by default */ return native; } @@ -74,8 +85,70 @@ static uint32_t eventport_events_from_native(int native) return events; } +/* Find FD entry */ +static eventport_fd_entry *eventport_find_fd_entry(eventport_backend_data_t *data, int fd) +{ + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (data->fd_entries[i].active && data->fd_entries[i].fd == fd) { + return &data->fd_entries[i]; + } + } + return NULL; +} + +/* Get or create FD entry */ +static eventport_fd_entry *eventport_get_fd_entry( + eventport_backend_data_t *data, int fd, bool persistent) +{ + eventport_fd_entry *entry = eventport_find_fd_entry(data, fd); + if (entry) { + return entry; + } + + /* Find empty slot */ + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (!data->fd_entries[i].active) { + data->fd_entries[i].fd = fd; + data->fd_entries[i].active = true; + data->fd_count++; + return &data->fd_entries[i]; + } + } + + int new_capacity = data->fd_entries_capacity ? data->fd_entries_capacity * 2 : 64; + eventport_fd_entry *new_entries + = perealloc(data->fd_entries, new_capacity * sizeof(eventport_fd_entry), persistent); + if (!new_entries) { + return NULL; + } + + memset(new_entries + data->fd_entries_capacity, 0, + (new_capacity - data->fd_entries_capacity) * sizeof(eventport_fd_entry)); + + data->fd_entries = new_entries; + + /* Use first new slot */ + eventport_fd_entry *new_entry = &data->fd_entries[data->fd_entries_capacity]; + new_entry->fd = fd; + new_entry->active = true; + data->fd_count++; + + data->fd_entries_capacity = new_capacity; + return new_entry; +} + +/* Remove FD entry */ +static void eventport_remove_fd_entry(eventport_backend_data_t *data, int fd) +{ + eventport_fd_entry *entry = eventport_find_fd_entry(data, fd); + if (entry) { + entry->active = false; + data->fd_count--; + } +} + /* Initialize event port backend */ -static zend_result eventport_backend_init(php_poll_ctx_t *ctx, int max_events) +static zend_result eventport_backend_init(php_poll_ctx *ctx) { eventport_backend_data_t *data = pecalloc(1, sizeof(eventport_backend_data_t), ctx->persistent); if (!data) { @@ -87,28 +160,41 @@ static zend_result eventport_backend_init(php_poll_ctx_t *ctx, int max_events) data->port_fd = port_create(); if (data->port_fd == -1) { pefree(data, ctx->persistent); - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); return FAILURE; } - data->max_events = max_events; data->active_associations = 0; + data->fd_count = 0; - /* Allocate event array for port_getn() */ - data->events = pecalloc(max_events, sizeof(port_event_t), ctx->persistent); + /* 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 = pecalloc(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 array */ + data->fd_entries = pecalloc(initial_capacity, sizeof(eventport_fd_entry), ctx->persistent); + if (!data->fd_entries) { + close(data->port_fd); + pefree(data->events, ctx->persistent); + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + data->fd_entries_capacity = initial_capacity; ctx->backend_data = data; return SUCCESS; } /* Cleanup event port backend */ -static void eventport_backend_cleanup(php_poll_ctx_t *ctx) +static void eventport_backend_cleanup(php_poll_ctx *ctx) { eventport_backend_data_t *data = (eventport_backend_data_t *) ctx->backend_data; if (data) { @@ -116,6 +202,7 @@ static void eventport_backend_cleanup(php_poll_ctx_t *ctx) close(data->port_fd); } pefree(data->events, ctx->persistent); + pefree(data->fd_entries, ctx->persistent); pefree(data, ctx->persistent); ctx->backend_data = NULL; } @@ -123,14 +210,31 @@ static void eventport_backend_cleanup(php_poll_ctx_t *ctx) /* Add file descriptor to event port */ static zend_result eventport_backend_add( - php_poll_ctx_t *ctx, int fd, uint32_t events, void *user_data) + 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; + /* Check if FD already exists */ + if (eventport_find_fd_entry(backend_data, fd)) { + php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); + return FAILURE; + } + + /* Get FD entry for tracking */ + eventport_fd_entry *entry = eventport_get_fd_entry(backend_data, fd, ctx->persistent); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + entry->events = events; + entry->data = user_data; + int native_events = eventport_events_to_native(events); /* Associate file descriptor with event port */ if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, user_data) == -1) { + eventport_remove_fd_entry(backend_data, fd); switch (errno) { case EEXIST: php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); @@ -155,10 +259,21 @@ static zend_result eventport_backend_add( /* Modify file descriptor in event port */ static zend_result eventport_backend_modify( - php_poll_ctx_t *ctx, int fd, uint32_t events, void *user_data) + 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; + /* Find existing entry */ + eventport_fd_entry *entry = eventport_find_fd_entry(backend_data, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + /* Update entry */ + entry->events = events; + entry->data = user_data; + /* For event ports, we need to dissociate and re-associate */ /* Note: dissociate might fail if the fd was already fired and auto-dissociated */ port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd); @@ -184,33 +299,41 @@ static zend_result eventport_backend_modify( } /* Remove file descriptor from event port */ -static zend_result eventport_backend_remove(php_poll_ctx_t *ctx, int fd) +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; + /* Find existing entry */ + eventport_fd_entry *entry = eventport_find_fd_entry(backend_data, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + if (port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd) == -1) { - switch (errno) { - case ENOENT: - php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); - break; - case EBADF: - case EINVAL: - php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); - break; - default: - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); - break; + /* Only fail if it's not ENOENT (might already be dissociated) */ + if (errno != ENOENT) { + switch (errno) { + case EBADF: + case EINVAL: + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; + default: + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; + } + return FAILURE; } - return FAILURE; } + eventport_remove_fd_entry(backend_data, fd); backend_data->active_associations--; return SUCCESS; } /* Wait for events using event port */ static int eventport_backend_wait( - php_poll_ctx_t *ctx, php_poll_event *events, int max_events, int timeout) + php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; @@ -225,6 +348,18 @@ static int eventport_backend_wait( return 0; } + /* Ensure we have enough space for the requested events */ + if (max_events > backend_data->events_capacity) { + port_event_t *new_events = perealloc( + backend_data->events, max_events * sizeof(port_event_t), ctx->persistent); + if (!new_events) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return -1; + } + backend_data->events = new_events; + backend_data->events_capacity = max_events; + } + /* Setup timeout structure */ struct timespec ts = { 0 }, *tsp = NULL; if (timeout >= 0) { @@ -235,8 +370,7 @@ static int eventport_backend_wait( /* 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, - MIN(max_events, backend_data->max_events), &nget, tsp); + int result = port_getn(backend_data->port_fd, backend_data->events, max_events, &nget, tsp); if (result == -1) { if (errno == ETIME) { @@ -253,33 +387,50 @@ static int eventport_backend_wait( int nfds = (int) nget; - /* Process the events */ + /* Process the raw events first */ 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) { - events[i].fd = (int) port_event->portev_object; + int fd = (int) port_event->portev_object; + events[i].fd = fd; events[i].events = 0; /* Not used in results */ events[i].revents = eventport_events_from_native(port_event->portev_events); events[i].data = port_event->portev_user; - /* Event ports automatically dissociate after firing, so we need to - re-associate if this is not a oneshot event and we want level-triggered behavior */ - php_poll_fd_entry_t *entry = php_poll_find_fd_entry(ctx, events[i].fd); + /* Event ports are naturally level-triggered but auto-dissociate after firing. + * Re-associate unless it's oneshot */ + eventport_fd_entry *entry = eventport_find_fd_entry(backend_data, fd); if (entry && !(entry->events & PHP_POLL_ONESHOT)) { - /* Re-associate for continued monitoring */ - int native_events = eventport_events_to_native(entry->events); - if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, events[i].fd, - native_events, entry->data) - == 0) { - /* Re-association successful */ + /* For level-triggered: re-associate with all events + * For edge-triggered: re-associate with events that didn't fire */ + uint32_t reassoc_events = entry->events; + if (entry->events & PHP_POLL_ET) { + /* Edge-triggered: don't re-associate with events that just fired */ + reassoc_events &= ~events[i].revents; + reassoc_events &= ~PHP_POLL_ET; /* Remove ET flag for port_associate */ + } + + if (reassoc_events != 0) { + int native_events = eventport_events_to_native(reassoc_events); + if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, + entry->data) + != 0) { + /* Re-association failed might be due to fd being closed */ + eventport_remove_fd_entry(backend_data, fd); + backend_data->active_associations--; + } } else { - /* Re-association failed - might be due to fd being closed */ + /* No events to re-associate with */ + eventport_remove_fd_entry(backend_data, fd); backend_data->active_associations--; } } else { - /* Oneshot event or entry not found - reduce association count */ + /* Oneshot or entry not found - remove from tracking */ + if (entry) { + eventport_remove_fd_entry(backend_data, fd); + } backend_data->active_associations--; } } else { @@ -306,7 +457,7 @@ static bool eventport_backend_is_available(void) } /* Event port backend operations structure */ -const php_poll_backend_ops_t php_poll_backend_eventport_ops = { +const php_poll_backend_ops php_poll_backend_eventport_ops = { .type = PHP_POLL_BACKEND_EVENTPORT, .name = "eventport", .init = eventport_backend_init, @@ -316,7 +467,7 @@ const php_poll_backend_ops_t php_poll_backend_eventport_ops = { .remove = eventport_backend_remove, .wait = eventport_backend_wait, .is_available = eventport_backend_is_available, - .supports_et = true /* Event ports provide edge-triggered semantics by default */ + .supports_et = true /* Supports both level and edge triggering */ }; #endif /* HAVE_EVENT_PORTS */ diff --git a/main/poll/poll_backend_iocp.c b/main/poll/poll_backend_iocp.c index 640d8c7a26880..f68520f380918 100644 --- a/main/poll/poll_backend_iocp.c +++ b/main/poll/poll_backend_iocp.c @@ -25,20 +25,101 @@ typedef struct iocp_operation { int fd; uint32_t events; void *user_data; + bool active; char buffer[1]; /* Minimal buffer for accept/recv operations */ } iocp_operation_t; +typedef struct { + int fd; + uint32_t events; + void *data; + bool active; + bool associated; /* Whether socket is associated with IOCP */ +} iocp_fd_entry; + typedef struct { HANDLE iocp_handle; iocp_operation_t *operations; - int max_operations; + int operations_capacity; int operation_count; + + /* FD tracking */ + iocp_fd_entry *fd_entries; + int fd_entries_capacity; + int fd_count; + LPFN_ACCEPTEX AcceptEx; LPFN_CONNECTEX ConnectEx; LPFN_GETACCEPTEXSOCKADDRS GetAcceptExSockaddrs; } iocp_backend_data_t; -static zend_result iocp_backend_init(php_poll_ctx *ctx, int max_events) +/* Find FD entry */ +static iocp_fd_entry *iocp_find_fd_entry(iocp_backend_data_t *data, int fd) +{ + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (data->fd_entries[i].active && data->fd_entries[i].fd == fd) { + return &data->fd_entries[i]; + } + } + return NULL; +} + +/* Get or create FD entry */ +static iocp_fd_entry *iocp_get_fd_entry(iocp_backend_data_t *data, int fd, bool persistent) +{ + iocp_fd_entry *entry = iocp_find_fd_entry(data, fd); + if (entry) { + return entry; + } + + /* Find empty slot */ + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (!data->fd_entries[i].active) { + data->fd_entries[i].fd = fd; + data->fd_entries[i].active = true; + data->fd_entries[i].associated = false; + data->fd_count++; + return &data->fd_entries[i]; + } + } + + /* Need to grow the array */ + int new_capacity = data->fd_entries_capacity ? data->fd_entries_capacity * 2 : 64; + iocp_fd_entry *new_entries + = perealloc(data->fd_entries, new_capacity * sizeof(iocp_fd_entry), persistent); + if (!new_entries) { + return NULL; + } + + /* Initialize new entries */ + memset(new_entries + data->fd_entries_capacity, 0, + (new_capacity - data->fd_entries_capacity) * sizeof(iocp_fd_entry)); + + data->fd_entries = new_entries; + + /* Use first new slot */ + iocp_fd_entry *new_entry = &data->fd_entries[data->fd_entries_capacity]; + new_entry->fd = fd; + new_entry->active = true; + new_entry->associated = false; + data->fd_count++; + + data->fd_entries_capacity = new_capacity; + return new_entry; +} + +/* Remove FD entry */ +static void iocp_remove_fd_entry(iocp_backend_data_t *data, int fd) +{ + iocp_fd_entry *entry = iocp_find_fd_entry(data, fd); + if (entry) { + entry->active = false; + entry->associated = false; + data->fd_count--; + } +} + +static zend_result iocp_backend_init(php_poll_ctx *ctx) { iocp_backend_data_t *data = pecalloc(1, sizeof(iocp_backend_data_t), ctx->persistent); if (!data) { @@ -54,15 +135,30 @@ static zend_result iocp_backend_init(php_poll_ctx *ctx, int max_events) return FAILURE; } - data->max_operations = max_events; - data->operations = pecalloc(max_events, sizeof(iocp_operation_t), ctx->persistent); + /* Use hint for initial allocation if provided, otherwise start with reasonable default */ + int initial_capacity = ctx->max_events_hint > 0 ? ctx->max_events_hint : 64; + data->operations = pecalloc(initial_capacity, sizeof(iocp_operation_t), ctx->persistent); if (!data->operations) { CloseHandle(data->iocp_handle); pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } + data->operations_capacity = initial_capacity; + data->operation_count = 0; + + /* Initialize FD tracking array */ + data->fd_entries = pecalloc(initial_capacity, sizeof(iocp_fd_entry), ctx->persistent); + if (!data->fd_entries) { + CloseHandle(data->iocp_handle); + pefree(data->operations, ctx->persistent); + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + data->fd_entries_capacity = initial_capacity; + data->fd_count = 0; /* Load Winsock extension functions */ SOCKET dummy_socket = socket(AF_INET, SOCK_STREAM, 0); @@ -86,7 +182,6 @@ static zend_result iocp_backend_init(php_poll_ctx *ctx, int max_events) closesocket(dummy_socket); } - data->operation_count = 0; ctx->backend_data = data; return SUCCESS; } @@ -99,6 +194,7 @@ static void iocp_backend_cleanup(php_poll_ctx *ctx) CloseHandle(data->iocp_handle); } pefree(data->operations, ctx->persistent); + pefree(data->fd_entries, ctx->persistent); pefree(data, ctx->persistent); ctx->backend_data = NULL; } @@ -109,49 +205,60 @@ static zend_result iocp_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; SOCKET sock = (SOCKET) fd; - if (backend_data->operation_count >= backend_data->max_operations) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + /* Check if FD already exists */ + if (iocp_find_fd_entry(backend_data, fd)) { + php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); return FAILURE; } - /* Associate socket with completion port */ - HANDLE result - = CreateIoCompletionPort((HANDLE) sock, backend_data->iocp_handle, (ULONG_PTR) sock, 0); - if (result == NULL) { - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + /* Get FD entry for tracking */ + iocp_fd_entry *entry = iocp_get_fd_entry(backend_data, fd, ctx->persistent); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } - /* Set up operation structure */ - iocp_operation_t *op = &backend_data->operations[backend_data->operation_count++]; - memset(op, 0, sizeof(iocp_operation_t)); - op->fd = fd; - op->events = events; - op->user_data = data; - - /* For read events, post a recv operation */ - if (events & PHP_POLL_READ) { - WSABUF wsabuf = { 1, op->buffer }; - DWORD flags = 0; - DWORD bytes_received; + entry->events = events; + entry->data = data; - int result = WSARecv(sock, &wsabuf, 1, &bytes_received, &flags, &op->overlapped, NULL); - - if (result == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) { - backend_data->operation_count--; + /* Associate socket with completion port if not already done */ + if (!entry->associated) { + HANDLE result = CreateIoCompletionPort( + (HANDLE) sock, backend_data->iocp_handle, (ULONG_PTR) sock, 0); + if (result == NULL) { + iocp_remove_fd_entry(backend_data, fd); php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); return FAILURE; } + entry->associated = true; } + /* Note: IOCP operations are typically initiated on-demand rather than pre-posted. + * For a polling API, we would need to simulate readiness using techniques like: + * - Zero-byte reads to check readability + * - Connect() to localhost to check writability + * This is complex and IOCP is better suited for async I/O rather than polling. + * For now, we just track the association. */ + return SUCCESS; } static zend_result iocp_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { - /* For IOCP, we need to cancel existing operations and re-add */ - iocp_backend_remove(ctx, fd); - return iocp_backend_add(ctx, fd, events, data); + iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + + /* Find existing entry */ + iocp_fd_entry *entry = iocp_find_fd_entry(backend_data, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + /* Update entry */ + entry->events = events; + entry->data = data; + + return SUCCESS; } static zend_result iocp_backend_remove(php_poll_ctx *ctx, int fd) @@ -159,20 +266,18 @@ static zend_result iocp_backend_remove(php_poll_ctx *ctx, int fd) iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; SOCKET sock = (SOCKET) fd; + /* Find existing entry */ + iocp_fd_entry *entry = iocp_find_fd_entry(backend_data, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + /* Cancel all I/O operations on this socket */ CancelIo((HANDLE) sock); - /* Remove from our operation list */ - for (int i = 0; i < backend_data->operation_count; i++) { - if (backend_data->operations[i].fd == fd) { - /* Shift remaining operations */ - for (int j = i; j < backend_data->operation_count - 1; j++) { - backend_data->operations[j] = backend_data->operations[j + 1]; - } - backend_data->operation_count--; - break; - } - } + /* Remove from tracking */ + iocp_remove_fd_entry(backend_data, fd); return SUCCESS; } @@ -181,6 +286,24 @@ static int iocp_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ { iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + /* IOCP is fundamentally different from other polling mechanisms. + * It's completion-based rather than readiness-based. + * A proper implementation would need to: + * 1. Post overlapped I/O operations for all registered FDs + * 2. Wait for completions + * 3. Return the completed operations as events + * + * This is complex and doesn't map well to a traditional polling API. + * For now, we provide a minimal implementation that can detect some completions. */ + + if (backend_data->fd_count == 0) { + /* No FDs to monitor, but respect timeout */ + if (timeout > 0) { + Sleep(timeout); + } + return 0; + } + DWORD bytes_transferred; ULONG_PTR completion_key; LPOVERLAPPED overlapped; @@ -190,30 +313,38 @@ static int iocp_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ if (!result && overlapped == NULL) { /* Timeout or error */ - return (GetLastError() == WAIT_TIMEOUT) ? 0 : -1; + DWORD error = GetLastError(); + if (error == WAIT_TIMEOUT) { + return 0; + } else { + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + return -1; + } } if (overlapped != NULL) { - /* Find the operation that completed */ - iocp_operation_t *op = CONTAINING_RECORD(overlapped, iocp_operation_t, overlapped); - - events[0].fd = op->fd; - events[0].events = op->events; - events[0].data = op->user_data; - - if (result) { - /* Successful completion */ - if (op->events & PHP_POLL_READ) { - events[0].revents = PHP_POLL_READ; - } else if (op->events & PHP_POLL_WRITE) { - events[0].revents = PHP_POLL_WRITE; + /* We got a completion, but since we're not posting operations in add(), + * this would only happen if the application is doing its own overlapped I/O. + * Try to match it to one of our tracked FDs. */ + + SOCKET completed_socket = (SOCKET) completion_key; + iocp_fd_entry *entry = iocp_find_fd_entry(backend_data, (int) completed_socket); + + if (entry && max_events > 0) { + events[0].fd = entry->fd; + events[0].events = entry->events; + events[0].data = entry->data; + + if (result) { + /* Successful completion - determine event type */ + events[0].revents = entry->events & (PHP_POLL_READ | PHP_POLL_WRITE); + } else { + /* Error completion */ + events[0].revents = PHP_POLL_ERROR; } - } else { - /* Error completion */ - events[0].revents = PHP_POLL_ERROR; - } - return 1; + return 1; + } } return 0; @@ -221,15 +352,8 @@ static int iocp_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ static bool iocp_backend_is_available(void) { - /* IOCP is available on Windows NT and later */ - OSVERSIONINFO osvi = { 0 }; - osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); - - if (GetVersionEx(&osvi)) { - return (osvi.dwPlatformId == VER_PLATFORM_WIN32_NT); - } - - return false; + /* IOCP is available on Windows NT and later (basically all modern Windows) */ + return true; } const php_poll_backend_ops php_poll_backend_iocp_ops = { @@ -242,7 +366,7 @@ const php_poll_backend_ops php_poll_backend_iocp_ops = { .remove = iocp_backend_remove, .wait = iocp_backend_wait, .is_available = iocp_backend_is_available, - .supports_et = true /* IOCP provides completion-based model which is edge-triggered */ + .supports_et = true /* IOCP provides completion-based model which is naturally edge-triggered */ }; #endif /* _WIN32 */ diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index f8a4e2d6f7c31..434f6855acbd9 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -19,13 +19,14 @@ #include #include #include + typedef struct { int kqueue_fd; struct kevent *events; int events_capacity; } kqueue_backend_data_t; -static zend_result kqueue_backend_init(php_poll_ctx *ctx, int max_events) +static zend_result kqueue_backend_init(php_poll_ctx *ctx) { kqueue_backend_data_t *data = pecalloc(1, sizeof(kqueue_backend_data_t), ctx->persistent); if (!data) { @@ -40,18 +41,16 @@ static zend_result kqueue_backend_init(php_poll_ctx *ctx, int max_events) return FAILURE; } - /* Use reasonable default if max_events is 0 */ - int initial_events = (max_events > 0) ? max_events : 64; - - data->events = pecalloc(initial_events, sizeof(struct kevent), ctx->persistent); - data->events_capacity = initial_events; - + /* 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 = pecalloc(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; ctx->backend_data = data; return SUCCESS; @@ -98,7 +97,21 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events if (change_count > 0) { int result = kevent(backend_data->kqueue_fd, changes, change_count, NULL, 0, NULL); if (result == -1) { - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + switch (errno) { + case EEXIST: + php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); + break; + case ENOMEM: + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + break; + case EBADF: + case EINVAL: + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; + default: + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; + } return FAILURE; } } @@ -106,19 +119,6 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events 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 changes[2]; - EV_SET(&changes[0], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); - EV_SET(&changes[1], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); - - kevent(backend_data->kqueue_fd, changes, 2, NULL, 0, NULL); - - 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; @@ -136,7 +136,7 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve add_flags |= EV_CLEAR; } - /* It needs to reset read and write so it works in the same way as epoll */ + /* 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++; @@ -146,7 +146,7 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve delete_count++; } - /* Prepare add operations */ + /* 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++; @@ -156,19 +156,34 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve add_count++; } + /* Delete existing filters (ignore ENOENT errors) */ if (delete_count > 0) { int result = kevent(backend_data->kqueue_fd, deletes, delete_count, NULL, 0, NULL); - /* ENOENT is not an issue here because we try to delete only if it is there */ if (result == -1 && errno != ENOENT) { php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); return FAILURE; } } + /* 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_error(ctx, PHP_POLL_ERR_SYSTEM); + switch (errno) { + case ENOENT: + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + break; + case ENOMEM: + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + break; + case EBADF: + case EINVAL: + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; + default: + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; + } return FAILURE; } } @@ -176,22 +191,46 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve 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 changes[2]; + EV_SET(&changes[0], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); + EV_SET(&changes[1], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); + + int result = kevent(backend_data->kqueue_fd, changes, 2, NULL, 0, NULL); + if (result == -1 && errno != ENOENT) { + switch (errno) { + case EBADF: + case EINVAL: + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; + default: + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; + } + return FAILURE; + } + + return SUCCESS; +} + static int kqueue_backend_wait( php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; - /* Grow events array if needed */ + /* Ensure we have enough space for the requested events */ if (max_events > backend_data->events_capacity) { - int new_capacity = max_events; struct kevent *new_events = perealloc( - backend_data->events, new_capacity * sizeof(struct kevent), ctx->persistent); + backend_data->events, max_events * sizeof(struct kevent), ctx->persistent); if (!new_events) { - max_events = backend_data->events_capacity; - } else { - backend_data->events = new_events; - backend_data->events_capacity = new_capacity; + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return -1; } + backend_data->events = new_events; + backend_data->events_capacity = max_events; } struct timespec ts = { 0 }, *tsp = NULL; @@ -206,7 +245,7 @@ static int kqueue_backend_wait( if (nfds > 0) { for (int i = 0; i < nfds; i++) { events[i].fd = (int) backend_data->events[i].ident; - events[i].events = 0; + events[i].events = 0; /* Not used in results */ events[i].data = backend_data->events[i].udata; events[i].revents = 0; diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 88a32817dd82c..68666d7895a60 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -20,10 +20,23 @@ #include #include +typedef struct { + int fd; + uint32_t events; + void *data; + bool active; + uint32_t last_revents; /* For edge-trigger simulation */ +} poll_fd_entry; + typedef struct { struct pollfd *fds; - int allocated; - int used; + int fds_capacity; + int fds_used; + + /* FD tracking for user data and edge-trigger simulation */ + poll_fd_entry *fd_entries; + int fd_entries_capacity; + int fd_count; } poll_backend_data_t; static uint32_t poll_events_to_native(uint32_t events) @@ -62,7 +75,94 @@ static uint32_t poll_events_from_native(uint32_t native) return events; } -static zend_result poll_backend_init(php_poll_ctx *ctx, int max_events) +/* Find FD entry */ +static poll_fd_entry *poll_find_fd_entry(poll_backend_data_t *data, int fd) +{ + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (data->fd_entries[i].active && data->fd_entries[i].fd == fd) { + return &data->fd_entries[i]; + } + } + return NULL; +} + +/* Get or create FD entry */ +static poll_fd_entry *poll_get_fd_entry(poll_backend_data_t *data, int fd, bool persistent) +{ + poll_fd_entry *entry = poll_find_fd_entry(data, fd); + if (entry) { + return entry; + } + + /* Find empty slot */ + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (!data->fd_entries[i].active) { + data->fd_entries[i].fd = fd; + data->fd_entries[i].active = true; + data->fd_entries[i].last_revents = 0; + data->fd_count++; + return &data->fd_entries[i]; + } + } + + /* Need to grow the array */ + int new_capacity = data->fd_entries_capacity ? data->fd_entries_capacity * 2 : 64; + poll_fd_entry *new_entries + = perealloc(data->fd_entries, new_capacity * sizeof(poll_fd_entry), persistent); + if (!new_entries) { + return NULL; + } + + /* Initialize new entries */ + memset(new_entries + data->fd_entries_capacity, 0, + (new_capacity - data->fd_entries_capacity) * sizeof(poll_fd_entry)); + + data->fd_entries = new_entries; + + /* Use first new slot */ + poll_fd_entry *new_entry = &data->fd_entries[data->fd_entries_capacity]; + new_entry->fd = fd; + new_entry->active = true; + new_entry->last_revents = 0; + data->fd_count++; + + data->fd_entries_capacity = new_capacity; + return new_entry; +} + +/* Remove FD entry */ +static void poll_remove_fd_entry(poll_backend_data_t *data, int fd) +{ + poll_fd_entry *entry = poll_find_fd_entry(data, fd); + if (entry) { + entry->active = false; + data->fd_count--; + } +} + +/* Find pollfd slot */ +static struct pollfd *poll_find_pollfd_slot(poll_backend_data_t *data, int fd) +{ + for (int i = 0; i < data->fds_capacity; i++) { + if (data->fds[i].fd == fd) { + return &data->fds[i]; + } + } + return NULL; +} + +/* Get empty pollfd slot */ +static struct pollfd *poll_get_empty_pollfd_slot(poll_backend_data_t *data) +{ + for (int i = 0; i < data->fds_capacity; i++) { + if (data->fds[i].fd == -1) { + return &data->fds[i]; + } + } + return NULL; +} + +static zend_result poll_backend_init(php_poll_ctx *ctx) { poll_backend_data_t *data = pecalloc(1, sizeof(poll_backend_data_t), ctx->persistent); if (!data) { @@ -70,18 +170,31 @@ static zend_result poll_backend_init(php_poll_ctx *ctx, int max_events) return FAILURE; } - data->fds = pecalloc(max_events, sizeof(struct pollfd), ctx->persistent); + /* 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->fds = pecalloc(initial_capacity, sizeof(struct pollfd), ctx->persistent); if (!data->fds) { pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } + data->fds_capacity = initial_capacity; + data->fds_used = 0; - data->allocated = max_events; - data->used = 0; + /* Initialize FD tracking array */ + data->fd_entries = pecalloc(initial_capacity, sizeof(poll_fd_entry), ctx->persistent); + if (!data->fd_entries) { + pefree(data->fds, ctx->persistent); + pefree(data, ctx->persistent); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + data->fd_entries_capacity = initial_capacity; + data->fd_count = 0; /* Initialize all fds to -1 */ - for (int i = 0; i < max_events; i++) { + for (int i = 0; i < initial_capacity; i++) { data->fds[i].fd = -1; } @@ -94,89 +207,210 @@ static void poll_backend_cleanup(php_poll_ctx *ctx) poll_backend_data_t *data = (poll_backend_data_t *) ctx->backend_data; if (data) { pefree(data->fds, ctx->persistent); + pefree(data->fd_entries, 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 *data) +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; - /* Find empty slot */ - for (int i = 0; i < backend_data->allocated; i++) { - if (backend_data->fds[i].fd == -1) { - backend_data->fds[i].fd = fd; - backend_data->fds[i].events = poll_events_to_native(events); - backend_data->fds[i].revents = 0; - backend_data->used++; - return SUCCESS; + /* Check if FD already exists */ + if (poll_find_fd_entry(backend_data, fd)) { + php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); + return FAILURE; + } + + /* Get FD entry for tracking */ + poll_fd_entry *entry = poll_get_fd_entry(backend_data, fd, ctx->persistent); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + entry->events = events; + entry->data = user_data; + + /* Find empty pollfd slot */ + struct pollfd *pfd = poll_get_empty_pollfd_slot(backend_data); + if (!pfd) { + /* Need to grow the pollfd array */ + int new_capacity = backend_data->fds_capacity * 2; + struct pollfd *new_fds = perealloc( + backend_data->fds, new_capacity * sizeof(struct pollfd), ctx->persistent); + if (!new_fds) { + poll_remove_fd_entry(backend_data, fd); + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; } + + /* Initialize new slots */ + for (int i = backend_data->fds_capacity; i < new_capacity; i++) { + new_fds[i].fd = -1; + } + + backend_data->fds = new_fds; + pfd = &backend_data->fds[backend_data->fds_capacity]; + backend_data->fds_capacity = new_capacity; } - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); - return FAILURE; + /* Set up pollfd */ + pfd->fd = fd; + pfd->events = poll_events_to_native(events & ~(PHP_POLL_ET | PHP_POLL_ONESHOT)); + pfd->revents = 0; + backend_data->fds_used++; + + return SUCCESS; } -static zend_result poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +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; - for (int i = 0; i < backend_data->allocated; i++) { - if (backend_data->fds[i].fd == fd) { - backend_data->fds[i].events = poll_events_to_native(events); - return SUCCESS; - } + /* Find existing entry */ + poll_fd_entry *entry = poll_find_fd_entry(backend_data, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + /* Update entry */ + entry->events = events; + entry->data = user_data; + entry->last_revents = 0; /* Reset on modify */ + + /* Find pollfd and update */ + struct pollfd *pfd = poll_find_pollfd_slot(backend_data, fd); + if (pfd) { + pfd->events = poll_events_to_native(events & ~(PHP_POLL_ET | PHP_POLL_ONESHOT)); } - php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); - return FAILURE; + 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; - for (int i = 0; i < backend_data->allocated; i++) { - if (backend_data->fds[i].fd == fd) { - backend_data->fds[i].fd = -1; - backend_data->fds[i].events = 0; - backend_data->fds[i].revents = 0; - backend_data->used--; - return SUCCESS; + /* Find existing entry */ + poll_fd_entry *entry = poll_find_fd_entry(backend_data, fd); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; + } + + /* Find and clear pollfd */ + struct pollfd *pfd = poll_find_pollfd_slot(backend_data, fd); + if (pfd) { + pfd->fd = -1; + pfd->events = 0; + pfd->revents = 0; + backend_data->fds_used--; + } + + /* Remove from tracking */ + poll_remove_fd_entry(backend_data, fd); + + return SUCCESS; +} + +/* Edge-trigger simulation */ +static int poll_simulate_edge_trigger( + poll_backend_data_t *backend_data, php_poll_event *events, int nfds) +{ + int filtered_count = 0; + + for (int i = 0; i < nfds; i++) { + poll_fd_entry *entry = poll_find_fd_entry(backend_data, events[i].fd); + if (!entry) { + continue; + } + + uint32_t new_events = events[i].revents; + uint32_t reported_events = 0; + + if (entry->events & PHP_POLL_ET) { + /* Edge-triggered: report edges only */ + if ((new_events & PHP_POLL_READ) && !(entry->last_revents & PHP_POLL_READ)) { + reported_events |= PHP_POLL_READ; + } + if ((new_events & PHP_POLL_WRITE) && !(entry->last_revents & PHP_POLL_WRITE)) { + reported_events |= PHP_POLL_WRITE; + } + /* Always report error and hangup events */ + reported_events |= (new_events & (PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP)); + } else { + /* Level-triggered: report all active events */ + reported_events = new_events; + } + + entry->last_revents = new_events; + + /* Only include this event if we have something to report */ + if (reported_events != 0) { + if (filtered_count != i) { + events[filtered_count] = events[i]; + } + events[filtered_count].revents = reported_events; + filtered_count++; + + /* Handle oneshot: remove after first event */ + if (entry->events & PHP_POLL_ONESHOT) { + /* Mark pollfd as disabled */ + struct pollfd *pfd = poll_find_pollfd_slot(backend_data, events[i].fd); + if (pfd) { + pfd->fd = -1; + pfd->events = 0; + pfd->revents = 0; + backend_data->fds_used--; + } + poll_remove_fd_entry(backend_data, events[i].fd); + } } } - php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); - return FAILURE; + return filtered_count; } static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; - int nfds = poll(backend_data->fds, backend_data->allocated, timeout); + if (backend_data->fds_used == 0) { + /* No FDs to monitor, but respect timeout */ + if (timeout > 0) { + struct timespec ts; + ts.tv_sec = timeout / 1000; + ts.tv_nsec = (timeout % 1000) * 1000000; + nanosleep(&ts, NULL); + } + return 0; + } + + int nfds = poll(backend_data->fds, backend_data->fds_capacity, timeout); if (nfds > 0) { int event_count = 0; - for (int i = 0; i < backend_data->allocated && event_count < max_events; i++) { + for (int i = 0; i < backend_data->fds_capacity && event_count < max_events; i++) { if (backend_data->fds[i].fd != -1 && backend_data->fds[i].revents != 0) { - events[event_count].fd = backend_data->fds[i].fd; - events[event_count].events = backend_data->fds[i].events; - events[event_count].revents = poll_events_from_native(backend_data->fds[i].revents); - events[event_count].data = NULL; /* poll doesn't support user data directly */ - - /* Find the user data from our FD tracking */ - php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, events[event_count].fd); + poll_fd_entry *entry = poll_find_fd_entry(backend_data, backend_data->fds[i].fd); if (entry) { + events[event_count].fd = backend_data->fds[i].fd; + events[event_count].events = entry->events; + events[event_count].revents + = poll_events_from_native(backend_data->fds[i].revents); events[event_count].data = entry->data; + event_count++; } backend_data->fds[i].revents = 0; /* Clear for next poll */ - event_count++; } } - nfds = event_count; + + /* Apply edge-trigger simulation */ + nfds = poll_simulate_edge_trigger(backend_data, events, event_count); } return nfds; @@ -197,7 +431,7 @@ const php_poll_backend_ops php_poll_backend_poll_ops = { .remove = poll_backend_remove, .wait = poll_backend_wait, .is_available = poll_backend_is_available, - .supports_et = false, + .supports_et = false, /* poll() doesn't support ET natively, but we simulate it */ }; #endif /* HAVE_POLL */ diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c index d43dd7b376813..2e02a44c7c11c 100644 --- a/main/poll/poll_backend_select.c +++ b/main/poll/poll_backend_select.c @@ -15,16 +15,104 @@ #include "php_poll_internal.h" #include "php_network.h" +typedef struct { + php_socket_t fd; + uint32_t events; + void *data; + bool active; + uint32_t last_revents; /* For edge-trigger simulation */ +} select_fd_entry; + typedef struct { fd_set read_fds, write_fds, error_fds; fd_set master_read_fds, master_write_fds, master_error_fds; - php_socket_t *socket_list; - void **data_list; - int socket_count; - int max_sockets; + + /* FD tracking for user data and edge-trigger simulation */ + select_fd_entry *fd_entries; + int fd_entries_capacity; + int fd_count; + + php_socket_t max_fd; /* Highest fd for select() */ } select_backend_data_t; -static zend_result select_backend_init(php_poll_ctx *ctx, int max_events) +/* Find FD entry */ +static select_fd_entry *select_find_fd_entry(select_backend_data_t *data, php_socket_t fd) +{ + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (data->fd_entries[i].active && data->fd_entries[i].fd == fd) { + return &data->fd_entries[i]; + } + } + return NULL; +} + +/* Get or create FD entry */ +static select_fd_entry *select_get_fd_entry( + select_backend_data_t *data, php_socket_t fd, bool persistent) +{ + select_fd_entry *entry = select_find_fd_entry(data, fd); + if (entry) { + return entry; + } + + /* Find empty slot */ + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (!data->fd_entries[i].active) { + data->fd_entries[i].fd = fd; + data->fd_entries[i].active = true; + data->fd_entries[i].last_revents = 0; + data->fd_count++; + return &data->fd_entries[i]; + } + } + + /* Need to grow the array */ + int new_capacity = data->fd_entries_capacity ? data->fd_entries_capacity * 2 : 64; + select_fd_entry *new_entries + = perealloc(data->fd_entries, new_capacity * sizeof(select_fd_entry), persistent); + if (!new_entries) { + return NULL; + } + + /* Initialize new entries */ + memset(new_entries + data->fd_entries_capacity, 0, + (new_capacity - data->fd_entries_capacity) * sizeof(select_fd_entry)); + + data->fd_entries = new_entries; + + /* Use first new slot */ + select_fd_entry *new_entry = &data->fd_entries[data->fd_entries_capacity]; + new_entry->fd = fd; + new_entry->active = true; + new_entry->last_revents = 0; + data->fd_count++; + + data->fd_entries_capacity = new_capacity; + return new_entry; +} + +/* Remove FD entry */ +static void select_remove_fd_entry(select_backend_data_t *data, php_socket_t fd) +{ + select_fd_entry *entry = select_find_fd_entry(data, fd); + if (entry) { + entry->active = false; + data->fd_count--; + } +} + +/* Update max_fd for select() */ +static void select_update_max_fd(select_backend_data_t *data) +{ + data->max_fd = 0; + for (int i = 0; i < data->fd_entries_capacity; i++) { + if (data->fd_entries[i].active && data->fd_entries[i].fd > data->max_fd) { + data->max_fd = data->fd_entries[i].fd; + } + } +} + +static zend_result select_backend_init(php_poll_ctx *ctx) { select_backend_data_t *data = pecalloc(1, sizeof(select_backend_data_t), ctx->persistent); if (!data) { @@ -32,22 +120,22 @@ static zend_result select_backend_init(php_poll_ctx *ctx, int max_events) return FAILURE; } - data->max_sockets = max_events; - data->socket_list = pecalloc(max_events, sizeof(php_socket_t), ctx->persistent); - data->data_list = pecalloc(max_events, sizeof(void *), ctx->persistent); + /* 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; - if (!data->socket_list || !data->data_list) { - pefree(data->socket_list, ctx->persistent); - pefree(data->data_list, ctx->persistent); + data->fd_entries = pecalloc(initial_capacity, sizeof(select_fd_entry), ctx->persistent); + if (!data->fd_entries) { pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } + data->fd_entries_capacity = initial_capacity; + data->fd_count = 0; FD_ZERO(&data->master_read_fds); FD_ZERO(&data->master_write_fds); FD_ZERO(&data->master_error_fds); - data->socket_count = 0; + data->max_fd = 0; ctx->backend_data = data; return SUCCESS; @@ -57,43 +145,39 @@ static void select_backend_cleanup(php_poll_ctx *ctx) { select_backend_data_t *data = (select_backend_data_t *) ctx->backend_data; if (data) { - pefree(data->socket_list, ctx->persistent); - pefree(data->data_list, ctx->persistent); + pefree(data->fd_entries, ctx->persistent); pefree(data, ctx->persistent); ctx->backend_data = NULL; } } -static int select_find_socket_index(select_backend_data_t *data, php_socket_t sock) -{ - for (int i = 0; i < data->socket_count; i++) { - if (data->socket_list[i] == sock) { - return i; - } - } - return -1; -} - -static zend_result select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +static zend_result select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *user_data) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; - if (backend_data->socket_count >= backend_data->max_sockets) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); +#ifdef FD_SETSIZE + if (sock >= FD_SETSIZE) { + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); return FAILURE; } +#endif - /* Check if socket already exists */ - if (select_find_socket_index(backend_data, sock) >= 0) { + /* Check if FD already exists */ + if (select_find_fd_entry(backend_data, sock)) { php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); return FAILURE; } - /* Add socket to our tracking */ - int index = backend_data->socket_count++; - backend_data->socket_list[index] = sock; - backend_data->data_list[index] = data; + /* Get FD entry for tracking */ + select_fd_entry *entry = select_get_fd_entry(backend_data, sock, ctx->persistent); + if (!entry) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); + return FAILURE; + } + + entry->events = events; + entry->data = user_data; /* Add to appropriate fd_sets */ if (events & PHP_POLL_READ) { @@ -105,22 +189,31 @@ static zend_result select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events /* Always monitor for errors */ FD_SET(sock, &backend_data->master_error_fds); + /* Update max_fd */ + if (sock > backend_data->max_fd) { + backend_data->max_fd = sock; + } + return SUCCESS; } -static zend_result select_backend_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) +static zend_result select_backend_modify( + php_poll_ctx *ctx, int fd, uint32_t events, void *user_data) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; - int index = select_find_socket_index(backend_data, sock); - if (index < 0) { + /* Find existing entry */ + select_fd_entry *entry = select_find_fd_entry(backend_data, sock); + if (!entry) { php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); return FAILURE; } - /* Update user data */ - backend_data->data_list[index] = data; + /* Update entry */ + entry->events = events; + entry->data = user_data; + entry->last_revents = 0; /* Reset on modify */ /* Remove from all sets first */ FD_CLR(sock, &backend_data->master_read_fds); @@ -144,8 +237,9 @@ static zend_result select_backend_remove(php_poll_ctx *ctx, int fd) select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; - int index = select_find_socket_index(backend_data, sock); - if (index < 0) { + /* Find existing entry */ + select_fd_entry *entry = select_find_fd_entry(backend_data, sock); + if (!entry) { php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); return FAILURE; } @@ -155,25 +249,96 @@ static zend_result select_backend_remove(php_poll_ctx *ctx, int fd) FD_CLR(sock, &backend_data->master_write_fds); FD_CLR(sock, &backend_data->master_error_fds); - /* Remove from socket list by shifting elements */ - for (int i = index; i < backend_data->socket_count - 1; i++) { - backend_data->socket_list[i] = backend_data->socket_list[i + 1]; - backend_data->data_list[i] = backend_data->data_list[i + 1]; + /* Remove from tracking */ + select_remove_fd_entry(backend_data, sock); + + /* Update max_fd if this was the highest */ + if (sock == backend_data->max_fd) { + select_update_max_fd(backend_data); } - backend_data->socket_count--; return SUCCESS; } +/* Edge-trigger simulation */ +static int select_simulate_edge_trigger( + select_backend_data_t *backend_data, php_poll_event *events, int nfds) +{ + int filtered_count = 0; + + for (int i = 0; i < nfds; i++) { + select_fd_entry *entry = select_find_fd_entry(backend_data, (php_socket_t) events[i].fd); + if (!entry) { + continue; + } + + uint32_t new_events = events[i].revents; + uint32_t reported_events = 0; + + if (entry->events & PHP_POLL_ET) { + /* Edge-triggered: report edges only */ + if ((new_events & PHP_POLL_READ) && !(entry->last_revents & PHP_POLL_READ)) { + reported_events |= PHP_POLL_READ; + } + if ((new_events & PHP_POLL_WRITE) && !(entry->last_revents & PHP_POLL_WRITE)) { + reported_events |= PHP_POLL_WRITE; + } + /* Always report error and hangup events */ + reported_events |= (new_events & (PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP)); + } else { + /* Level-triggered: report all active events */ + reported_events = new_events; + } + + entry->last_revents = new_events; + + /* Only include this event if we have something to report */ + if (reported_events != 0) { + if (filtered_count != i) { + events[filtered_count] = events[i]; + } + events[filtered_count].revents = reported_events; + filtered_count++; + + /* Handle oneshot: remove after first event */ + if (entry->events & PHP_POLL_ONESHOT) { + php_socket_t sock = (php_socket_t) events[i].fd; + + /* Remove from fd_sets */ + FD_CLR(sock, &backend_data->master_read_fds); + FD_CLR(sock, &backend_data->master_write_fds); + FD_CLR(sock, &backend_data->master_error_fds); + + /* Remove from tracking */ + select_remove_fd_entry(backend_data, sock); + + /* Update max_fd if needed */ + if (sock == backend_data->max_fd) { + select_update_max_fd(backend_data); + } + } + } + } + + return filtered_count; +} + static int select_backend_wait( php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; - if (backend_data->socket_count == 0) { - /* No sockets to wait for */ + if (backend_data->fd_count == 0) { + /* No sockets to wait for, but respect timeout */ if (timeout > 0) { - php_sleep(timeout); +#ifdef _WIN32 + Sleep(timeout); +#else + struct timespec ts; + ts.tv_sec = timeout / 1000; + ts.tv_nsec = (timeout % 1000) * 1000000; + nanosleep(&ts, NULL); +#endif } return 0; } @@ -192,8 +357,8 @@ static int select_backend_wait( } /* Call select() */ - int result = select( - 0, &backend_data->read_fds, &backend_data->write_fds, &backend_data->error_fds, ptv); + int result = select((int) (backend_data->max_fd + 1), &backend_data->read_fds, + &backend_data->write_fds, &backend_data->error_fds, ptv); if (result <= 0) { return (result == 0) ? 0 : -1; @@ -201,8 +366,12 @@ static int select_backend_wait( /* Process results */ int event_count = 0; - for (int i = 0; i < backend_data->socket_count && event_count < max_events; i++) { - php_socket_t sock = backend_data->socket_list[i]; + for (int i = 0; i < backend_data->fd_entries_capacity && event_count < max_events; i++) { + if (!backend_data->fd_entries[i].active) { + continue; + } + + php_socket_t sock = backend_data->fd_entries[i].fd; uint32_t revents = 0; if (FD_ISSET(sock, &backend_data->read_fds)) { @@ -217,14 +386,17 @@ static int select_backend_wait( if (revents != 0) { events[event_count].fd = (int) sock; - events[event_count].events = 0; /* Not used in results */ + events[event_count].events = backend_data->fd_entries[i].events; events[event_count].revents = revents; - events[event_count].data = backend_data->data_list[i]; + events[event_count].data = backend_data->fd_entries[i].data; event_count++; } } - return event_count; + /* Apply edge-trigger simulation */ + int nfds = select_simulate_edge_trigger(backend_data, events, event_count); + + return nfds; } static bool select_backend_is_available(void) @@ -242,5 +414,5 @@ const php_poll_backend_ops php_poll_backend_select_ops = { .remove = select_backend_remove, .wait = select_backend_wait, .is_available = select_backend_is_available, - .supports_et = false /* select() doesn't support edge triggering */ + .supports_et = false /* select() doesn't support ET natively, but we simulate it */ }; diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 7b72ccd1ccaf5..8e19af94a4f25 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -96,91 +96,9 @@ const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backe return NULL; } -/* Find FD entry */ -php_poll_fd_entry *php_poll_find_fd_entry(php_poll_ctx *ctx, int fd) -{ - for (int i = 0; i < ctx->fd_entries_size; i++) { - if (ctx->fd_entries[i].active && ctx->fd_entries[i].fd == fd) { - return &ctx->fd_entries[i]; - } - } - return NULL; -} - -/* Get or create FD entry */ -static php_poll_fd_entry *php_poll_get_fd_entry(php_poll_ctx *ctx, int fd) -{ - php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); - if (entry) { - return entry; - } - - /* Find empty slot */ - for (int i = 0; i < ctx->fd_entries_size; i++) { - if (!ctx->fd_entries[i].active) { - ctx->fd_entries[i].fd = fd; - ctx->fd_entries[i].active = true; - ctx->fd_entries[i].last_revents = 0; - ctx->fd_entries[i].et_armed = true; - return &ctx->fd_entries[i]; - } - } - - return NULL; -} - -/* Edge-trigger simulation */ -static int php_poll_simulate_et(php_poll_ctx *ctx, php_poll_event *events, int nfds) -{ - if (!ctx->simulate_et) { - return nfds; /* No simulation needed */ - } - - int filtered_count = 0; - - for (int i = 0; i < nfds; i++) { - php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, events[i].fd); - if (!entry) { - continue; - } - - uint32_t new_events = events[i].revents; - uint32_t edge_events = 0; - - /* Detect edges for each event type */ - if ((new_events & PHP_POLL_READ) && !(entry->last_revents & PHP_POLL_READ)) { - edge_events |= PHP_POLL_READ; - } - if ((new_events & PHP_POLL_WRITE) && !(entry->last_revents & PHP_POLL_WRITE)) { - edge_events |= PHP_POLL_WRITE; - } - - /* Always report error and hangup events */ - edge_events |= (new_events & (PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP)); - - entry->last_revents = new_events; - - /* Only include this event if we detected an edge or it's an error */ - if (edge_events != 0) { - if (filtered_count != i) { - events[filtered_count] = events[i]; - } - events[filtered_count].revents = edge_events; - filtered_count++; - } - } - - return filtered_count; -} - /* Create new poll context */ -PHPAPI php_poll_ctx *php_poll_create( - int max_events, php_poll_backend_type preferred_backend, bool persistent) +PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, bool persistent) { - if (max_events <= 0) { - return NULL; - } - php_poll_ctx *ctx = pecalloc(1, sizeof(php_poll_ctx), persistent); if (!ctx) { return NULL; @@ -194,30 +112,42 @@ PHPAPI php_poll_ctx *php_poll_create( return NULL; } - ctx->max_events = max_events; ctx->backend_type = preferred_backend; - ctx->simulate_et = !ctx->backend_ops->supports_et; + ctx->max_events_hint = 0; /* No hint by default */ - /* Allocate FD entries for edge-trigger simulation */ - ctx->fd_entries = pecalloc(max_events, sizeof(php_poll_fd_entry), persistent); - ctx->fd_entries_size = max_events; - if (!ctx->fd_entries) { - pefree(ctx, persistent); - return NULL; + 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; } - return ctx; + 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 */ - if (EXPECTED(ctx->backend_ops->init(ctx, ctx->max_events) == SUCCESS)) { + /* Initialize backend - can use ctx->max_events_hint if helpful */ + if (EXPECTED(ctx->backend_ops->init(ctx) == SUCCESS)) { ctx->initialized = true; return SUCCESS; } @@ -237,7 +167,6 @@ PHPAPI void php_poll_destroy(php_poll_ctx *ctx) ctx->backend_ops->cleanup(ctx); } - pefree(ctx->fd_entries, ctx->persistent); pefree(ctx, ctx->persistent); } @@ -249,35 +178,12 @@ PHPAPI zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void return FAILURE; } - if (UNEXPECTED(ctx->num_fds >= ctx->max_events)) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); - return FAILURE; - } - - /* Get FD entry for tracking */ - php_poll_fd_entry *entry = php_poll_get_fd_entry(ctx, fd); - if (!entry) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); - return FAILURE; - } - - entry->events = events; - entry->data = data; - - /* If simulating edge triggering, convert ET events to level-triggered */ - uint32_t backend_events = events; - if (ctx->simulate_et && (events & PHP_POLL_ET)) { - backend_events &= ~PHP_POLL_ET; /* Remove ET flag for backend */ - } - - if (EXPECTED(ctx->backend_ops->add(ctx, fd, backend_events, data) == SUCCESS)) { - ctx->num_fds++; + /* Delegate to backend - it handles all validation and tracking */ + if (EXPECTED(ctx->backend_ops->add(ctx, fd, events, data) == SUCCESS)) { return SUCCESS; } - entry->active = false; /* Rollback */ php_poll_set_system_error_if_not_set(ctx); - return FAILURE; } @@ -289,22 +195,8 @@ PHPAPI zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, v return FAILURE; } - php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); - if (UNEXPECTED(!entry)) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); - return FAILURE; - } - - entry->events = events; - entry->data = data; - entry->et_armed = true; /* Re-arm edge triggering */ - - uint32_t backend_events = events; - if (ctx->simulate_et && (events & PHP_POLL_ET)) { - backend_events &= ~PHP_POLL_ET; - } - - if (EXPECTED(ctx->backend_ops->modify(ctx, fd, backend_events, data) == SUCCESS)) { + /* Delegate to backend - it handles validation */ + if (EXPECTED(ctx->backend_ops->modify(ctx, fd, events, data) == SUCCESS)) { return SUCCESS; } @@ -320,15 +212,9 @@ PHPAPI zend_result php_poll_remove(php_poll_ctx *ctx, int fd) return FAILURE; } - php_poll_fd_entry *entry = php_poll_find_fd_entry(ctx, fd); - if (UNEXPECTED(!entry)) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); - return FAILURE; - } - + /* Delegate to backend - it handles validation */ if (EXPECTED(ctx->backend_ops->remove(ctx, fd) == SUCCESS)) { - entry->active = false; - ctx->num_fds--; + return SUCCESS; } php_poll_set_system_error_if_not_set(ctx); @@ -343,12 +229,9 @@ PHPAPI int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_even return -1; } + /* Delegate to backend - it handles everything including ET simulation if needed */ int nfds = ctx->backend_ops->wait(ctx, events, max_events, timeout); - if (nfds > 0 && ctx->simulate_et) { - nfds = php_poll_simulate_et(ctx, events, nfds); - } - if (UNEXPECTED(nfds < 0)) { php_poll_set_system_error_if_not_set(ctx); } @@ -371,7 +254,7 @@ PHPAPI php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx) /* Check edge-triggering support */ PHPAPI bool php_poll_supports_et(php_poll_ctx *ctx) { - return ctx && (ctx->backend_ops->supports_et || ctx->simulate_et); + return ctx && ctx->backend_ops && ctx->backend_ops->supports_et; } /* Error retrieval */ From 5b1c6f4efda5058f0131a67692bdfe8d56c85435 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 21 Aug 2025 16:02:52 +0200 Subject: [PATCH 21/52] poll: use common logic for fd tracking --- configure.ac | 1 + main/poll/php_poll_internal.h | 24 ++++ main/poll/poll_backend_eventport.c | 181 ++++++++---------------- main/poll/poll_backend_poll.c | 191 +++++-------------------- main/poll/poll_backend_select.c | 219 ++++++++--------------------- main/poll/poll_fd_table.c | 157 +++++++++++++++++++++ 6 files changed, 332 insertions(+), 441 deletions(-) create mode 100644 main/poll/poll_fd_table.c diff --git a/configure.ac b/configure.ac index f06c242b16474..a70fd1c1208ae 100644 --- a/configure.ac +++ b/configure.ac @@ -1685,6 +1685,7 @@ PHP_ADD_SOURCES([main/poll], m4_normalize([ poll_backend_poll.c poll_backend_select.c poll_core.c + poll_fd_table.c ]), [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h index eb5b7598fbc20..236ac45f56bfb 100644 --- a/main/poll/php_poll_internal.h +++ b/main/poll/php_poll_internal.h @@ -65,6 +65,30 @@ struct php_poll_ctx { 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; /* For edge-trigger simulation */ +} php_poll_fd_entry; + +/* FD tracking table */ +typedef struct php_poll_fd_table { + php_poll_fd_entry *entries; + int capacity; + int count; + bool persistent; +} php_poll_fd_table; + +php_poll_fd_table *php_poll_fd_table_init(int initial_capacity, bool persistent); +void php_poll_fd_table_cleanup(php_poll_fd_table *table); +php_poll_fd_entry *php_poll_fd_table_find(php_poll_fd_table *table, int fd); +php_poll_fd_entry *php_poll_fd_table_get(php_poll_fd_table *table, int fd); +void php_poll_fd_table_remove(php_poll_fd_table *table, int fd); +int php_poll_simulate_edge_trigger(php_poll_fd_table *table, php_poll_event *events, int nfds); + /* Internal functions */ const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backend); diff --git a/main/poll/poll_backend_eventport.c b/main/poll/poll_backend_eventport.c index 9ff2fc8a44965..2f407e6f79d8d 100644 --- a/main/poll/poll_backend_eventport.c +++ b/main/poll/poll_backend_eventport.c @@ -22,23 +22,12 @@ #include #include -typedef struct { - int fd; - uint32_t events; - void *data; - bool active; -} eventport_fd_entry; - typedef struct { int port_fd; port_event_t *events; int events_capacity; int active_associations; - - /* FD tracking for re-association */ - eventport_fd_entry *fd_entries; - int fd_entries_capacity; - int fd_count; + php_poll_fd_table *fd_table; } eventport_backend_data_t; /* Convert our event flags to event port flags */ @@ -85,68 +74,6 @@ static uint32_t eventport_events_from_native(int native) return events; } -/* Find FD entry */ -static eventport_fd_entry *eventport_find_fd_entry(eventport_backend_data_t *data, int fd) -{ - for (int i = 0; i < data->fd_entries_capacity; i++) { - if (data->fd_entries[i].active && data->fd_entries[i].fd == fd) { - return &data->fd_entries[i]; - } - } - return NULL; -} - -/* Get or create FD entry */ -static eventport_fd_entry *eventport_get_fd_entry( - eventport_backend_data_t *data, int fd, bool persistent) -{ - eventport_fd_entry *entry = eventport_find_fd_entry(data, fd); - if (entry) { - return entry; - } - - /* Find empty slot */ - for (int i = 0; i < data->fd_entries_capacity; i++) { - if (!data->fd_entries[i].active) { - data->fd_entries[i].fd = fd; - data->fd_entries[i].active = true; - data->fd_count++; - return &data->fd_entries[i]; - } - } - - int new_capacity = data->fd_entries_capacity ? data->fd_entries_capacity * 2 : 64; - eventport_fd_entry *new_entries - = perealloc(data->fd_entries, new_capacity * sizeof(eventport_fd_entry), persistent); - if (!new_entries) { - return NULL; - } - - memset(new_entries + data->fd_entries_capacity, 0, - (new_capacity - data->fd_entries_capacity) * sizeof(eventport_fd_entry)); - - data->fd_entries = new_entries; - - /* Use first new slot */ - eventport_fd_entry *new_entry = &data->fd_entries[data->fd_entries_capacity]; - new_entry->fd = fd; - new_entry->active = true; - data->fd_count++; - - data->fd_entries_capacity = new_capacity; - return new_entry; -} - -/* Remove FD entry */ -static void eventport_remove_fd_entry(eventport_backend_data_t *data, int fd) -{ - eventport_fd_entry *entry = eventport_find_fd_entry(data, fd); - if (entry) { - entry->active = false; - data->fd_count--; - } -} - /* Initialize event port backend */ static zend_result eventport_backend_init(php_poll_ctx *ctx) { @@ -165,7 +92,6 @@ static zend_result eventport_backend_init(php_poll_ctx *ctx) } data->active_associations = 0; - data->fd_count = 0; /* Use hint for initial allocation if provided, otherwise start with reasonable default */ int initial_capacity = ctx->max_events_hint > 0 ? ctx->max_events_hint : 64; @@ -178,16 +104,15 @@ static zend_result eventport_backend_init(php_poll_ctx *ctx) } data->events_capacity = initial_capacity; - /* Initialize FD tracking array */ - data->fd_entries = pecalloc(initial_capacity, sizeof(eventport_fd_entry), ctx->persistent); - if (!data->fd_entries) { + /* 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; } - data->fd_entries_capacity = initial_capacity; ctx->backend_data = data; return SUCCESS; @@ -202,7 +127,7 @@ static void eventport_backend_cleanup(php_poll_ctx *ctx) close(data->port_fd); } pefree(data->events, ctx->persistent); - pefree(data->fd_entries, ctx->persistent); + php_poll_fd_table_cleanup(data->fd_table); pefree(data, ctx->persistent); ctx->backend_data = NULL; } @@ -214,14 +139,12 @@ static zend_result eventport_backend_add( { eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; - /* Check if FD already exists */ - if (eventport_find_fd_entry(backend_data, fd)) { + if (php_poll_fd_table_find(backend_data->fd_table, fd)) { php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); return FAILURE; } - /* Get FD entry for tracking */ - eventport_fd_entry *entry = eventport_get_fd_entry(backend_data, fd, ctx->persistent); + php_poll_fd_entry *entry = php_poll_fd_table_get(backend_data->fd_table, fd); if (!entry) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; @@ -234,7 +157,7 @@ static zend_result eventport_backend_add( /* Associate file descriptor with event port */ if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, user_data) == -1) { - eventport_remove_fd_entry(backend_data, fd); + php_poll_fd_table_remove(backend_data->fd_table, fd); switch (errno) { case EEXIST: php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); @@ -263,8 +186,7 @@ static zend_result eventport_backend_modify( { eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; - /* Find existing entry */ - eventport_fd_entry *entry = eventport_find_fd_entry(backend_data, fd); + 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; @@ -303,9 +225,8 @@ 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; - /* Find existing entry */ - eventport_fd_entry *entry = eventport_find_fd_entry(backend_data, fd); - if (!entry) { + /* Check if exists using helper */ + if (!php_poll_fd_table_find(backend_data->fd_table, fd)) { php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); return FAILURE; } @@ -326,11 +247,51 @@ static zend_result eventport_backend_remove(php_poll_ctx *ctx, int fd) } } - eventport_remove_fd_entry(backend_data, fd); + php_poll_fd_table_remove(backend_data->fd_table, fd); backend_data->active_associations--; return SUCCESS; } +/* Handle re-association after event */ +static void eventport_handle_reassociation( + eventport_backend_data_t *backend_data, int fd, uint32_t fired_events) +{ + php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, fd); + if (!entry) { + return; + } + + if (entry->events & PHP_POLL_ONESHOT) { + /* Oneshot: remove from tracking */ + php_poll_fd_table_remove(backend_data->fd_table, fd); + backend_data->active_associations--; + return; + } + + /* Determine which events to re-associate with */ + uint32_t reassoc_events = entry->events; + if (entry->events & PHP_POLL_ET) { + /* Edge-triggered: don't re-associate with events that just fired */ + reassoc_events &= ~fired_events; + reassoc_events &= ~PHP_POLL_ET; /* Remove ET flag for port_associate */ + } + + if (reassoc_events != 0) { + /* Re-associate for continued monitoring */ + int native_events = eventport_events_to_native(reassoc_events); + if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, entry->data) + != 0) { + /* Re-association failed - might be due to fd being closed */ + php_poll_fd_table_remove(backend_data->fd_table, fd); + backend_data->active_associations--; + } + } else { + /* No events to re-associate with */ + php_poll_fd_table_remove(backend_data->fd_table, fd); + backend_data->active_associations--; + } +} + /* Wait for events using event port */ static int eventport_backend_wait( php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) @@ -387,7 +348,7 @@ static int eventport_backend_wait( int nfds = (int) nget; - /* Process the raw events first */ + /* Process the events and handle re-association */ for (int i = 0; i < nfds; i++) { port_event_t *port_event = &backend_data->events[i]; @@ -399,40 +360,8 @@ static int eventport_backend_wait( events[i].revents = eventport_events_from_native(port_event->portev_events); events[i].data = port_event->portev_user; - /* Event ports are naturally level-triggered but auto-dissociate after firing. - * Re-associate unless it's oneshot */ - eventport_fd_entry *entry = eventport_find_fd_entry(backend_data, fd); - if (entry && !(entry->events & PHP_POLL_ONESHOT)) { - /* For level-triggered: re-associate with all events - * For edge-triggered: re-associate with events that didn't fire */ - uint32_t reassoc_events = entry->events; - if (entry->events & PHP_POLL_ET) { - /* Edge-triggered: don't re-associate with events that just fired */ - reassoc_events &= ~events[i].revents; - reassoc_events &= ~PHP_POLL_ET; /* Remove ET flag for port_associate */ - } - - if (reassoc_events != 0) { - int native_events = eventport_events_to_native(reassoc_events); - if (port_associate(backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, - entry->data) - != 0) { - /* Re-association failed might be due to fd being closed */ - eventport_remove_fd_entry(backend_data, fd); - backend_data->active_associations--; - } - } else { - /* No events to re-associate with */ - eventport_remove_fd_entry(backend_data, fd); - backend_data->active_associations--; - } - } else { - /* Oneshot or entry not found - remove from tracking */ - if (entry) { - eventport_remove_fd_entry(backend_data, fd); - } - backend_data->active_associations--; - } + /* Handle re-association based on event type */ + eventport_handle_reassociation(backend_data, fd, events[i].revents); } else { /* Handle other event sources if needed (timers, user events, etc.) */ events[i].fd = -1; diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 68666d7895a60..6a005beea0634 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -20,23 +20,13 @@ #include #include -typedef struct { - int fd; - uint32_t events; - void *data; - bool active; - uint32_t last_revents; /* For edge-trigger simulation */ -} poll_fd_entry; - typedef struct { struct pollfd *fds; int fds_capacity; int fds_used; - /* FD tracking for user data and edge-trigger simulation */ - poll_fd_entry *fd_entries; - int fd_entries_capacity; - int fd_count; + /* Use common FD tracking helper */ + php_poll_fd_table *fd_table; } poll_backend_data_t; static uint32_t poll_events_to_native(uint32_t events) @@ -75,71 +65,6 @@ static uint32_t poll_events_from_native(uint32_t native) return events; } -/* Find FD entry */ -static poll_fd_entry *poll_find_fd_entry(poll_backend_data_t *data, int fd) -{ - for (int i = 0; i < data->fd_entries_capacity; i++) { - if (data->fd_entries[i].active && data->fd_entries[i].fd == fd) { - return &data->fd_entries[i]; - } - } - return NULL; -} - -/* Get or create FD entry */ -static poll_fd_entry *poll_get_fd_entry(poll_backend_data_t *data, int fd, bool persistent) -{ - poll_fd_entry *entry = poll_find_fd_entry(data, fd); - if (entry) { - return entry; - } - - /* Find empty slot */ - for (int i = 0; i < data->fd_entries_capacity; i++) { - if (!data->fd_entries[i].active) { - data->fd_entries[i].fd = fd; - data->fd_entries[i].active = true; - data->fd_entries[i].last_revents = 0; - data->fd_count++; - return &data->fd_entries[i]; - } - } - - /* Need to grow the array */ - int new_capacity = data->fd_entries_capacity ? data->fd_entries_capacity * 2 : 64; - poll_fd_entry *new_entries - = perealloc(data->fd_entries, new_capacity * sizeof(poll_fd_entry), persistent); - if (!new_entries) { - return NULL; - } - - /* Initialize new entries */ - memset(new_entries + data->fd_entries_capacity, 0, - (new_capacity - data->fd_entries_capacity) * sizeof(poll_fd_entry)); - - data->fd_entries = new_entries; - - /* Use first new slot */ - poll_fd_entry *new_entry = &data->fd_entries[data->fd_entries_capacity]; - new_entry->fd = fd; - new_entry->active = true; - new_entry->last_revents = 0; - data->fd_count++; - - data->fd_entries_capacity = new_capacity; - return new_entry; -} - -/* Remove FD entry */ -static void poll_remove_fd_entry(poll_backend_data_t *data, int fd) -{ - poll_fd_entry *entry = poll_find_fd_entry(data, fd); - if (entry) { - entry->active = false; - data->fd_count--; - } -} - /* Find pollfd slot */ static struct pollfd *poll_find_pollfd_slot(poll_backend_data_t *data, int fd) { @@ -182,16 +107,14 @@ static zend_result poll_backend_init(php_poll_ctx *ctx) data->fds_capacity = initial_capacity; data->fds_used = 0; - /* Initialize FD tracking array */ - data->fd_entries = pecalloc(initial_capacity, sizeof(poll_fd_entry), ctx->persistent); - if (!data->fd_entries) { + /* Initialize FD tracking using helper */ + data->fd_table = php_poll_fd_table_init(initial_capacity, ctx->persistent); + if (!data->fd_table) { pefree(data->fds, ctx->persistent); pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } - data->fd_entries_capacity = initial_capacity; - data->fd_count = 0; /* Initialize all fds to -1 */ for (int i = 0; i < initial_capacity; i++) { @@ -207,7 +130,7 @@ static void poll_backend_cleanup(php_poll_ctx *ctx) poll_backend_data_t *data = (poll_backend_data_t *) ctx->backend_data; if (data) { pefree(data->fds, ctx->persistent); - pefree(data->fd_entries, ctx->persistent); + php_poll_fd_table_cleanup(data->fd_table); pefree(data, ctx->persistent); ctx->backend_data = NULL; } @@ -217,14 +140,14 @@ static zend_result poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; - /* Check if FD already exists */ - if (poll_find_fd_entry(backend_data, fd)) { + /* Check if FD already exists using helper */ + if (php_poll_fd_table_find(backend_data->fd_table, fd)) { php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); return FAILURE; } - /* Get FD entry for tracking */ - poll_fd_entry *entry = poll_get_fd_entry(backend_data, fd, ctx->persistent); + /* Get FD entry using helper */ + php_poll_fd_entry *entry = php_poll_fd_table_get(backend_data->fd_table, fd); if (!entry) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; @@ -241,7 +164,7 @@ static zend_result poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, struct pollfd *new_fds = perealloc( backend_data->fds, new_capacity * sizeof(struct pollfd), ctx->persistent); if (!new_fds) { - poll_remove_fd_entry(backend_data, fd); + php_poll_fd_table_remove(backend_data->fd_table, fd); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; } @@ -269,8 +192,8 @@ static zend_result poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t event { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; - /* Find existing entry */ - poll_fd_entry *entry = poll_find_fd_entry(backend_data, fd); + /* Find existing entry using helper */ + 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; @@ -294,9 +217,8 @@ 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; - /* Find existing entry */ - poll_fd_entry *entry = poll_find_fd_entry(backend_data, fd); - if (!entry) { + /* Check if exists using helper */ + if (!php_poll_fd_table_find(backend_data->fd_table, fd)) { php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); return FAILURE; } @@ -310,68 +232,24 @@ static zend_result poll_backend_remove(php_poll_ctx *ctx, int fd) backend_data->fds_used--; } - /* Remove from tracking */ - poll_remove_fd_entry(backend_data, fd); + /* Remove from tracking using helper */ + php_poll_fd_table_remove(backend_data->fd_table, fd); return SUCCESS; } -/* Edge-trigger simulation */ -static int poll_simulate_edge_trigger( - poll_backend_data_t *backend_data, php_poll_event *events, int nfds) +/* Handle oneshot removal after event */ +static void poll_handle_oneshot_removal(poll_backend_data_t *backend_data, int fd) { - int filtered_count = 0; - - for (int i = 0; i < nfds; i++) { - poll_fd_entry *entry = poll_find_fd_entry(backend_data, events[i].fd); - if (!entry) { - continue; - } - - uint32_t new_events = events[i].revents; - uint32_t reported_events = 0; - - if (entry->events & PHP_POLL_ET) { - /* Edge-triggered: report edges only */ - if ((new_events & PHP_POLL_READ) && !(entry->last_revents & PHP_POLL_READ)) { - reported_events |= PHP_POLL_READ; - } - if ((new_events & PHP_POLL_WRITE) && !(entry->last_revents & PHP_POLL_WRITE)) { - reported_events |= PHP_POLL_WRITE; - } - /* Always report error and hangup events */ - reported_events |= (new_events & (PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP)); - } else { - /* Level-triggered: report all active events */ - reported_events = new_events; - } - - entry->last_revents = new_events; - - /* Only include this event if we have something to report */ - if (reported_events != 0) { - if (filtered_count != i) { - events[filtered_count] = events[i]; - } - events[filtered_count].revents = reported_events; - filtered_count++; - - /* Handle oneshot: remove after first event */ - if (entry->events & PHP_POLL_ONESHOT) { - /* Mark pollfd as disabled */ - struct pollfd *pfd = poll_find_pollfd_slot(backend_data, events[i].fd); - if (pfd) { - pfd->fd = -1; - pfd->events = 0; - pfd->revents = 0; - backend_data->fds_used--; - } - poll_remove_fd_entry(backend_data, events[i].fd); - } - } + /* Mark pollfd as disabled */ + struct pollfd *pfd = poll_find_pollfd_slot(backend_data, fd); + if (pfd) { + pfd->fd = -1; + pfd->events = 0; + pfd->revents = 0; + backend_data->fds_used--; } - - return filtered_count; + php_poll_fd_table_remove(backend_data->fd_table, fd); } static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) @@ -395,7 +273,8 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ int event_count = 0; for (int i = 0; i < backend_data->fds_capacity && event_count < max_events; i++) { if (backend_data->fds[i].fd != -1 && backend_data->fds[i].revents != 0) { - poll_fd_entry *entry = poll_find_fd_entry(backend_data, backend_data->fds[i].fd); + php_poll_fd_entry *entry + = php_poll_fd_table_find(backend_data->fd_table, backend_data->fds[i].fd); if (entry) { events[event_count].fd = backend_data->fds[i].fd; events[event_count].events = entry->events; @@ -409,8 +288,16 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ } } - /* Apply edge-trigger simulation */ - nfds = poll_simulate_edge_trigger(backend_data, events, event_count); + /* Apply edge-trigger simulation using helper */ + nfds = php_poll_simulate_edge_trigger(backend_data->fd_table, events, event_count); + + /* Handle oneshot removals after simulation */ + for (int i = 0; i < nfds; 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) { + poll_handle_oneshot_removal(backend_data, events[i].fd); + } + } } return nfds; diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c index 2e02a44c7c11c..c9417a00f30b5 100644 --- a/main/poll/poll_backend_select.c +++ b/main/poll/poll_backend_select.c @@ -15,99 +15,28 @@ #include "php_poll_internal.h" #include "php_network.h" -typedef struct { - php_socket_t fd; - uint32_t events; - void *data; - bool active; - uint32_t last_revents; /* For edge-trigger simulation */ -} select_fd_entry; - typedef struct { fd_set read_fds, write_fds, error_fds; fd_set master_read_fds, master_write_fds, master_error_fds; - /* FD tracking for user data and edge-trigger simulation */ - select_fd_entry *fd_entries; - int fd_entries_capacity; - int fd_count; + /* Use common FD tracking helper */ + php_poll_fd_table *fd_table; php_socket_t max_fd; /* Highest fd for select() */ } select_backend_data_t; -/* Find FD entry */ -static select_fd_entry *select_find_fd_entry(select_backend_data_t *data, php_socket_t fd) -{ - for (int i = 0; i < data->fd_entries_capacity; i++) { - if (data->fd_entries[i].active && data->fd_entries[i].fd == fd) { - return &data->fd_entries[i]; - } - } - return NULL; -} - -/* Get or create FD entry */ -static select_fd_entry *select_get_fd_entry( - select_backend_data_t *data, php_socket_t fd, bool persistent) -{ - select_fd_entry *entry = select_find_fd_entry(data, fd); - if (entry) { - return entry; - } - - /* Find empty slot */ - for (int i = 0; i < data->fd_entries_capacity; i++) { - if (!data->fd_entries[i].active) { - data->fd_entries[i].fd = fd; - data->fd_entries[i].active = true; - data->fd_entries[i].last_revents = 0; - data->fd_count++; - return &data->fd_entries[i]; - } - } - - /* Need to grow the array */ - int new_capacity = data->fd_entries_capacity ? data->fd_entries_capacity * 2 : 64; - select_fd_entry *new_entries - = perealloc(data->fd_entries, new_capacity * sizeof(select_fd_entry), persistent); - if (!new_entries) { - return NULL; - } - - /* Initialize new entries */ - memset(new_entries + data->fd_entries_capacity, 0, - (new_capacity - data->fd_entries_capacity) * sizeof(select_fd_entry)); - - data->fd_entries = new_entries; - - /* Use first new slot */ - select_fd_entry *new_entry = &data->fd_entries[data->fd_entries_capacity]; - new_entry->fd = fd; - new_entry->active = true; - new_entry->last_revents = 0; - data->fd_count++; - - data->fd_entries_capacity = new_capacity; - return new_entry; -} - -/* Remove FD entry */ -static void select_remove_fd_entry(select_backend_data_t *data, php_socket_t fd) -{ - select_fd_entry *entry = select_find_fd_entry(data, fd); - if (entry) { - entry->active = false; - data->fd_count--; - } -} - -/* Update max_fd for select() */ +/* Update max_fd for select() by scanning all active FDs */ static void select_update_max_fd(select_backend_data_t *data) { data->max_fd = 0; - for (int i = 0; i < data->fd_entries_capacity; i++) { - if (data->fd_entries[i].active && data->fd_entries[i].fd > data->max_fd) { - data->max_fd = data->fd_entries[i].fd; + php_poll_fd_table *table = data->fd_table; + + for (int i = 0; i < table->capacity; i++) { + if (table->entries[i].active) { + php_socket_t sock = (php_socket_t) table->entries[i].fd; + if (sock > data->max_fd) { + data->max_fd = sock; + } } } } @@ -123,14 +52,13 @@ static zend_result select_backend_init(php_poll_ctx *ctx) /* 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->fd_entries = pecalloc(initial_capacity, sizeof(select_fd_entry), ctx->persistent); - if (!data->fd_entries) { + /* Initialize FD tracking using helper */ + 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->fd_entries_capacity = initial_capacity; - data->fd_count = 0; FD_ZERO(&data->master_read_fds); FD_ZERO(&data->master_write_fds); @@ -145,7 +73,7 @@ static void select_backend_cleanup(php_poll_ctx *ctx) { select_backend_data_t *data = (select_backend_data_t *) ctx->backend_data; if (data) { - pefree(data->fd_entries, ctx->persistent); + php_poll_fd_table_cleanup(data->fd_table); pefree(data, ctx->persistent); ctx->backend_data = NULL; } @@ -163,14 +91,14 @@ static zend_result select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events } #endif - /* Check if FD already exists */ - if (select_find_fd_entry(backend_data, sock)) { + /* Check if FD already exists using helper */ + if (php_poll_fd_table_find(backend_data->fd_table, fd)) { php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); return FAILURE; } - /* Get FD entry for tracking */ - select_fd_entry *entry = select_get_fd_entry(backend_data, sock, ctx->persistent); + /* Get FD entry using helper */ + php_poll_fd_entry *entry = php_poll_fd_table_get(backend_data->fd_table, fd); if (!entry) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; @@ -203,8 +131,8 @@ static zend_result select_backend_modify( select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; - /* Find existing entry */ - select_fd_entry *entry = select_find_fd_entry(backend_data, sock); + /* Find existing entry using helper */ + 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; @@ -237,9 +165,8 @@ static zend_result select_backend_remove(php_poll_ctx *ctx, int fd) select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; - /* Find existing entry */ - select_fd_entry *entry = select_find_fd_entry(backend_data, sock); - if (!entry) { + /* Check if exists using helper */ + if (!php_poll_fd_table_find(backend_data->fd_table, fd)) { php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); return FAILURE; } @@ -249,8 +176,8 @@ static zend_result select_backend_remove(php_poll_ctx *ctx, int fd) FD_CLR(sock, &backend_data->master_write_fds); FD_CLR(sock, &backend_data->master_error_fds); - /* Remove from tracking */ - select_remove_fd_entry(backend_data, sock); + /* Remove from tracking using helper */ + php_poll_fd_table_remove(backend_data->fd_table, fd); /* Update max_fd if this was the highest */ if (sock == backend_data->max_fd) { @@ -260,67 +187,23 @@ static zend_result select_backend_remove(php_poll_ctx *ctx, int fd) return SUCCESS; } -/* Edge-trigger simulation */ -static int select_simulate_edge_trigger( - select_backend_data_t *backend_data, php_poll_event *events, int nfds) +/* Handle oneshot removal after event */ +static void select_handle_oneshot_removal(select_backend_data_t *backend_data, int fd) { - int filtered_count = 0; - - for (int i = 0; i < nfds; i++) { - select_fd_entry *entry = select_find_fd_entry(backend_data, (php_socket_t) events[i].fd); - if (!entry) { - continue; - } - - uint32_t new_events = events[i].revents; - uint32_t reported_events = 0; - - if (entry->events & PHP_POLL_ET) { - /* Edge-triggered: report edges only */ - if ((new_events & PHP_POLL_READ) && !(entry->last_revents & PHP_POLL_READ)) { - reported_events |= PHP_POLL_READ; - } - if ((new_events & PHP_POLL_WRITE) && !(entry->last_revents & PHP_POLL_WRITE)) { - reported_events |= PHP_POLL_WRITE; - } - /* Always report error and hangup events */ - reported_events |= (new_events & (PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP)); - } else { - /* Level-triggered: report all active events */ - reported_events = new_events; - } - - entry->last_revents = new_events; - - /* Only include this event if we have something to report */ - if (reported_events != 0) { - if (filtered_count != i) { - events[filtered_count] = events[i]; - } - events[filtered_count].revents = reported_events; - filtered_count++; - - /* Handle oneshot: remove after first event */ - if (entry->events & PHP_POLL_ONESHOT) { - php_socket_t sock = (php_socket_t) events[i].fd; + php_socket_t sock = (php_socket_t) fd; - /* Remove from fd_sets */ - FD_CLR(sock, &backend_data->master_read_fds); - FD_CLR(sock, &backend_data->master_write_fds); - FD_CLR(sock, &backend_data->master_error_fds); + /* Remove from fd_sets */ + FD_CLR(sock, &backend_data->master_read_fds); + FD_CLR(sock, &backend_data->master_write_fds); + FD_CLR(sock, &backend_data->master_error_fds); - /* Remove from tracking */ - select_remove_fd_entry(backend_data, sock); + /* Remove from tracking */ + php_poll_fd_table_remove(backend_data->fd_table, fd); - /* Update max_fd if needed */ - if (sock == backend_data->max_fd) { - select_update_max_fd(backend_data); - } - } - } + /* Update max_fd if needed */ + if (sock == backend_data->max_fd) { + select_update_max_fd(backend_data); } - - return filtered_count; } static int select_backend_wait( @@ -328,7 +211,7 @@ static int select_backend_wait( { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; - if (backend_data->fd_count == 0) { + if (backend_data->fd_table->count == 0) { /* No sockets to wait for, but respect timeout */ if (timeout > 0) { #ifdef _WIN32 @@ -366,12 +249,14 @@ static int select_backend_wait( /* Process results */ int event_count = 0; - for (int i = 0; i < backend_data->fd_entries_capacity && event_count < max_events; i++) { - if (!backend_data->fd_entries[i].active) { + php_poll_fd_table *table = backend_data->fd_table; + + for (int i = 0; i < table->capacity && event_count < max_events; i++) { + if (!table->entries[i].active) { continue; } - php_socket_t sock = backend_data->fd_entries[i].fd; + php_socket_t sock = (php_socket_t) table->entries[i].fd; uint32_t revents = 0; if (FD_ISSET(sock, &backend_data->read_fds)) { @@ -385,16 +270,24 @@ static int select_backend_wait( } if (revents != 0) { - events[event_count].fd = (int) sock; - events[event_count].events = backend_data->fd_entries[i].events; + events[event_count].fd = table->entries[i].fd; + events[event_count].events = table->entries[i].events; events[event_count].revents = revents; - events[event_count].data = backend_data->fd_entries[i].data; + events[event_count].data = table->entries[i].data; event_count++; } } - /* Apply edge-trigger simulation */ - int nfds = select_simulate_edge_trigger(backend_data, events, event_count); + /* Apply edge-trigger simulation using helper */ + int nfds = php_poll_simulate_edge_trigger(backend_data->fd_table, events, event_count); + + /* Handle oneshot removals after simulation */ + for (int i = 0; i < nfds; 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) { + select_handle_oneshot_removal(backend_data, events[i].fd); + } + } return nfds; } diff --git a/main/poll/poll_fd_table.c b/main/poll/poll_fd_table.c new file mode 100644 index 0000000000000..9147e49ed21f8 --- /dev/null +++ b/main/poll/poll_fd_table.c @@ -0,0 +1,157 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +#include "php_poll_internal.h" + +/* Initialize FD table */ +php_poll_fd_table *php_poll_fd_table_init(int initial_capacity, bool persistent) +{ + php_poll_fd_table *table = pecalloc(1, sizeof(php_poll_fd_table), persistent); + if (!table) { + return NULL; + } + + if (initial_capacity <= 0) { + initial_capacity = 64; + } + + table->entries = pecalloc(initial_capacity, sizeof(php_poll_fd_entry), persistent); + if (!table->entries) { + pefree(table, persistent); + return NULL; + } + + table->capacity = initial_capacity; + table->count = 0; + table->persistent = persistent; + return table; +} + +/* Cleanup FD table */ +void php_poll_fd_table_cleanup(php_poll_fd_table *table) +{ + if (table) { + pefree(table->entries, table->persistent); + pefree(table, table->persistent); + } +} + +/* Find FD entry */ +php_poll_fd_entry *php_poll_fd_table_find(php_poll_fd_table *table, int fd) +{ + for (int i = 0; i < table->capacity; i++) { + if (table->entries[i].active && table->entries[i].fd == fd) { + return &table->entries[i]; + } + } + return NULL; +} + +/* Get or create FD 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; + } + + /* Find empty slot */ + for (int i = 0; i < table->capacity; i++) { + if (!table->entries[i].active) { + table->entries[i].fd = fd; + table->entries[i].active = true; + table->entries[i].last_revents = 0; + table->count++; + return &table->entries[i]; + } + } + + /* Need to grow the array */ + int new_capacity = table->capacity * 2; + php_poll_fd_entry *new_entries = perealloc( + table->entries, new_capacity * sizeof(php_poll_fd_entry), table->persistent); + if (!new_entries) { + return NULL; + } + + /* Initialize new entries */ + memset(new_entries + table->capacity, 0, + (new_capacity - table->capacity) * sizeof(php_poll_fd_entry)); + + table->entries = new_entries; + + /* Use first new slot */ + php_poll_fd_entry *new_entry = &table->entries[table->capacity]; + new_entry->fd = fd; + new_entry->active = true; + new_entry->last_revents = 0; + table->count++; + + table->capacity = new_capacity; + return new_entry; +} + +/* Remove FD entry */ +void php_poll_fd_table_remove(php_poll_fd_table *table, int fd) +{ + php_poll_fd_entry *entry = php_poll_fd_table_find(table, fd); + if (entry) { + entry->active = false; + table->count--; + } +} + +/* Edge-trigger simulation helper */ +int php_poll_simulate_edge_trigger(php_poll_fd_table *table, php_poll_event *events, int nfds) +{ + int filtered_count = 0; + + for (int i = 0; i < nfds; i++) { + php_poll_fd_entry *entry = php_poll_fd_table_find(table, events[i].fd); + if (!entry) { + continue; + } + + uint32_t new_events = events[i].revents; + uint32_t reported_events = 0; + + if (entry->events & PHP_POLL_ET) { + /* Edge-triggered: report edges only */ + if ((new_events & PHP_POLL_READ) && !(entry->last_revents & PHP_POLL_READ)) { + reported_events |= PHP_POLL_READ; + } + if ((new_events & PHP_POLL_WRITE) && !(entry->last_revents & PHP_POLL_WRITE)) { + reported_events |= PHP_POLL_WRITE; + } + /* Always report error and hangup events */ + reported_events |= (new_events & (PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP)); + } else { + /* Level-triggered: report all active events */ + reported_events = new_events; + } + + entry->last_revents = new_events; + + /* Only include this event if we have something to report */ + if (reported_events != 0) { + if (filtered_count != i) { + events[filtered_count] = events[i]; + } + events[filtered_count].revents = reported_events; + filtered_count++; + } + } + + return filtered_count; +} From 793b0946930e8eaa68ba83f3651a39d21ee8be65 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 21 Aug 2025 18:27:30 +0200 Subject: [PATCH 22/52] poll: use custom allocation macros that can fail on persistent --- main/poll/php_poll_internal.h | 8 ++++++++ main/poll/poll_backend_epoll.c | 6 +++--- main/poll/poll_backend_eventport.c | 7 ++++--- main/poll/poll_backend_iocp.c | 8 ++++---- main/poll/poll_backend_kqueue.c | 7 ++++--- main/poll/poll_backend_poll.c | 6 +++--- main/poll/poll_backend_select.c | 3 ++- main/poll/poll_core.c | 4 ++-- main/poll/poll_fd_table.c | 6 +++--- 9 files changed, 33 insertions(+), 22 deletions(-) diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h index 236ac45f56bfb..2d9ae3ce2497c 100644 --- a/main/poll/php_poll_internal.h +++ b/main/poll/php_poll_internal.h @@ -17,6 +17,13 @@ #include "php_poll.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; @@ -82,6 +89,7 @@ typedef struct php_poll_fd_table { bool persistent; } php_poll_fd_table; +/* Poll FD helpers */ 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); diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c index 11408d58c6da4..c027a4c299854 100644 --- a/main/poll/poll_backend_epoll.c +++ b/main/poll/poll_backend_epoll.c @@ -74,7 +74,7 @@ static uint32_t epoll_events_from_native(uint32_t native) static zend_result epoll_backend_init(php_poll_ctx *ctx) { - epoll_backend_data_t *data = pecalloc(1, sizeof(epoll_backend_data_t), ctx->persistent); + 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; @@ -89,7 +89,7 @@ static zend_result epoll_backend_init(php_poll_ctx *ctx) /* 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 = pecalloc(initial_capacity, sizeof(struct epoll_event), ctx->persistent); + data->events = php_poll_calloc(initial_capacity, sizeof(struct epoll_event), ctx->persistent); if (!data->events) { close(data->epoll_fd); pefree(data, ctx->persistent); @@ -166,7 +166,7 @@ static int epoll_backend_wait( /* Ensure we have enough space for the requested events */ if (max_events > backend_data->events_capacity) { - struct epoll_event *new_events = perealloc( + 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); diff --git a/main/poll/poll_backend_eventport.c b/main/poll/poll_backend_eventport.c index 2f407e6f79d8d..177b9d9f101a5 100644 --- a/main/poll/poll_backend_eventport.c +++ b/main/poll/poll_backend_eventport.c @@ -77,7 +77,8 @@ static uint32_t eventport_events_from_native(int native) /* Initialize event port backend */ static zend_result eventport_backend_init(php_poll_ctx *ctx) { - eventport_backend_data_t *data = pecalloc(1, sizeof(eventport_backend_data_t), ctx->persistent); + 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; @@ -95,7 +96,7 @@ static zend_result eventport_backend_init(php_poll_ctx *ctx) /* 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 = pecalloc(initial_capacity, sizeof(port_event_t), ctx->persistent); + data->events = php_poll_calloc(initial_capacity, sizeof(port_event_t), ctx->persistent); if (!data->events) { close(data->port_fd); pefree(data, ctx->persistent); @@ -311,7 +312,7 @@ static int eventport_backend_wait( /* Ensure we have enough space for the requested events */ if (max_events > backend_data->events_capacity) { - port_event_t *new_events = perealloc( + 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); diff --git a/main/poll/poll_backend_iocp.c b/main/poll/poll_backend_iocp.c index f68520f380918..6762875ae75c7 100644 --- a/main/poll/poll_backend_iocp.c +++ b/main/poll/poll_backend_iocp.c @@ -86,7 +86,7 @@ static iocp_fd_entry *iocp_get_fd_entry(iocp_backend_data_t *data, int fd, bool /* Need to grow the array */ int new_capacity = data->fd_entries_capacity ? data->fd_entries_capacity * 2 : 64; iocp_fd_entry *new_entries - = perealloc(data->fd_entries, new_capacity * sizeof(iocp_fd_entry), persistent); + = php_poll_realloc(data->fd_entries, new_capacity * sizeof(iocp_fd_entry), persistent); if (!new_entries) { return NULL; } @@ -121,7 +121,7 @@ static void iocp_remove_fd_entry(iocp_backend_data_t *data, int fd) static zend_result iocp_backend_init(php_poll_ctx *ctx) { - iocp_backend_data_t *data = pecalloc(1, sizeof(iocp_backend_data_t), ctx->persistent); + iocp_backend_data_t *data = php_poll_calloc(1, sizeof(iocp_backend_data_t), ctx->persistent); if (!data) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; @@ -138,7 +138,7 @@ static zend_result iocp_backend_init(php_poll_ctx *ctx) /* Use hint for initial allocation if provided, otherwise start with reasonable default */ int initial_capacity = ctx->max_events_hint > 0 ? ctx->max_events_hint : 64; - data->operations = pecalloc(initial_capacity, sizeof(iocp_operation_t), ctx->persistent); + data->operations = php_poll_calloc(initial_capacity, sizeof(iocp_operation_t), ctx->persistent); if (!data->operations) { CloseHandle(data->iocp_handle); pefree(data, ctx->persistent); @@ -149,7 +149,7 @@ static zend_result iocp_backend_init(php_poll_ctx *ctx) data->operation_count = 0; /* Initialize FD tracking array */ - data->fd_entries = pecalloc(initial_capacity, sizeof(iocp_fd_entry), ctx->persistent); + data->fd_entries = php_poll_calloc(initial_capacity, sizeof(iocp_fd_entry), ctx->persistent); if (!data->fd_entries) { CloseHandle(data->iocp_handle); pefree(data->operations, ctx->persistent); diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 434f6855acbd9..466c701b3eb07 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -28,7 +28,8 @@ typedef struct { static zend_result kqueue_backend_init(php_poll_ctx *ctx) { - kqueue_backend_data_t *data = pecalloc(1, sizeof(kqueue_backend_data_t), ctx->persistent); + 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; @@ -43,7 +44,7 @@ static zend_result kqueue_backend_init(php_poll_ctx *ctx) /* 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 = pecalloc(initial_capacity, sizeof(struct kevent), ctx->persistent); + data->events = php_poll_calloc(initial_capacity, sizeof(struct kevent), ctx->persistent); if (!data->events) { close(data->kqueue_fd); pefree(data, ctx->persistent); @@ -223,7 +224,7 @@ static int kqueue_backend_wait( /* Ensure we have enough space for the requested events */ if (max_events > backend_data->events_capacity) { - struct kevent *new_events = perealloc( + struct kevent *new_events = php_poll_realloc( backend_data->events, max_events * sizeof(struct kevent), ctx->persistent); if (!new_events) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 6a005beea0634..06c2f41f9ceee 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -89,7 +89,7 @@ static struct pollfd *poll_get_empty_pollfd_slot(poll_backend_data_t *data) static zend_result poll_backend_init(php_poll_ctx *ctx) { - poll_backend_data_t *data = pecalloc(1, sizeof(poll_backend_data_t), ctx->persistent); + 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; @@ -98,7 +98,7 @@ static zend_result poll_backend_init(php_poll_ctx *ctx) /* 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->fds = pecalloc(initial_capacity, sizeof(struct pollfd), ctx->persistent); + data->fds = php_poll_calloc(initial_capacity, sizeof(struct pollfd), ctx->persistent); if (!data->fds) { pefree(data, ctx->persistent); php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); @@ -161,7 +161,7 @@ static zend_result poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, if (!pfd) { /* Need to grow the pollfd array */ int new_capacity = backend_data->fds_capacity * 2; - struct pollfd *new_fds = perealloc( + struct pollfd *new_fds = php_poll_realloc( backend_data->fds, new_capacity * sizeof(struct pollfd), ctx->persistent); if (!new_fds) { php_poll_fd_table_remove(backend_data->fd_table, fd); diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c index c9417a00f30b5..058d8896669c6 100644 --- a/main/poll/poll_backend_select.c +++ b/main/poll/poll_backend_select.c @@ -43,7 +43,8 @@ static void select_update_max_fd(select_backend_data_t *data) static zend_result select_backend_init(php_poll_ctx *ctx) { - select_backend_data_t *data = pecalloc(1, sizeof(select_backend_data_t), ctx->persistent); + select_backend_data_t *data + = php_poll_calloc(1, sizeof(select_backend_data_t), ctx->persistent); if (!data) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return FAILURE; diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 8e19af94a4f25..440608bd3feb8 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -99,7 +99,7 @@ const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backe /* Create new poll context */ PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, bool persistent) { - php_poll_ctx *ctx = pecalloc(1, sizeof(php_poll_ctx), persistent); + php_poll_ctx *ctx = php_poll_calloc(1, sizeof(php_poll_ctx), persistent); if (!ctx) { return NULL; } @@ -261,4 +261,4 @@ PHPAPI bool php_poll_supports_et(php_poll_ctx *ctx) PHPAPI php_poll_error php_poll_get_error(php_poll_ctx *ctx) { return ctx ? ctx->last_error : PHP_POLL_ERR_INVALID; -} +} \ No newline at end of file diff --git a/main/poll/poll_fd_table.c b/main/poll/poll_fd_table.c index 9147e49ed21f8..bcf2ce18cc7b2 100644 --- a/main/poll/poll_fd_table.c +++ b/main/poll/poll_fd_table.c @@ -17,7 +17,7 @@ /* Initialize FD table */ php_poll_fd_table *php_poll_fd_table_init(int initial_capacity, bool persistent) { - php_poll_fd_table *table = pecalloc(1, sizeof(php_poll_fd_table), persistent); + php_poll_fd_table *table = php_poll_calloc(1, sizeof(php_poll_fd_table), persistent); if (!table) { return NULL; } @@ -26,7 +26,7 @@ php_poll_fd_table *php_poll_fd_table_init(int initial_capacity, bool persistent) initial_capacity = 64; } - table->entries = pecalloc(initial_capacity, sizeof(php_poll_fd_entry), persistent); + table->entries = php_poll_calloc(initial_capacity, sizeof(php_poll_fd_entry), persistent); if (!table->entries) { pefree(table, persistent); return NULL; @@ -79,7 +79,7 @@ php_poll_fd_entry *php_poll_fd_table_get(php_poll_fd_table *table, int fd) /* Need to grow the array */ int new_capacity = table->capacity * 2; - php_poll_fd_entry *new_entries = perealloc( + php_poll_fd_entry *new_entries = php_poll_realloc( table->entries, new_capacity * sizeof(php_poll_fd_entry), table->persistent); if (!new_entries) { return NULL; From 39cb2c90996d8c05c60d5fd8b15ef4aba8e6720e Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 21 Aug 2025 18:38:58 +0200 Subject: [PATCH 23/52] poll: add Windows build config changes --- ext/standard/config.w32 | 3 ++- win32/build/config.w32 | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/ext/standard/config.w32 b/ext/standard/config.w32 index c7c14b8705ca2..e20a2b105dfdc 100644 --- a/ext/standard/config.w32 +++ b/ext/standard/config.w32 @@ -35,7 +35,8 @@ EXTENSION("standard", "array.c base64.c basic_functions.c browscap.c \ url_scanner_ex.c ftp_fopen_wrapper.c http_fopen_wrapper.c \ php_fopen_wrapper.c credits.c css.c var_unserializer.c ftok.c sha1.c \ user_filters.c uuencode.c filters.c proc_open.c password.c \ - streamsfuncs.c http.c flock_compat.c hrtime.c", false /* never shared */, + stream_poll.c streamsfuncs.c http.c flock_compat.c hrtime.c", + false /* never shared */, '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1'); ADD_SOURCES("ext/standard/libavifinfo", "avifinfo.c", "standard"); PHP_STANDARD = "yes"; diff --git a/win32/build/config.w32 b/win32/build/config.w32 index 403f0aa6efbfe..b6409063c080f 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -298,6 +298,10 @@ AC_DEFINE('HAVE_STRNLEN', 1); AC_DEFINE('ZEND_CHECK_STACK_LIMIT', 1) +ADD_SOURCES("main/poll", "poll_backend_iocp.c poll_backend_select.c poll_core.c \ + poll_fd_table.c"); +ADD_FLAG("CFLAGS_BD_MAIN_POLL", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + ADD_SOURCES("main/streams", "streams.c cast.c memory.c filter.c plain_wrapper.c \ userspace.c transports.c xp_socket.c mmap.c glob_wrapper.c"); ADD_FLAG("CFLAGS_BD_MAIN_STREAMS", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); @@ -309,7 +313,7 @@ ADD_SOURCES("win32", "dllmain.c readdir.c \ ADD_FLAG("CFLAGS_BD_WIN32", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); -PHP_INSTALL_HEADERS("", "Zend/ TSRM/ main/ main/streams/ win32/"); +PHP_INSTALL_HEADERS("", "Zend/ TSRM/ main/ main/poll/ main/streams/ win32/"); PHP_INSTALL_HEADERS("Zend/Optimizer", "zend_call_graph.h zend_cfg.h zend_dfg.h zend_dump.h zend_func_info.h zend_inference.h zend_optimizer.h zend_ssa.h zend_worklist.h"); STDOUT.WriteBlankLines(1); From d697f96e63a78c75f96202cfea6382ed60408a5e Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 21 Aug 2025 19:26:09 +0200 Subject: [PATCH 24/52] poll: support poll creation by backend name --- ext/standard/basic_functions.stub.php | 2 +- ext/standard/basic_functions_arginfo.h | 4 +- ext/standard/stream_poll.c | 12 +++--- .../stream_poll_basic_sock_add_only.phpt | 6 --- main/php_poll.h | 1 + main/poll/php_poll_internal.h | 3 -- main/poll/poll_core.c | 40 +++++++++++++++++-- 7 files changed, 48 insertions(+), 20 deletions(-) diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index c2dcee282e5bf..a0ff89de99e9f 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -3386,7 +3386,7 @@ function soundex(string $string): string {} /* streamsfuncs.c */ -function stream_poll_create(int $backend = STREAM_POLL_BACKEND_AUTO): StreamPollContext {} +function stream_poll_create(int|string $backend = STREAM_POLL_BACKEND_AUTO): StreamPollContext {} /** @param resource $stream */ function stream_poll_add(StreamPollContext $poll_ctx, $stream, int $events, mixed $data = null): void {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index f7919d52a444d..d9ca0512615ae 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: e8c56e78a608a3dbdd13010e6f98f9a24426c2fb */ + * Stub hash: fcf29622f3ae629b790b19d4d753750974e332b3 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -1811,7 +1811,7 @@ ZEND_END_ARG_INFO() #define arginfo_soundex arginfo_base64_encode ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_stream_poll_create, 0, 0, StreamPollContext, 0) - ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, backend, IS_LONG, 0, "STREAM_POLL_BACKEND_AUTO") + ZEND_ARG_TYPE_MASK(0, backend, MAY_BE_LONG|MAY_BE_STRING, "STREAM_POLL_BACKEND_AUTO") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_poll_add, 0, 3, IS_VOID, 0) diff --git a/ext/standard/stream_poll.c b/ext/standard/stream_poll.c index 646afd8bb98bf..9f4660da6811e 100644 --- a/ext/standard/stream_poll.c +++ b/ext/standard/stream_poll.c @@ -98,22 +98,24 @@ static zend_object *php_stream_poll_context_create_object(zend_class_entry *ce) PHP_FUNCTION(stream_poll_create) { zend_long backend_long = PHP_POLL_BACKEND_AUTO; + zend_string *backend_str = NULL; php_poll_ctx *poll_ctx; ZEND_PARSE_PARAMETERS_START(0, 1) Z_PARAM_OPTIONAL - Z_PARAM_LONG(backend_long) + Z_PARAM_STR_OR_LONG(backend_str, backend_long) ZEND_PARSE_PARAMETERS_END(); - php_poll_backend_type backend = (php_poll_backend_type) backend_long; - - poll_ctx = php_poll_create(backend, false); + if (backend_str == NULL) { + poll_ctx = php_poll_create((php_poll_backend_type) backend_long, false); + } else { + poll_ctx = php_poll_create_by_name(ZSTR_VAL(backend_str), false); + } if (!poll_ctx) { zend_throw_exception( stream_poll_exception_class_entry, "Failed to create polling context", 0); RETURN_THROWS(); } - if (php_poll_init(poll_ctx) != SUCCESS) { php_poll_destroy(poll_ctx); zend_throw_exception( diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt index 026cf197f5ff2..149e43a472f9b 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt @@ -1,11 +1,5 @@ --TEST-- Stream polling basic functionality - only add ---SKIPIF-- - --FILE-- last_error = error; diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 440608bd3feb8..1398a99f798b7 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -80,7 +80,7 @@ PHPAPI void php_poll_register_backends(void) } /* Get backend operations */ -const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backend) +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 */ @@ -96,6 +96,22 @@ const php_poll_backend_ops *php_poll_get_backend_ops(php_poll_backend_type backe return NULL; } +/* Get backend operations by backend name */ +static const php_poll_backend_ops *php_poll_get_backend_ops_by_name(const char *backend_name) +{ + if (!backend_name) { + return NULL; + } + + for (int i = 0; i < num_registered_backends; i++) { + if (registered_backends[i] && strcmp(registered_backends[i]->name, backend_name) == 0) { + return registered_backends[i]; + } + } + + return NULL; +} + /* Create new poll context */ PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, bool persistent) { @@ -111,9 +127,27 @@ PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, bo pefree(ctx, persistent); return NULL; } - ctx->backend_type = preferred_backend; - ctx->max_events_hint = 0; /* No hint by default */ + + return ctx; +} + +/* Create new poll context */ +PHPAPI php_poll_ctx *php_poll_create_by_name(const char *preferred_backend, bool persistent) +{ + php_poll_ctx *ctx = php_poll_calloc(1, sizeof(php_poll_ctx), persistent); + if (!ctx) { + return NULL; + } + ctx->persistent = persistent; + + /* Get backend operations */ + ctx->backend_ops = php_poll_get_backend_ops_by_name(preferred_backend); + if (!ctx->backend_ops) { + pefree(ctx, persistent); + return NULL; + } + ctx->backend_type = ctx->backend_ops->type; return ctx; } From dffdc43fe66a46d99978d13fbff951b2e8a97958 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 21 Aug 2025 19:52:00 +0200 Subject: [PATCH 25/52] poll: clean up and update tests to allow backend selection --- ext/standard/tests/streams/stream_poll.inc | 6 ++++++ .../streams/stream_poll_basic_sock_add_only.phpt | 3 ++- .../streams/stream_poll_basic_sock_modify_write.phpt | 9 ++------- .../tests/streams/stream_poll_basic_sock_read.phpt | 11 +++-------- .../tests/streams/stream_poll_basic_sock_write.phpt | 11 +++-------- .../streams/stream_poll_basic_sock_write_read.phpt | 9 ++------- .../tests/streams/stream_poll_basic_wait_no_add.phpt | 9 ++------- 7 files changed, 20 insertions(+), 38 deletions(-) create mode 100644 ext/standard/tests/streams/stream_poll.inc diff --git a/ext/standard/tests/streams/stream_poll.inc b/ext/standard/tests/streams/stream_poll.inc new file mode 100644 index 0000000000000..6ccf8a09ca826 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll.inc @@ -0,0 +1,6 @@ + --FILE-- +Stream polling basic functionality - socket read --FILE-- +Stream polling basic functionality - socket write --FILE-- --FILE-- --FILE-- Date: Thu, 21 Aug 2025 21:08:20 +0200 Subject: [PATCH 26/52] poll: refactore, simplify and extend tests --- ext/standard/tests/streams/stream_poll.inc | 28 +++++++++- .../stream_poll_add_error_duplicite.phpt | 19 +++++++ .../stream_poll_backend_name_basic.phpt | 16 ++++++ .../stream_poll_basic_sock_add_only.phpt | 13 +---- .../stream_poll_basic_sock_modify_write.phpt | 32 ++--------- .../streams/stream_poll_basic_sock_read.phpt | 34 +++--------- .../stream_poll_basic_sock_remove_write.phpt | 25 +++++++++ .../streams/stream_poll_basic_sock_write.phpt | 39 +++----------- .../stream_poll_basic_sock_write_read.phpt | 54 +++---------------- .../stream_poll_basic_wait_no_add.phpt | 6 +-- 10 files changed, 118 insertions(+), 148 deletions(-) create mode 100644 ext/standard/tests/streams/stream_poll_add_error_duplicite.phpt create mode 100644 ext/standard/tests/streams/stream_poll_backend_name_basic.phpt create mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt diff --git a/ext/standard/tests/streams/stream_poll.inc b/ext/standard/tests/streams/stream_poll.inc index 6ccf8a09ca826..84fcd4a6e9400 100644 --- a/ext/standard/tests/streams/stream_poll.inc +++ b/ext/standard/tests/streams/stream_poll.inc @@ -1,6 +1,32 @@ $event) { + if (!$event instanceof StreamPollEvent) { + die('Invalid event type'); + } + echo "Event[$i]: " . $event->events . ", user data: " . $event->data; + if ($read_data && $event->events & STREAM_POLL_READ) { + $data = fread($event->stream, 1024); + echo ", read data: '$data'"; + } + echo "\n"; + } +} diff --git a/ext/standard/tests/streams/stream_poll_add_error_duplicite.phpt b/ext/standard/tests/streams/stream_poll_add_error_duplicite.phpt new file mode 100644 index 0000000000000..c2ff7a4011418 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_add_error_duplicite.phpt @@ -0,0 +1,19 @@ +--TEST-- +Stream polling - add duplicite error +--FILE-- +getMessage() . "\n"; +} +?> +--EXPECT-- +ERROR: Stream already added diff --git a/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt b/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt new file mode 100644 index 0000000000000..feee887c4627f --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt @@ -0,0 +1,16 @@ +--TEST-- +Stream polling basic functionality - backend name +--FILE-- + +--EXPECT-- +string(6) "select" +string(6) "select" diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt index 22e1b6a9f0efc..11a2164eb39ed 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_add_only.phpt @@ -4,20 +4,11 @@ Stream polling basic functionality - only add diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt index f645aa49a5e16..55d24e9d6be94 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt @@ -1,40 +1,18 @@ --TEST-- -Stream polling basic functionality +Stream polling basic functionality - socket modify write --FILE-- events . ", Data: " . $event->data . "\n"; - } -} - -// Clean up -fclose($socket1); -fclose($socket2); - +pt_print_events($events); ?> --EXPECT-- -bool(true) Events count: 1 -Event: bool(true) -Events: 2, Data: modified_data +Event[0]: 2, user data: modified_data diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt index fa7c3992558bd..678086d02b55d 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt @@ -4,36 +4,16 @@ Stream polling basic functionality - socket read events . ", Data: " . $event->data . "\n"; - } -} - -// Clean up -fclose($socket1); -fclose($socket2); +fwrite($socket1w, "test data"); +$events = stream_poll_wait($poll_ctx, 100); +pt_print_events($events, true); ?> --EXPECT-- -bool(true) Events count: 1 -Event: bool(true) -Events: 2, Data: socket_data +Event[0]: 1, user data: socket_data, read data: 'test data' diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt new file mode 100644 index 0000000000000..6a0b7e7f3047b --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt @@ -0,0 +1,25 @@ +--TEST-- +Stream polling basic functionality - socket delete write +--FILE-- + +--EXPECT-- +Events count: 2 +Event[0]: 2, user data: socket_data_1 +Event[1]: 2, user data: socket_data_2 +Events count: 1 +Event[0]: 2, user data: socket_data_2 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt index 6df02872f42bd..aacfea7789df9 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt @@ -4,40 +4,15 @@ Stream polling basic functionality - socket write events & STREAM_POLL_READ) { - echo "Read event detected on: " . $event->data . "\n"; - // Read the data - $data = fread($event->stream, 1024); - echo "Read data: '$data'\n"; - } - if ($event->events & STREAM_POLL_WRITE) { - echo "Write event detected on: " . $event->data . "\n"; - } -} - -// Clean up -fclose($socket1); -fclose($socket2); +stream_poll_add($poll_ctx, $socket1w, STREAM_POLL_WRITE, "socket_data"); +$events = stream_poll_wait($poll_ctx, 0); +pt_print_events($events); ?> --EXPECT-- -Events after write: 1 -Read event detected on: socket_data -Read data: 'test data' +Events count: 1 +Event[0]: 2, user data: socket_data diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt index dfb9c2a65f78f..156ecc7f49bc6 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt @@ -4,61 +4,23 @@ Stream polling basic functionality - socket write / read events . ", Data: " . $event->data . "\n"; - } -} +pt_print_events($events); fwrite($socket2, "test data"); $events = stream_poll_wait($poll_ctx, 100); // 100ms timeout -echo "Events after write: " . count($events) . "\n"; - -foreach ($events as $event) { - echo "Event: "; - if ($event instanceof StreamPollEvent) { - if ($event->events & STREAM_POLL_READ) { - echo "Read event detected on: " . $event->data . "\n"; - // Read the data - $data = fread($event->stream, 1024); - echo "Events: " . $event->events . ", Read data: '$data'\n"; - } - if ($event->events & STREAM_POLL_WRITE) { - echo "Write event detected on: " . $event->data . "\n"; - echo "Events: " . $event->events . "\n"; - } - } -} - -// Clean up -fclose($socket1); -fclose($socket2); - +pt_print_events($events, true); ?> --EXPECT-- -bool(true) Events count: 1 -Event: Events: 2, Data: socket2_data -Events after write: 2 -Event: Write event detected on: socket2_data -Events: 2 -Event: Read event detected on: socket1_data -Events: 1, Read data: 'test data' +Event[0]: 2, user data: socket2_data +Events count: 2 +Event[0]: 2, user data: socket2_data +Event[1]: 1, user data: socket1_data, read data: 'test data' diff --git a/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt b/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt index 74059b86d3a78..43036110f65df 100644 --- a/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt @@ -3,12 +3,10 @@ Stream polling basic functionality - only wait --FILE-- --EXPECT-- -bool(true) Events count: 0 From 64ad95cef0740dda3186c9a8ab5d92f87400867b Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 22 Aug 2025 12:21:09 +0200 Subject: [PATCH 27/52] poll: fix kqueue removal logic --- main/poll/poll_backend_kqueue.c | 47 +++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 466c701b3eb07..f9540d4fdd72c 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -195,26 +195,39 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve 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 changes[2]; - EV_SET(&changes[0], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); - EV_SET(&changes[1], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); - - int result = kevent(backend_data->kqueue_fd, changes, 2, NULL, 0, NULL); - if (result == -1 && errno != ENOENT) { - switch (errno) { - case EBADF: - case EINVAL: - php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); - break; - default: - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); - break; + struct kevent change; + + /* Remove read first */ + EV_SET(&change, fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); + int result = kevent(backend_data->kqueue_fd, &change, 1, NULL, 0, NULL); + /* Check if read failed because the (fd, event) combination did not exist */ + bool read_noent = (result == -1 && errno == ENOENT); + if (result == 0 || read_noent) { + /* Remove write if there was no error other than noent */ + EV_SET(&change, fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); + result = kevent(backend_data->kqueue_fd, &change, 1, NULL, 0, NULL); + /* If both read and write failed because not found, then fail */ + if (result == -1 && errno == ENOENT && read_noent) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); + return FAILURE; } - return FAILURE; } - return SUCCESS; + if (result == 0) { + return SUCCESS; + } + + switch (errno) { + case EBADF: + case EINVAL: + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; + default: + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; + } + + return FAILURE; } static int kqueue_backend_wait( From d4f7164c33b14327139763aaae4db927bde5227b Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 22 Aug 2025 15:26:09 +0200 Subject: [PATCH 28/52] poll: check stream map before deleting for better error --- ext/standard/stream_poll.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ext/standard/stream_poll.c b/ext/standard/stream_poll.c index 9f4660da6811e..2138a583e4776 100644 --- a/ext/standard/stream_poll.c +++ b/ext/standard/stream_poll.c @@ -283,6 +283,11 @@ PHP_FUNCTION(stream_poll_remove) RETURN_THROWS(); } + if (!zend_hash_index_exists(context->stream_map, (zend_ulong) fd)) { + zend_throw_exception(stream_poll_exception_class_entry, "Stream not found", 0); + RETURN_THROWS(); + } + /* Remove from poll context */ if (php_poll_remove(context->ctx, (int) fd) != SUCCESS) { zend_throw_exception( From 9c08f0b291d0a8468255ccf8f744194a2196376a Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 22 Aug 2025 15:26:29 +0200 Subject: [PATCH 29/52] poll: extend and simplify tests --- .../stream_poll_basic_sock_modify_write.phpt | 3 +- .../streams/stream_poll_basic_sock_read.phpt | 3 +- .../stream_poll_basic_sock_remove_write.phpt | 20 ++++++-- .../stream_poll_basic_sock_rw_multi_edge.phpt | 42 +++++++++++++++++ ...stream_poll_basic_sock_rw_multi_level.phpt | 47 +++++++++++++++++++ ...tream_poll_basic_sock_rw_single_level.phpt | 23 +++++++++ .../streams/stream_poll_basic_sock_write.phpt | 3 +- .../stream_poll_basic_sock_write_read.phpt | 26 ---------- .../stream_poll_modify_error_not_found.phpt | 17 +++++++ .../stream_poll_remove_error_not_found.phpt | 17 +++++++ 10 files changed, 165 insertions(+), 36 deletions(-) create mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt create mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt create mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt delete mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt create mode 100644 ext/standard/tests/streams/stream_poll_modify_error_not_found.phpt create mode 100644 ext/standard/tests/streams/stream_poll_remove_error_not_found.phpt diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt index 55d24e9d6be94..ac47593bdd4ee 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt @@ -10,8 +10,7 @@ $poll_ctx = pt_new_stream_poll(); stream_poll_add($poll_ctx, $socket2, STREAM_POLL_WRITE, "socket_data"); stream_poll_modify($poll_ctx, $socket2, STREAM_POLL_WRITE, "modified_data"); -$events = stream_poll_wait($poll_ctx, 0); -pt_print_events($events); +pt_print_events(stream_poll_wait($poll_ctx, 0)); ?> --EXPECT-- Events count: 1 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt index 678086d02b55d..2585223c1181d 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt @@ -10,8 +10,7 @@ $poll_ctx = pt_new_stream_poll(); stream_poll_add($poll_ctx, $socket1r, STREAM_POLL_READ, "socket_data"); fwrite($socket1w, "test data"); -$events = stream_poll_wait($poll_ctx, 100); -pt_print_events($events, true); +pt_print_events(stream_poll_wait($poll_ctx, 100), true); ?> --EXPECT-- diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt index 6a0b7e7f3047b..991197b7eb69b 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt @@ -11,11 +11,18 @@ $poll_ctx = pt_new_stream_poll(); stream_poll_add($poll_ctx, $socket1w, STREAM_POLL_WRITE, "socket_data_1"); stream_poll_add($poll_ctx, $socket2w, STREAM_POLL_WRITE, "socket_data_2"); -$events = stream_poll_wait($poll_ctx, 0); -pt_print_events($events); +pt_print_events(stream_poll_wait($poll_ctx, 0)); + stream_poll_remove($poll_ctx, $socket1w); -$events = stream_poll_wait($poll_ctx, 0); -pt_print_events($events); + +pt_print_events(stream_poll_wait($poll_ctx, 0)); + +// 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 count: 2 @@ -23,3 +30,8 @@ Event[0]: 2, user data: socket_data_1 Event[1]: 2, user data: socket_data_2 Events count: 1 Event[0]: 2, user data: socket_data_2 +int(6) +int(6) +string(6) "test 1" +string(6) "test 2" + diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt new file mode 100644 index 0000000000000..2f54599f66d3d --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt @@ -0,0 +1,42 @@ +--TEST-- +Stream polling basic functionality - socket write / read +--FILE-- + +--EXPECT-- +Events count: 1 +Event[0]: 2, user data: socket2_data +Events count: 0 +Events count: 1 +Event[0]: 1, user data: socket1_data, read data: 'test data' +Events count: 2 +Event[0]: 2, user data: socket2_data +Event[1]: 1, user data: socket1_data +Events count: 0 +Events count: 1 +Event[0]: 1, user data: socket1_data, read data: 'more data and even more data' +Events count: 1 +Event[0]: 10, user data: socket2_data +Events count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt new file mode 100644 index 0000000000000..11a5b8a2d202c --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt @@ -0,0 +1,47 @@ +--TEST-- +Stream polling basic functionality - socket write / read +--FILE-- + +--EXPECT-- +Events count: 1 +Event[0]: 2, user data: socket2_data +Events count: 1 +Event[0]: 2, user data: socket2_data +Events count: 2 +Event[0]: 2, user data: socket2_data +Event[1]: 1, user data: socket1_data, read data: 'test data' +Events count: 2 +Event[0]: 2, user data: socket2_data +Event[1]: 1, user data: socket1_data +Events count: 2 +Event[0]: 2, user data: socket2_data +Event[1]: 1, user data: socket1_data +Events count: 2 +Event[0]: 2, user data: socket2_data +Event[1]: 1, user data: socket1_data, read data: 'more data and even more data' +Events count: 1 +Event[0]: 10, user data: socket2_data +Events count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt new file mode 100644 index 0000000000000..4f3bb2ae5329d --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt @@ -0,0 +1,23 @@ +--TEST-- +Stream polling basic functionality - socket write / read +--FILE-- + +--EXPECT-- +Events count: 1 +Event[0]: 2, user data: socket2_data +Events count: 2 +Event[0]: 2, user data: socket2_data +Event[1]: 1, user data: socket1_data, read data: 'test data' diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt index aacfea7789df9..8a88aca41ddad 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt @@ -9,8 +9,7 @@ $poll_ctx = pt_new_stream_poll(); stream_poll_add($poll_ctx, $socket1w, STREAM_POLL_WRITE, "socket_data"); -$events = stream_poll_wait($poll_ctx, 0); -pt_print_events($events); +pt_print_events(stream_poll_wait($poll_ctx, 0)); ?> --EXPECT-- diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt deleted file mode 100644 index 156ecc7f49bc6..0000000000000 --- a/ext/standard/tests/streams/stream_poll_basic_sock_write_read.phpt +++ /dev/null @@ -1,26 +0,0 @@ ---TEST-- -Stream polling basic functionality - socket write / read ---FILE-- - ---EXPECT-- -Events count: 1 -Event[0]: 2, user data: socket2_data -Events count: 2 -Event[0]: 2, user data: socket2_data -Event[1]: 1, user data: socket1_data, read data: 'test data' diff --git a/ext/standard/tests/streams/stream_poll_modify_error_not_found.phpt b/ext/standard/tests/streams/stream_poll_modify_error_not_found.phpt new file mode 100644 index 0000000000000..04cc0fee4c80d --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_modify_error_not_found.phpt @@ -0,0 +1,17 @@ +--TEST-- +Stream polling - modify not found error +--FILE-- +getMessage() . "\n"; +} +?> +--EXPECT-- +ERROR: Stream not found diff --git a/ext/standard/tests/streams/stream_poll_remove_error_not_found.phpt b/ext/standard/tests/streams/stream_poll_remove_error_not_found.phpt new file mode 100644 index 0000000000000..b03fd08b8b299 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_remove_error_not_found.phpt @@ -0,0 +1,17 @@ +--TEST-- +Stream polling - remove not found error +--FILE-- +getMessage() . "\n"; +} +?> +--EXPECT-- +ERROR: Stream not found From b2cb12266b58649d19bb1c0c956198b6bdde35eb Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 22 Aug 2025 17:54:03 +0200 Subject: [PATCH 30/52] poll: add suitable max events callback to get right number of events to wait --- ext/standard/stream_poll.c | 8 ++- main/php_poll.h | 3 + main/poll/php_poll_internal.h | 3 + main/poll/poll_backend_epoll.c | 24 ++++++++ main/poll/poll_backend_eventport.c | 21 +++++++ main/poll/poll_backend_iocp.c | 22 +++++++ main/poll/poll_backend_kqueue.c | 96 ++++++++++++++++++++++-------- main/poll/poll_backend_poll.c | 19 ++++++ main/poll/poll_backend_select.c | 20 +++++++ main/poll/poll_core.c | 12 +++- 10 files changed, 199 insertions(+), 29 deletions(-) diff --git a/ext/standard/stream_poll.c b/ext/standard/stream_poll.c index 2138a583e4776..1f7337c2ef70e 100644 --- a/ext/standard/stream_poll.c +++ b/ext/standard/stream_poll.c @@ -323,8 +323,12 @@ PHP_FUNCTION(stream_poll_wait) } if (max_events <= 0) { - // TODO: get some recommended value from polling api basend on number of added events - max_events = 1024; + /* Get suitable value from the polling backend */ + max_events = php_poll_get_suitable_max_events(context->ctx); + if (max_events <= 0) { + /* This should not happen but use fallback just in case */ + max_events = 64; + } } events = emalloc(sizeof(php_poll_event) * max_events); diff --git a/main/php_poll.h b/main/php_poll.h index 74fc0f2d7244e..a1de289ce03c4 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -84,6 +84,9 @@ 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); diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h index a1958593598d6..fe0e8438e6b99 100644 --- a/main/poll/php_poll_internal.h +++ b/main/poll/php_poll_internal.h @@ -50,6 +50,9 @@ typedef struct php_poll_backend_ops { /* 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; diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c index c027a4c299854..5e523e41f4bb3 100644 --- a/main/poll/poll_backend_epoll.c +++ b/main/poll/poll_backend_epoll.c @@ -22,6 +22,7 @@ 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) @@ -127,6 +128,7 @@ static zend_result epoll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, php_poll_set_error(ctx, (errno == EEXIST) ? PHP_POLL_ERR_EXISTS : PHP_POLL_ERR_SYSTEM); return FAILURE; } + backend_data->fd_count++; return SUCCESS; } @@ -155,6 +157,7 @@ static zend_result epoll_backend_remove(php_poll_ctx *ctx, int fd) php_poll_set_error(ctx, (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERR_SYSTEM); return FAILURE; } + backend_data->fd_count--; return SUCCESS; } @@ -190,6 +193,26 @@ static int epoll_backend_wait( 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); @@ -210,6 +233,7 @@ const php_poll_backend_ops php_poll_backend_epoll_ops = { .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, }; diff --git a/main/poll/poll_backend_eventport.c b/main/poll/poll_backend_eventport.c index 177b9d9f101a5..d1db4cec3c0a3 100644 --- a/main/poll/poll_backend_eventport.c +++ b/main/poll/poll_backend_eventport.c @@ -386,6 +386,26 @@ static bool eventport_backend_is_available(void) return false; } +static int eventport_backend_get_suitable_max_events(php_poll_ctx *ctx) +{ + eventport_backend_data_t *backend_data = (eventport_backend_data_t *) ctx->backend_data; + + if (!backend_data || !backend_data->fd_table) { + return -1; + } + + /* For event ports, we track exactly how many FD associations are active */ + int active_associations = backend_data->active_associations; + + if (active_associations == 0) { + return 1; + } + + /* Event ports can return exactly one event per association, + * so the suitable max_events is exactly the number of active associations */ + return active_associations; +} + /* Event port backend operations structure */ const php_poll_backend_ops php_poll_backend_eventport_ops = { .type = PHP_POLL_BACKEND_EVENTPORT, @@ -397,6 +417,7 @@ const php_poll_backend_ops php_poll_backend_eventport_ops = { .remove = eventport_backend_remove, .wait = eventport_backend_wait, .is_available = eventport_backend_is_available, + .get_suitable_max_events = eventport_backend_get_suitable_max_events, .supports_et = true /* Supports both level and edge triggering */ }; diff --git a/main/poll/poll_backend_iocp.c b/main/poll/poll_backend_iocp.c index 6762875ae75c7..96327fd0f63c2 100644 --- a/main/poll/poll_backend_iocp.c +++ b/main/poll/poll_backend_iocp.c @@ -356,6 +356,27 @@ static bool iocp_backend_is_available(void) return true; } +static int iocp_backend_get_suitable_max_events(php_poll_ctx *ctx) +{ + iocp_backend_data_t *backend_data = (iocp_backend_data_t *) ctx->backend_data; + + if (!backend_data) { + return -1; + } + + /* For IOCP, we track exactly how many FDs are registered */ + int active_fds = backend_data->fd_count; + + if (active_fds == 0) { + return 1; + } + + /* IOCP can potentially return multiple completions per socket, + * but typically it's one completion per operation. + * Since we're simulating polling behavior, use the FD count directly. */ + return active_fds; +} + const php_poll_backend_ops php_poll_backend_iocp_ops = { .type = PHP_POLL_BACKEND_IOCP, .name = "iocp", @@ -366,6 +387,7 @@ const php_poll_backend_ops php_poll_backend_iocp_ops = { .remove = iocp_backend_remove, .wait = iocp_backend_wait, .is_available = iocp_backend_is_available, + .get_suitable_max_events = iocp_backend_get_suitable_max_events, .supports_et = true /* IOCP provides completion-based model which is naturally edge-triggered */ }; diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index f9540d4fdd72c..c9bde2010272f 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -24,6 +24,7 @@ typedef struct { int kqueue_fd; struct kevent *events; int events_capacity; + int filter_count; } kqueue_backend_data_t; static zend_result kqueue_backend_init(php_poll_ctx *ctx) @@ -115,6 +116,7 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events } return FAILURE; } + backend_data->filter_count += change_count; } return SUCCESS; @@ -128,6 +130,7 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve 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) { @@ -157,13 +160,17 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve add_count++; } - /* Delete existing filters (ignore ENOENT errors) */ - if (delete_count > 0) { - int result = kevent(backend_data->kqueue_fd, deletes, delete_count, NULL, 0, NULL); - if (result == -1 && errno != ENOENT) { + //* 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 (errno != ENOENT) { + /* Real error (not just "doesn't exist") */ php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); return FAILURE; } + /* ENOENT is ignored - filter didn't exist */ } /* Add new filters */ @@ -189,45 +196,61 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve } } + backend_data->filter_count = backend_data->filter_count - successful_deletes + add_count; + 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; - /* Remove read first */ + /* 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); - /* Check if read failed because the (fd, event) combination did not exist */ - bool read_noent = (result == -1 && errno == ENOENT); - if (result == 0 || read_noent) { - /* Remove write if there was no error other than noent */ - EV_SET(&change, fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); - result = kevent(backend_data->kqueue_fd, &change, 1, NULL, 0, NULL); - /* If both read and write failed because not found, then fail */ - if (result == -1 && errno == ENOENT && read_noent) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); - return FAILURE; + if (result == 0) { + successful_deletes++; + } else if (errno != ENOENT) { + switch (errno) { + case EBADF: + case EINVAL: + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; + default: + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; } + return FAILURE; } + /* 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) { - return SUCCESS; + successful_deletes++; + } else if (errno != ENOENT) { + switch (errno) { + case EBADF: + case EINVAL: + php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); + break; + default: + php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + break; + } + return FAILURE; } - switch (errno) { - case EBADF: - case EINVAL: - php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); - break; - default: - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); - break; + /* 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; } - return FAILURE; + backend_data->filter_count -= successful_deletes; + + return SUCCESS; } static int kqueue_backend_wait( @@ -291,6 +314,26 @@ static bool kqueue_backend_is_available(void) return false; } +static int kqueue_backend_get_suitable_max_events(php_poll_ctx *ctx) +{ + kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; + + if (!backend_data) { + return -1; + } + + /* For kqueue, we now track exactly how many filters are registered */ + int active_filters = backend_data->filter_count; + + if (active_filters == 0) { + return 1; + } + + /* Kqueue can return exactly one event per registered filter, + * so the suitable max_events is exactly the number of registered filters */ + return active_filters; +} + const php_poll_backend_ops php_poll_backend_kqueue_ops = { .type = PHP_POLL_BACKEND_KQUEUE, .name = "kqueue", @@ -301,6 +344,7 @@ const php_poll_backend_ops php_poll_backend_kqueue_ops = { .remove = kqueue_backend_remove, .wait = kqueue_backend_wait, .is_available = kqueue_backend_is_available, + .get_suitable_max_events = kqueue_backend_get_suitable_max_events, .supports_et = true /* kqueue supports EV_CLEAR for edge triggering */ }; diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 06c2f41f9ceee..051cfec50616b 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -308,6 +308,24 @@ static bool poll_backend_is_available(void) return true; /* poll() is always available */ } +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; + } + + /* For poll(), we know exactly how many FDs are registered */ + int active_fds = backend_data->fds_used; + + if (active_fds == 0) { + return 1; + } + + return active_fds; +} + const php_poll_backend_ops php_poll_backend_poll_ops = { .type = PHP_POLL_BACKEND_POLL, .name = "poll", @@ -318,6 +336,7 @@ const php_poll_backend_ops php_poll_backend_poll_ops = { .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, /* poll() doesn't support ET natively, but we simulate it */ }; diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c index 058d8896669c6..c1e9de329e57e 100644 --- a/main/poll/poll_backend_select.c +++ b/main/poll/poll_backend_select.c @@ -298,6 +298,25 @@ static bool select_backend_is_available(void) return true; /* select() is always available */ } +static int select_backend_get_suitable_max_events(php_poll_ctx *ctx) +{ + select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; + + if (UNEXPECTED(!backend_data || !backend_data->fd_table)) { + return -1; + } + + /* For select(), we know exactly how many FDs are registered */ + int active_fds = backend_data->fd_table->count; + + if (active_fds == 0) { + /* No active FDs, return 1 just to not pass empty events */ + return 1; + } + + return active_fds; +} + const php_poll_backend_ops php_poll_backend_select_ops = { .type = PHP_POLL_BACKEND_SELECT, .name = "select", @@ -308,5 +327,6 @@ const php_poll_backend_ops php_poll_backend_select_ops = { .remove = select_backend_remove, .wait = select_backend_wait, .is_available = select_backend_is_available, + .get_suitable_max_events = select_backend_get_suitable_max_events, .supports_et = false /* select() doesn't support ET natively, but we simulate it */ }; diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 1398a99f798b7..d24da08d04de5 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -295,4 +295,14 @@ PHPAPI bool php_poll_supports_et(php_poll_ctx *ctx) PHPAPI php_poll_error php_poll_get_error(php_poll_ctx *ctx) { return ctx ? ctx->last_error : PHP_POLL_ERR_INVALID; -} \ No newline at end of file +} + +/* 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); +} From c5822f912501095081f7cd9e4dbaae3bb6a6bfc9 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 22 Aug 2025 19:30:44 +0200 Subject: [PATCH 31/52] poll: group kqueue events and correctly count max events --- ext/standard/tests/streams/stream_poll.inc | 29 ++++- ...stream_poll_basic_tcp_rw_single_level.phpt | 25 +++++ main/poll/poll_backend_kqueue.c | 105 +++++++++++++----- 3 files changed, 133 insertions(+), 26 deletions(-) create mode 100644 ext/standard/tests/streams/stream_poll_basic_tcp_rw_single_level.phpt diff --git a/ext/standard/tests/streams/stream_poll.inc b/ext/standard/tests/streams/stream_poll.inc index 84fcd4a6e9400..d182c5dbe01c4 100644 --- a/ext/standard/tests/streams/stream_poll.inc +++ b/ext/standard/tests/streams/stream_poll.inc @@ -1,13 +1,40 @@ +--EXPECT-- +Events count: 2 +Event[0]: 2, user data: client_data +Event[1]: 2, user data: server_data +Events count: 2 +Event[0]: 2, user data: client_data +Event[1]: 3, user data: server_data, read data: 'test data' diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index c9bde2010272f..09893fa3702bf 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -24,7 +24,7 @@ typedef struct { int kqueue_fd; struct kevent *events; int events_capacity; - int filter_count; + int fd_count; /* Track number of unique FDs (not individual filters) */ } kqueue_backend_data_t; static zend_result kqueue_backend_init(php_poll_ctx *ctx) @@ -116,7 +116,8 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events } return FAILURE; } - backend_data->filter_count += change_count; + + backend_data->fd_count++; } return SUCCESS; @@ -160,7 +161,7 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve add_count++; } - //* Delete existing filters individually to count successes */ + /* 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) { @@ -196,10 +197,14 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve } } - backend_data->filter_count = backend_data->filter_count - successful_deletes + add_count; + /* If we're adding filters to a previously empty FD, increment */ + else if (successful_deletes == 0 && add_count > 0) { + backend_data->fd_count++; + } 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; @@ -248,7 +253,7 @@ static zend_result kqueue_backend_remove(php_poll_ctx *ctx, int fd) return FAILURE; } - backend_data->filter_count -= successful_deletes; + backend_data->fd_count--; return SUCCESS; } @@ -258,16 +263,19 @@ static int kqueue_backend_wait( { kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; - /* Ensure we have enough space for the requested events */ - if (max_events > backend_data->events_capacity) { + /* Ensure we have enough space for the requested events. + * Since kqueue can return up to 2 raw events per FD (read + write), + * we need capacity for potentially 2x max_events. */ + int required_capacity = max_events * 2; + if (required_capacity > backend_data->events_capacity) { struct kevent *new_events = php_poll_realloc( - backend_data->events, max_events * sizeof(struct kevent), ctx->persistent); + 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 = max_events; + backend_data->events_capacity = required_capacity; } struct timespec ts = { 0 }, *tsp = NULL; @@ -277,28 +285,77 @@ static int kqueue_backend_wait( tsp = &ts; } - int nfds = kevent(backend_data->kqueue_fd, NULL, 0, backend_data->events, max_events, tsp); + int nfds = kevent( + backend_data->kqueue_fd, NULL, 0, backend_data->events, required_capacity, tsp); if (nfds > 0) { + /* Group events by FD and combine read/write events */ + int unique_events = 0; + int oneshot_fds = 0; /* Count FDs that are completely oneshot */ + for (int i = 0; i < nfds; i++) { - events[i].fd = (int) backend_data->events[i].ident; - events[i].events = 0; /* Not used in results */ - events[i].data = backend_data->events[i].udata; - events[i].revents = 0; + int 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; if (backend_data->events[i].filter == EVFILT_READ) { - events[i].revents |= PHP_POLL_READ; + revents |= PHP_POLL_READ; } else if (backend_data->events[i].filter == EVFILT_WRITE) { - events[i].revents |= PHP_POLL_WRITE; + revents |= PHP_POLL_WRITE; } if (backend_data->events[i].flags & EV_EOF) { - events[i].revents |= PHP_POLL_HUP; + revents |= PHP_POLL_HUP; } if (backend_data->events[i].flags & EV_ERROR) { - events[i].revents |= PHP_POLL_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; + + /* Handle oneshot logic: if existing event was oneshot but current isn't, + * or vice versa, then the combined event is not oneshot */ + if (events[j].events & PHP_POLL_ONESHOT) { + if (!is_oneshot) { + /* Previously oneshot, now not oneshot - remove oneshot flag and + * decrement counter */ + events[j].events &= ~PHP_POLL_ONESHOT; + oneshot_fds--; + } + } + /* If existing wasn't oneshot and current is oneshot, keep it not oneshot */ + + 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 = is_oneshot ? PHP_POLL_ONESHOT : 0; + events[unique_events].revents = revents; + events[unique_events].data = data; + + if (is_oneshot) { + oneshot_fds++; + } + + unique_events++; } } + + /* Adjust fd_count by subtracting FDs that were completely oneshot */ + backend_data->fd_count -= oneshot_fds; + + return unique_events; } return nfds; @@ -322,16 +379,14 @@ static int kqueue_backend_get_suitable_max_events(php_poll_ctx *ctx) return -1; } - /* For kqueue, we now track exactly how many filters are registered */ - int active_filters = backend_data->filter_count; - - if (active_filters == 0) { + int active_fds = backend_data->fd_count; + if (active_fds == 0) { return 1; } - /* Kqueue can return exactly one event per registered filter, - * so the suitable max_events is exactly the number of registered filters */ - return active_filters; + /* Kqueue will return one combined event per FD (like epoll), + * so the suitable max_events is exactly the number of registered FDs */ + return active_fds; } const php_poll_backend_ops php_poll_backend_kqueue_ops = { From 51a07625cead88ab4bff0ea38bcf221456dc9e7e Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 22 Aug 2025 21:17:11 +0200 Subject: [PATCH 32/52] poll: make the kqueue one shot logic consistent with epoll It needs to track complete one shots and remove same fd filters --- ext/standard/tests/streams/stream_poll.inc | 6 + .../stream_poll_basic_tcp_rw_one_shot.phpt | 25 ++++ main/poll/poll_backend_kqueue.c | 113 +++++++++++++----- 3 files changed, 115 insertions(+), 29 deletions(-) create mode 100644 ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt diff --git a/ext/standard/tests/streams/stream_poll.inc b/ext/standard/tests/streams/stream_poll.inc index d182c5dbe01c4..30f5e1fcb5add 100644 --- a/ext/standard/tests/streams/stream_poll.inc +++ b/ext/standard/tests/streams/stream_poll.inc @@ -40,6 +40,12 @@ function pt_new_stream_poll(): StreamPollContext { return stream_poll_create($backend === false ? STREAM_POLL_BACKEND_AUTO : $backend); } +function pt_write_sleep($stream, $data, $delay = 10000): int|false { + $result = fwrite($stream, $data, $delay); + usleep($delay); + return $result; +} + function pt_print_events($events, $read_data = false): void { if (!is_array($events)) { die("Events must be an array\n"); diff --git a/ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt b/ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt new file mode 100644 index 0000000000000..16d562247d19a --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt @@ -0,0 +1,25 @@ +--TEST-- +Stream polling - TCP read write level +--FILE-- + +--EXPECT-- +Events count: 2 +Event[0]: 2, user data: client_data +Event[1]: 2, user data: server_data +Events count: 0 +Events count: 0 diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 09893fa3702bf..82c4ec277fb3a 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -25,6 +25,7 @@ typedef struct { struct kevent *events; int events_capacity; int fd_count; /* Track number of unique FDs (not individual filters) */ + HashTable *complete_oneshot_fds; /* Track FDs with both read+write oneshot */ } kqueue_backend_data_t; static zend_result kqueue_backend_init(php_poll_ctx *ctx) @@ -53,6 +54,11 @@ static zend_result kqueue_backend_init(php_poll_ctx *ctx) return FAILURE; } data->events_capacity = initial_capacity; + data->fd_count = 0; /* Initialize FD counter */ + + /* Initialize complete oneshot tracking */ + data->complete_oneshot_fds = emalloc(sizeof(HashTable)); + zend_hash_init(data->complete_oneshot_fds, 8, NULL, NULL, ctx->persistent); ctx->backend_data = data; return SUCCESS; @@ -66,6 +72,10 @@ static void kqueue_backend_cleanup(php_poll_ctx *ctx) close(data->kqueue_fd); } pefree(data->events, ctx->persistent); + if (data->complete_oneshot_fds) { + zend_hash_destroy(data->complete_oneshot_fds); + efree(data->complete_oneshot_fds); + } pefree(data, ctx->persistent); ctx->backend_data = NULL; } @@ -117,7 +127,14 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events return FAILURE; } + /* Increment FD count only once per unique FD */ backend_data->fd_count++; + + /* Track if this FD has both read+write oneshot */ + if ((events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT)) + == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT)) { + zend_hash_index_add_empty_element(backend_data->complete_oneshot_fds, fd); + } } return SUCCESS; @@ -197,9 +214,21 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve } } - /* If we're adding filters to a previously empty FD, increment */ - else if (successful_deletes == 0 && add_count > 0) { + /* Update FD count and oneshot tracking */ + if (successful_deletes > 0 && add_count == 0) { + /* Removed all filters - FD is gone */ + backend_data->fd_count--; + zend_hash_index_del(backend_data->complete_oneshot_fds, fd); + } else if (successful_deletes == 0 && add_count > 0) { + /* Added filters to previously empty FD */ backend_data->fd_count++; + if ((events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT)) + == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT)) { + zend_hash_index_add_empty_element(backend_data->complete_oneshot_fds, fd); + } + } else if (successful_deletes > 0 || add_count > 0) { + /* Modified existing filters - remove from oneshot tracking */ + zend_hash_index_del(backend_data->complete_oneshot_fds, fd); } return SUCCESS; @@ -208,15 +237,16 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve 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; + struct kevent changes[2]; 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); + EV_SET(&changes[0], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); + int result = kevent(backend_data->kqueue_fd, &changes[0], 1, NULL, 0, NULL); if (result == 0) { successful_deletes++; } else if (errno != ENOENT) { + /* Real error */ switch (errno) { case EBADF: case EINVAL: @@ -230,11 +260,12 @@ static zend_result kqueue_backend_remove(php_poll_ctx *ctx, int fd) } /* 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); + EV_SET(&changes[1], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); + result = kevent(backend_data->kqueue_fd, &changes[1], 1, NULL, 0, NULL); if (result == 0) { successful_deletes++; } else if (errno != ENOENT) { + /* Real error */ switch (errno) { case EBADF: case EINVAL: @@ -253,8 +284,12 @@ static zend_result kqueue_backend_remove(php_poll_ctx *ctx, int fd) return FAILURE; } + /* Update FD count - we removed all filters for this FD */ backend_data->fd_count--; + /* Remove from complete oneshot tracking */ + zend_hash_index_del(backend_data->complete_oneshot_fds, fd); + return SUCCESS; } @@ -291,7 +326,8 @@ static int kqueue_backend_wait( if (nfds > 0) { /* Group events by FD and combine read/write events */ int unique_events = 0; - int oneshot_fds = 0; /* Count FDs that are completely oneshot */ + HashTable processed_complete_oneshot; /* Track which complete oneshot FDs we've processed */ + zend_hash_init(&processed_complete_oneshot, 8, NULL, NULL, 0); for (int i = 0; i < nfds; i++) { int fd = (int) backend_data->events[i].ident; @@ -299,6 +335,7 @@ static int kqueue_backend_wait( 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) { @@ -318,19 +355,6 @@ static int kqueue_backend_wait( if (events[j].fd == fd) { /* Combine with existing event */ events[j].revents |= revents; - - /* Handle oneshot logic: if existing event was oneshot but current isn't, - * or vice versa, then the combined event is not oneshot */ - if (events[j].events & PHP_POLL_ONESHOT) { - if (!is_oneshot) { - /* Previously oneshot, now not oneshot - remove oneshot flag and - * decrement counter */ - events[j].events &= ~PHP_POLL_ONESHOT; - oneshot_fds--; - } - } - /* If existing wasn't oneshot and current is oneshot, keep it not oneshot */ - found = true; break; } @@ -340,21 +364,50 @@ static int kqueue_backend_wait( /* New FD, create new event */ ZEND_ASSERT(unique_events < max_events); events[unique_events].fd = fd; - events[unique_events].events = is_oneshot ? PHP_POLL_ONESHOT : 0; + events[unique_events].events + = 0; /* Will be set below based on complete oneshot logic */ events[unique_events].revents = revents; events[unique_events].data = data; + unique_events++; + } - if (is_oneshot) { - oneshot_fds++; - } + /* Handle complete oneshot logic */ + if (is_oneshot && zend_hash_index_exists(backend_data->complete_oneshot_fds, fd)) { + /* This FD was registered with both read+write oneshot */ + if (!zend_hash_index_exists(&processed_complete_oneshot, fd)) { + /* Mark as processed so we only handle once */ + zend_hash_index_add_empty_element(&processed_complete_oneshot, fd); + + /* Since this FD had both read+write oneshot, and at least one fired, + * we need to remove ALL remaining filters to simulate epoll behavior */ + struct kevent cleanup_changes[2]; + + /* Try to remove both read and write filters */ + EV_SET(&cleanup_changes[0], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); + EV_SET(&cleanup_changes[1], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); + + /* Execute cleanup (ignore ENOENT - some filters already auto-removed) */ + for (int c = 0; c < 2; c++) { + kevent(backend_data->kqueue_fd, &cleanup_changes[c], 1, NULL, 0, NULL); + /* Ignore errors - filters might already be gone */ + } - unique_events++; + /* This FD is now completely removed */ + backend_data->fd_count--; + zend_hash_index_del(backend_data->complete_oneshot_fds, fd); + + /* Mark the combined event as oneshot */ + for (int j = 0; j < unique_events; j++) { + if (events[j].fd == fd) { + events[j].events = PHP_POLL_ONESHOT; + break; + } + } + } } } - /* Adjust fd_count by subtracting FDs that were completely oneshot */ - backend_data->fd_count -= oneshot_fds; - + zend_hash_destroy(&processed_complete_oneshot); return unique_events; } @@ -379,7 +432,9 @@ static int kqueue_backend_get_suitable_max_events(php_poll_ctx *ctx) return -1; } + /* For kqueue, we now track exactly how many unique FDs are registered */ int active_fds = backend_data->fd_count; + if (active_fds == 0) { return 1; } From ac4e94a511f6e614dc692515c0b1c893d99131bf Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 23 Aug 2025 12:30:53 +0200 Subject: [PATCH 33/52] poll: refactore and simplify oneshot kqueue logic --- main/poll/poll_backend_kqueue.c | 83 +++++++++++++-------------------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index 82c4ec277fb3a..bd537128e3f6a 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -26,6 +26,7 @@ typedef struct { int events_capacity; int fd_count; /* Track number of unique FDs (not individual filters) */ HashTable *complete_oneshot_fds; /* Track FDs with both read+write oneshot */ + HashTable *garbage_oneshot_fds; /* Pre-cached hash table for FDs to delete */ } kqueue_backend_data_t; static zend_result kqueue_backend_init(php_poll_ctx *ctx) @@ -56,9 +57,11 @@ static zend_result kqueue_backend_init(php_poll_ctx *ctx) data->events_capacity = initial_capacity; data->fd_count = 0; /* Initialize FD counter */ - /* Initialize complete oneshot tracking */ - data->complete_oneshot_fds = emalloc(sizeof(HashTable)); + /* Initialize oneshot related hash tables */ + data->complete_oneshot_fds = php_poll_malloc(sizeof(HashTable), ctx->persistent); zend_hash_init(data->complete_oneshot_fds, 8, NULL, NULL, ctx->persistent); + data->garbage_oneshot_fds = php_poll_malloc(sizeof(HashTable), ctx->persistent); + zend_hash_init(data->garbage_oneshot_fds, 8, NULL, NULL, ctx->persistent); ctx->backend_data = data; return SUCCESS; @@ -72,10 +75,10 @@ static void kqueue_backend_cleanup(php_poll_ctx *ctx) close(data->kqueue_fd); } pefree(data->events, ctx->persistent); - if (data->complete_oneshot_fds) { - zend_hash_destroy(data->complete_oneshot_fds); - efree(data->complete_oneshot_fds); - } + zend_hash_destroy(data->complete_oneshot_fds); + pefree(data->complete_oneshot_fds, ctx->persistent); + zend_hash_destroy(data->garbage_oneshot_fds); + pefree(data->garbage_oneshot_fds, ctx->persistent); pefree(data, ctx->persistent); ctx->backend_data = NULL; } @@ -227,7 +230,7 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve zend_hash_index_add_empty_element(backend_data->complete_oneshot_fds, fd); } } else if (successful_deletes > 0 || add_count > 0) { - /* Modified existing filters - remove from oneshot tracking */ + /* One of the filter was deleted so remove from oneshot tracking */ zend_hash_index_del(backend_data->complete_oneshot_fds, fd); } @@ -298,9 +301,8 @@ static int kqueue_backend_wait( { kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; - /* Ensure we have enough space for the requested events. - * Since kqueue can return up to 2 raw events per FD (read + write), - * we need capacity for potentially 2x max_events. */ + /* Ensure we have enough space for the requested events as kqueue can return up to 2 raw events + * per FD (read + write), we need capacity for potentially 2x max_events. */ int required_capacity = max_events * 2; if (required_capacity > backend_data->events_capacity) { struct kevent *new_events = php_poll_realloc( @@ -325,12 +327,11 @@ static int kqueue_backend_wait( if (nfds > 0) { /* Group events by FD and combine read/write events */ - int unique_events = 0; - HashTable processed_complete_oneshot; /* Track which complete oneshot FDs we've processed */ - zend_hash_init(&processed_complete_oneshot, 8, NULL, NULL, 0); + int unique_events = 0, fd; + zend_hash_clean(backend_data->garbage_oneshot_fds); for (int i = 0; i < nfds; i++) { - int fd = (int) backend_data->events[i].ident; + 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; @@ -364,50 +365,32 @@ static int kqueue_backend_wait( /* New FD, create new event */ ZEND_ASSERT(unique_events < max_events); events[unique_events].fd = fd; - events[unique_events].events - = 0; /* Will be set below based on complete oneshot logic */ + events[unique_events].events = 0; events[unique_events].revents = revents; events[unique_events].data = data; unique_events++; - } - - /* Handle complete oneshot logic */ - if (is_oneshot && zend_hash_index_exists(backend_data->complete_oneshot_fds, fd)) { - /* This FD was registered with both read+write oneshot */ - if (!zend_hash_index_exists(&processed_complete_oneshot, fd)) { - /* Mark as processed so we only handle once */ - zend_hash_index_add_empty_element(&processed_complete_oneshot, fd); - - /* Since this FD had both read+write oneshot, and at least one fired, - * we need to remove ALL remaining filters to simulate epoll behavior */ - struct kevent cleanup_changes[2]; - - /* Try to remove both read and write filters */ - EV_SET(&cleanup_changes[0], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); - EV_SET(&cleanup_changes[1], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); - /* Execute cleanup (ignore ENOENT - some filters already auto-removed) */ - for (int c = 0; c < 2; c++) { - kevent(backend_data->kqueue_fd, &cleanup_changes[c], 1, NULL, 0, NULL); - /* Ignore errors - filters might already be gone */ - } - - /* This FD is now completely removed */ - backend_data->fd_count--; + if (is_oneshot && zend_hash_index_exists(backend_data->complete_oneshot_fds, fd)) { + zval dummy; + ZVAL_BOOL(&dummy, revents & PHP_POLL_READ); + zend_hash_index_add(backend_data->garbage_oneshot_fds, fd, &dummy); zend_hash_index_del(backend_data->complete_oneshot_fds, fd); - - /* Mark the combined event as oneshot */ - for (int j = 0; j < unique_events; j++) { - if (events[j].fd == fd) { - events[j].events = PHP_POLL_ONESHOT; - break; - } - } + backend_data->fd_count--; } + } else if (is_oneshot) { + zend_hash_index_del(backend_data->garbage_oneshot_fds, fd); } } - zend_hash_destroy(&processed_complete_oneshot); + /* Clean up all the same FD filters for other read or write side */ + zval *item; + struct kevent cleanup_change; + ZEND_HASH_FOREACH_NUM_KEY_VAL(backend_data->garbage_oneshot_fds, fd, item) { + int filter = Z_TYPE_P(item) == IS_TRUE ? EVFILT_WRITE : EVFILT_READ; + EV_SET(&cleanup_change, fd, filter, EV_DELETE, 0, 0, NULL); + kevent(backend_data->kqueue_fd, &cleanup_change, 1, NULL, 0, NULL); + } ZEND_HASH_FOREACH_END(); + return unique_events; } @@ -439,7 +422,7 @@ static int kqueue_backend_get_suitable_max_events(php_poll_ctx *ctx) return 1; } - /* Kqueue will return one combined event per FD (like epoll), + /* Kqueue backend will return one grouped event per FD (like epoll), * so the suitable max_events is exactly the number of registered FDs */ return active_fds; } From 1dd2d53b5718b610314a98e61cdd40af7581d4e7 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 23 Aug 2025 13:39:12 +0200 Subject: [PATCH 34/52] poll: improve and extend error handling with some helpers --- main/php_poll.h | 24 ++++-- main/poll/php_poll_internal.h | 23 ++++-- main/poll/poll_backend_eventport.c | 23 +----- main/poll/poll_backend_kqueue.c | 79 ++++--------------- main/poll/poll_core.c | 120 ++++++++++++++++++++++++++--- 5 files changed, 163 insertions(+), 106 deletions(-) diff --git a/main/php_poll.h b/main/php_poll.h index a1de289ce03c4..dd3317fb996ca 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -39,15 +39,20 @@ typedef enum { PHP_POLL_BACKEND_IOCP } php_poll_backend_type; -/* Result codes */ +/* Error codes */ typedef enum { - PHP_POLL_ERR_NONE, - PHP_POLL_ERR_SYSTEM, - PHP_POLL_ERR_NOMEM, - PHP_POLL_ERR_INVALID, - PHP_POLL_ERR_EXISTS, - PHP_POLL_ERR_NOTFOUND, - PHP_POLL_ERR_TIMEOUT, + PHP_POLL_ERR_NONE, /* No error */ + PHP_POLL_ERR_SYSTEM, /* Generic system error */ + PHP_POLL_ERR_NOMEM, /* Out of memory (ENOMEM) */ + PHP_POLL_ERR_INVALID, /* Invalid argument (EINVAL, EBADF) */ + PHP_POLL_ERR_EXISTS, /* Already exists (EEXIST) */ + PHP_POLL_ERR_NOTFOUND, /* Not found (ENOENT) */ + PHP_POLL_ERR_TIMEOUT, /* Operation timed out (ETIME, ETIMEDOUT) */ + PHP_POLL_ERR_INTERRUPTED, /* Interrupted by signal (EINTR) */ + PHP_POLL_ERR_PERMISSION, /* Permission denied (EACCES, EPERM) */ + PHP_POLL_ERR_TOOBIG, /* Too many resources (EMFILE, ENFILE) */ + PHP_POLL_ERR_AGAIN, /* Try again (EAGAIN, EWOULDBLOCK) */ + PHP_POLL_ERR_NOSUPPORT, /* Not supported (ENOSYS, EOPNOTSUPP) */ } php_poll_error; /* clang-format on */ @@ -90,4 +95,7 @@ PHPAPI int php_poll_get_suitable_max_events(php_poll_ctx *ctx); /* Backend registration */ PHPAPI void php_poll_register_backends(void); +/* Error string for the error */ +PHPAPI const char *php_poll_error_string(php_poll_error error); + #endif /* PHP_POLL_H */ diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h index fe0e8438e6b99..e7d6785e87b36 100644 --- a/main/poll/php_poll_internal.h +++ b/main/poll/php_poll_internal.h @@ -100,16 +100,27 @@ php_poll_fd_entry *php_poll_fd_table_get(php_poll_fd_table *table, int fd); void php_poll_fd_table_remove(php_poll_fd_table *table, int fd); int php_poll_simulate_edge_trigger(php_poll_fd_table *table, php_poll_event *events, int nfds); -static inline void php_poll_set_error(php_poll_ctx *ctx, php_poll_error error) +/* 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 = error; + 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 void php_poll_set_system_error_if_not_set(php_poll_ctx *ctx) +static inline bool php_poll_is_not_found_error(void) { - if (ctx->last_error == PHP_POLL_ERR_NONE) { - ctx->last_error = PHP_POLL_ERR_SYSTEM; - } + return errno == ENOENT; +} + +static inline void php_poll_set_error(php_poll_ctx *ctx, php_poll_error error) +{ + ctx->last_error = error; } #endif /* PHP_POLL_INTERNAL_H */ diff --git a/main/poll/poll_backend_eventport.c b/main/poll/poll_backend_eventport.c index d1db4cec3c0a3..26ff374f9dab1 100644 --- a/main/poll/poll_backend_eventport.c +++ b/main/poll/poll_backend_eventport.c @@ -234,16 +234,8 @@ static zend_result eventport_backend_remove(php_poll_ctx *ctx, int fd) if (port_dissociate(backend_data->port_fd, PORT_SOURCE_FD, fd) == -1) { /* Only fail if it's not ENOENT (might already be dissociated) */ - if (errno != ENOENT) { - switch (errno) { - case EBADF: - case EINVAL: - php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); - break; - default: - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); - break; - } + if (!php_poll_is_not_found_error()) { + php_poll_set_current_errno_error(ctx); return FAILURE; } } @@ -335,16 +327,7 @@ static int eventport_backend_wait( int result = port_getn(backend_data->port_fd, backend_data->events, max_events, &nget, tsp); if (result == -1) { - if (errno == ETIME) { - /* Timeout - this is normal */ - return 0; - } else if (errno == EINTR) { - /* Interrupted by signal */ - return 0; - } else { - /* Real error */ - return -1; - } + php_poll_set_current_errno_error(ctx); } int nfds = (int) nget; diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index bd537128e3f6a..6939972cb36da 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -112,21 +112,7 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events if (change_count > 0) { int result = kevent(backend_data->kqueue_fd, changes, change_count, NULL, 0, NULL); if (result == -1) { - switch (errno) { - case EEXIST: - php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); - break; - case ENOMEM: - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); - break; - case EBADF: - case EINVAL: - php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); - break; - default: - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); - break; - } + php_poll_set_current_errno_error(ctx); return FAILURE; } @@ -186,9 +172,8 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve int result = kevent(backend_data->kqueue_fd, &deletes[i], 1, NULL, 0, NULL); if (result == 0) { successful_deletes++; - } else if (errno != ENOENT) { - /* Real error (not just "doesn't exist") */ - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); + } else if (!php_poll_is_not_found_error()) { + php_poll_set_current_errno_error(ctx); return FAILURE; } /* ENOENT is ignored - filter didn't exist */ @@ -198,21 +183,7 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve if (add_count > 0) { int result = kevent(backend_data->kqueue_fd, adds, add_count, NULL, 0, NULL); if (result == -1) { - switch (errno) { - case ENOENT: - php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); - break; - case ENOMEM: - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); - break; - case EBADF: - case EINVAL: - php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); - break; - default: - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); - break; - } + php_poll_set_current_errno_error(ctx); return FAILURE; } } @@ -240,44 +211,26 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve 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 changes[2]; + struct kevent change; int successful_deletes = 0; /* Try to remove read filter */ - EV_SET(&changes[0], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); - int result = kevent(backend_data->kqueue_fd, &changes[0], 1, NULL, 0, NULL); + 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 (errno != ENOENT) { - /* Real error */ - switch (errno) { - case EBADF: - case EINVAL: - php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); - break; - default: - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); - break; - } + } else if (!php_poll_is_not_found_error()) { + php_poll_set_current_errno_error(ctx); return FAILURE; } /* Try to remove write filter */ - EV_SET(&changes[1], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); - result = kevent(backend_data->kqueue_fd, &changes[1], 1, NULL, 0, NULL); + 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 (errno != ENOENT) { - /* Real error */ - switch (errno) { - case EBADF: - case EINVAL: - php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); - break; - default: - php_poll_set_error(ctx, PHP_POLL_ERR_SYSTEM); - break; - } + } else if (!php_poll_is_not_found_error()) { + php_poll_set_current_errno_error(ctx); return FAILURE; } @@ -385,11 +338,13 @@ static int kqueue_backend_wait( /* Clean up all the same FD filters for other read or write side */ zval *item; struct kevent cleanup_change; - ZEND_HASH_FOREACH_NUM_KEY_VAL(backend_data->garbage_oneshot_fds, fd, item) { + ZEND_HASH_FOREACH_NUM_KEY_VAL(backend_data->garbage_oneshot_fds, fd, item) + { int filter = Z_TYPE_P(item) == IS_TRUE ? EVFILT_WRITE : EVFILT_READ; EV_SET(&cleanup_change, fd, filter, EV_DELETE, 0, 0, NULL); kevent(backend_data->kqueue_fd, &cleanup_change, 1, NULL, 0, NULL); - } ZEND_HASH_FOREACH_END(); + } + ZEND_HASH_FOREACH_END(); return unique_events; } diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index d24da08d04de5..f7fb34f1474d3 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -186,7 +186,7 @@ PHPAPI zend_result php_poll_init(php_poll_ctx *ctx) return SUCCESS; } - php_poll_set_system_error_if_not_set(ctx); + php_poll_set_current_errno_error(ctx); return FAILURE; } @@ -217,7 +217,7 @@ PHPAPI zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void return SUCCESS; } - php_poll_set_system_error_if_not_set(ctx); + php_poll_set_current_errno_error(ctx); return FAILURE; } @@ -234,7 +234,7 @@ PHPAPI zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, v return SUCCESS; } - php_poll_set_system_error_if_not_set(ctx); + php_poll_set_current_errno_error(ctx); return FAILURE; } @@ -251,7 +251,7 @@ PHPAPI zend_result php_poll_remove(php_poll_ctx *ctx, int fd) return SUCCESS; } - php_poll_set_system_error_if_not_set(ctx); + php_poll_set_current_errno_error(ctx); return FAILURE; } @@ -267,7 +267,7 @@ PHPAPI int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_even int nfds = ctx->backend_ops->wait(ctx, events, max_events, timeout); if (UNEXPECTED(nfds < 0)) { - php_poll_set_system_error_if_not_set(ctx); + php_poll_set_current_errno_error(ctx); } return nfds; @@ -291,18 +291,118 @@ 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; } -/* Get suitable max_events for backend */ -PHPAPI int php_poll_get_suitable_max_events(php_poll_ctx *ctx) +/* Errno to php_poll_error mapping helper */ +php_poll_error php_poll_errno_to_error(int err) { - if (UNEXPECTED(!ctx || !ctx->backend_ops)) { - return -1; + 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 +#ifdef EOPNOTSUPP + case EOPNOTSUPP: +#endif +#ifdef ENOTSUP + case ENOTSUP: +#endif + return PHP_POLL_ERR_NOSUPPORT; + + default: + return PHP_POLL_ERR_SYSTEM; } +} - return ctx->backend_ops->get_suitable_max_events(ctx); +/* 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"; + } } From 7e8f6850ce1d9e7763fbcbacfdc49ce2c565b6e8 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 23 Aug 2025 15:17:29 +0200 Subject: [PATCH 35/52] poll: extend and clean up tests --- ext/standard/tests/streams/stream_poll.inc | 30 +++++++++++ .../stream_poll_backend_name_basic.phpt | 2 +- .../stream_poll_basic_sock_add_only.phpt | 2 +- .../stream_poll_basic_sock_modify_write.phpt | 2 +- .../streams/stream_poll_basic_sock_read.phpt | 2 +- .../stream_poll_basic_sock_remove_write.phpt | 2 +- .../stream_poll_basic_sock_rw_multi_edge.phpt | 2 +- ...stream_poll_basic_sock_rw_multi_level.phpt | 2 +- ...tream_poll_basic_sock_rw_single_level.phpt | 2 +- .../streams/stream_poll_basic_sock_write.phpt | 2 +- ...am_poll_basic_tcp_read_multiple_level.phpt | 51 +++++++++++++++++++ .../stream_poll_basic_tcp_read_one_shot.phpt | 30 +++++++++++ .../stream_poll_basic_wait_no_add.phpt | 2 +- 13 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 ext/standard/tests/streams/stream_poll_basic_tcp_read_multiple_level.phpt create mode 100644 ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt diff --git a/ext/standard/tests/streams/stream_poll.inc b/ext/standard/tests/streams/stream_poll.inc index 30f5e1fcb5add..3e517a1999dab 100644 --- a/ext/standard/tests/streams/stream_poll.inc +++ b/ext/standard/tests/streams/stream_poll.inc @@ -35,6 +35,36 @@ function pt_new_tcp_socket_pair(): array { 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_new_stream_poll(): StreamPollContext { $backend = getenv('STREAM_POLL_TEST_BACKEND'); return stream_poll_create($backend === false ? STREAM_POLL_BACKEND_AUTO : $backend); diff --git a/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt b/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt index feee887c4627f..3fb06a19a3367 100644 --- a/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt +++ b/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt @@ -1,5 +1,5 @@ --TEST-- -Stream polling basic functionality - backend name +Stream polling - backend name --FILE-- +--EXPECT-- +Events count: 0 +Events count: 20 +Event[0]: 1, user data: server0_data, read data: 'test 0 data' +Event[1]: 1, user data: server1_data, read data: 'test 1 data' +Event[2]: 1, user data: server2_data, read data: 'test 2 data' +Event[3]: 1, user data: server3_data, read data: 'test 3 data' +Event[4]: 1, user data: server4_data, read data: 'test 4 data' +Event[5]: 1, user data: server5_data, read data: 'test 5 data' +Event[6]: 1, user data: server6_data, read data: 'test 6 data' +Event[7]: 1, user data: server7_data, read data: 'test 7 data' +Event[8]: 1, user data: server8_data, read data: 'test 8 data' +Event[9]: 1, user data: server9_data, read data: 'test 9 data' +Event[10]: 1, user data: server10_data, read data: 'test 10 data' +Event[11]: 1, user data: server11_data, read data: 'test 11 data' +Event[12]: 1, user data: server12_data, read data: 'test 12 data' +Event[13]: 1, user data: server13_data, read data: 'test 13 data' +Event[14]: 1, user data: server14_data, read data: 'test 14 data' +Event[15]: 1, user data: server15_data, read data: 'test 15 data' +Event[16]: 1, user data: server16_data, read data: 'test 16 data' +Event[17]: 1, user data: server17_data, read data: 'test 17 data' +Event[18]: 1, user data: server18_data, read data: 'test 18 data' +Event[19]: 1, user data: server19_data, read data: 'test 19 data' +Events count: 2 +Event[0]: 1, user data: server1_data, read data: 'more data' +Event[1]: 1, user data: server2_data, read data: 'more data' +Events count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt b/ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt new file mode 100644 index 0000000000000..f1038aa416c16 --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt @@ -0,0 +1,30 @@ +--TEST-- +Stream polling - TCP read write level +--FILE-- + +--EXPECT-- +Events count: 0 +Events count: 2 +Event[0]: 1, user data: server1_data, read data: 'test data' +Event[1]: 1, user data: server2_data, read data: 'test data' +Events count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt b/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt index 43036110f65df..1e5b602f50e30 100644 --- a/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_wait_no_add.phpt @@ -1,5 +1,5 @@ --TEST-- -Stream polling basic functionality - only wait +Stream polling - only wait --FILE-- Date: Sat, 23 Aug 2025 17:06:18 +0200 Subject: [PATCH 36/52] poll: rewrite tests to expect events in any order --- ext/standard/tests/streams/stream_poll.inc | 108 ++++++++++++++++++ .../stream_poll_basic_sock_modify_write.phpt | 7 +- .../streams/stream_poll_basic_sock_read.phpt | 7 +- .../stream_poll_basic_sock_remove_write.phpt | 17 +-- .../stream_poll_basic_sock_rw_multi_edge.phpt | 58 ++++++---- ...stream_poll_basic_sock_rw_multi_level.phpt | 68 ++++++----- ...tream_poll_basic_sock_rw_single_level.phpt | 16 +-- .../streams/stream_poll_basic_sock_write.phpt | 7 +- ...am_poll_basic_tcp_read_multiple_level.phpt | 50 ++++---- .../stream_poll_basic_tcp_read_one_shot.phpt | 27 +++-- .../stream_poll_basic_tcp_rw_one_shot.phpt | 23 ++-- ...stream_poll_basic_tcp_rw_single_level.phpt | 23 ++-- 12 files changed, 275 insertions(+), 136 deletions(-) diff --git a/ext/standard/tests/streams/stream_poll.inc b/ext/standard/tests/streams/stream_poll.inc index 3e517a1999dab..2e561f57dd266 100644 --- a/ext/standard/tests/streams/stream_poll.inc +++ b/ext/standard/tests/streams/stream_poll.inc @@ -93,3 +93,111 @@ function pt_print_events($events, $read_data = false): void { echo "\n"; } } + +function pt_expect_events($events, $expected): void { + if (!is_array($events)) { + die("Events must be an array\n"); + } + + if (!is_array($expected)) { + die("Expected events must be an array\n"); + } + + $event_count = count($events); + $expected_count = count($expected); + + if ($event_count !== $expected_count) { + echo "Event count mismatch: got $event_count, expected $expected_count\n"; + pt_print_mismatched_events($events, $expected); + return; + } + + // Convert events to comparable format for matching + $actual_events = []; + foreach ($events as $event) { + if (!$event instanceof StreamPollEvent) { + die('Invalid event type'); + } + + $event_data = [ + 'events' => $event->events, + 'data' => $event->data + ]; + + $actual_events[] = $event_data; + } + + // Try to match each expected event with an actual event + $matched = []; + $unmatched_expected = []; + + foreach ($expected as $exp_idx => $exp_event) { + $found_match = false; + + foreach ($actual_events as $act_idx => $act_event) { + if (isset($matched[$act_idx])) { + continue; // Already matched + } + + // Check if events and data match + if ($act_event['events'] === $exp_event['events'] && + $act_event['data'] === $exp_event['data']) { + + // If read data is expected, check it + if (isset($exp_event['read'])) { + $read_data = fread($events[$act_idx]->stream, 1024); + if ($read_data !== $exp_event['read']) { + continue; // Read data doesn't match + } + } + + $matched[$act_idx] = $exp_idx; + $found_match = true; + break; + } + } + + if (!$found_match) { + $unmatched_expected[] = $exp_event; + } + } + + // Check if all events matched + if (count($matched) === $event_count && empty($unmatched_expected)) { + echo "Events matched - count: $event_count\n"; + } else { + echo "Events did not match:\n"; + pt_print_mismatched_events($events, $expected, $matched); + } +} + +function pt_event_flags_to_string($flags): string { + $names = []; + if ($flags & STREAM_POLL_READ) $names[] = 'READ'; + if ($flags & STREAM_POLL_WRITE) $names[] = 'WRITE'; + if ($flags & STREAM_POLL_ERROR) $names[] = 'ERROR'; + if ($flags & STREAM_POLL_HUP) $names[] = 'HUP'; + + return empty($names) ? 'NONE' : implode('|', $names); +} + +function pt_print_mismatched_events($actual_events, $expected_events, $matched = []): void { + echo "Actual events:\n"; + foreach ($actual_events as $i => $event) { + $match_status = isset($matched[$i]) ? " [MATCHED]" : " [UNMATCHED]"; + $event_names = pt_event_flags_to_string($event->events); + echo " Event[$i]: $event_names, user data: " . $event->data . $match_status . "\n"; + } + + echo "Expected events:\n"; + foreach ($expected_events as $i => $exp_event) { + $was_matched = in_array($i, $matched); + $match_status = $was_matched ? " [MATCHED]" : " [UNMATCHED]"; + $event_names = pt_event_flags_to_string($exp_event['events']); + echo " Event[$i]: $event_names, user data: " . $exp_event['data']; + if (isset($exp_event['read'])) { + echo ", read data: '" . $exp_event['read'] . "'"; + } + echo $match_status . "\n"; + } +} diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt index c38d9345c7582..5b00be6ee6eb4 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_modify_write.phpt @@ -10,8 +10,9 @@ $poll_ctx = pt_new_stream_poll(); stream_poll_add($poll_ctx, $socket2, STREAM_POLL_WRITE, "socket_data"); stream_poll_modify($poll_ctx, $socket2, STREAM_POLL_WRITE, "modified_data"); -pt_print_events(stream_poll_wait($poll_ctx, 0)); +pt_expect_events(stream_poll_wait($poll_ctx, 0), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'modified_data'] +]); ?> --EXPECT-- -Events count: 1 -Event[0]: 2, user data: modified_data +Events matched - count: 1 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt index 2e944419fad3c..3a8d6988d5822 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_read.phpt @@ -10,9 +10,10 @@ $poll_ctx = pt_new_stream_poll(); stream_poll_add($poll_ctx, $socket1r, STREAM_POLL_READ, "socket_data"); fwrite($socket1w, "test data"); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_READ, 'data' => 'socket_data', 'read' => 'test data'] +]); ?> --EXPECT-- -Events count: 1 -Event[0]: 1, user data: socket_data, read data: 'test data' +Events matched - count: 1 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt index 4a50af5dafe00..02589be2d6327 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_remove_write.phpt @@ -11,11 +11,16 @@ $poll_ctx = pt_new_stream_poll(); stream_poll_add($poll_ctx, $socket1w, STREAM_POLL_WRITE, "socket_data_1"); stream_poll_add($poll_ctx, $socket2w, STREAM_POLL_WRITE, "socket_data_2"); -pt_print_events(stream_poll_wait($poll_ctx, 0)); +pt_expect_events(stream_poll_wait($poll_ctx, 0), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket_data_1'], + ['events' => STREAM_POLL_WRITE, 'data' => 'socket_data_2'] +]); stream_poll_remove($poll_ctx, $socket1w); -pt_print_events(stream_poll_wait($poll_ctx, 0)); +pt_expect_events(stream_poll_wait($poll_ctx, 0), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket_data_2'] +]); // check that both streams are still usable var_dump(fwrite($socket1w, "test 1")); @@ -25,13 +30,9 @@ var_dump(fread($socket2r, 100)); ?> --EXPECT-- -Events count: 2 -Event[0]: 2, user data: socket_data_1 -Event[1]: 2, user data: socket_data_2 -Events count: 1 -Event[0]: 2, user data: socket_data_2 +Events matched - count: 2 +Events matched - count: 1 int(6) int(6) string(6) "test 1" string(6) "test 2" - diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt index 5a249525dddab..a8ee73bd9266e 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt @@ -1,5 +1,5 @@ --TEST-- -Stream polling - socket write / read multiple times with edger triggering +Stream polling - socket write / read multiple times with edge triggering --FILE-- STREAM_POLL_WRITE, 'data' => 'socket2_data'] +]); + +pt_expect_events(stream_poll_wait($poll_ctx, 0), []); + fwrite($socket1w, "test data"); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'test data'] +]); + fwrite($socket1w, "more data"); -pt_print_events(stream_poll_wait($poll_ctx, 100)); -pt_print_events(stream_poll_wait($poll_ctx, 100)); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data'] +]); + +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); + fwrite($socket1w, " and even more data"); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'more data and even more data'] +]); + fclose($socket1r); -pt_print_events(stream_poll_wait($poll_ctx, 100)); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'data' => 'socket2_data'] +]); + fclose($socket1w); -pt_print_events(stream_poll_wait($poll_ctx, 100)); +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); ?> --EXPECT-- -Events count: 1 -Event[0]: 2, user data: socket2_data -Events count: 0 -Events count: 1 -Event[0]: 1, user data: socket1_data, read data: 'test data' -Events count: 2 -Event[0]: 2, user data: socket2_data -Event[1]: 1, user data: socket1_data -Events count: 0 -Events count: 1 -Event[0]: 1, user data: socket1_data, read data: 'more data and even more data' -Events count: 1 -Event[0]: 10, user data: socket2_data -Events count: 0 +Events matched - count: 1 +Events matched - count: 0 +Events matched - count: 1 +Events matched - count: 2 +Events matched - count: 0 +Events matched - count: 1 +Events matched - count: 1 +Events matched - count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt index 70f5de2c67857..31e0405a31824 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt @@ -10,38 +10,52 @@ $poll_ctx = pt_new_stream_poll(); stream_poll_add($poll_ctx, $socket1r, STREAM_POLL_READ, "socket1_data"); stream_poll_add($poll_ctx, $socket1w, STREAM_POLL_WRITE, "socket2_data"); -pt_print_events(stream_poll_wait($poll_ctx, 0)); -pt_print_events(stream_poll_wait($poll_ctx, 0)); +pt_expect_events(stream_poll_wait($poll_ctx, 0), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'] +]); + +pt_expect_events(stream_poll_wait($poll_ctx, 0), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'] +]); + fwrite($socket1w, "test data"); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'test data'] +]); + fwrite($socket1w, "more data"); -pt_print_events(stream_poll_wait($poll_ctx, 100)); -pt_print_events(stream_poll_wait($poll_ctx, 100)); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data'] +]); + +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data'] +]); + fwrite($socket1w, " and even more data"); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'more data and even more data'] +]); + fclose($socket1r); -pt_print_events(stream_poll_wait($poll_ctx, 100)); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'data' => 'socket2_data'] +]); + fclose($socket1w); -pt_print_events(stream_poll_wait($poll_ctx, 100)); +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); ?> --EXPECT-- -Events count: 1 -Event[0]: 2, user data: socket2_data -Events count: 1 -Event[0]: 2, user data: socket2_data -Events count: 2 -Event[0]: 2, user data: socket2_data -Event[1]: 1, user data: socket1_data, read data: 'test data' -Events count: 2 -Event[0]: 2, user data: socket2_data -Event[1]: 1, user data: socket1_data -Events count: 2 -Event[0]: 2, user data: socket2_data -Event[1]: 1, user data: socket1_data -Events count: 2 -Event[0]: 2, user data: socket2_data -Event[1]: 1, user data: socket1_data, read data: 'more data and even more data' -Events count: 1 -Event[0]: 10, user data: socket2_data -Events count: 0 +Events matched - count: 1 +Events matched - count: 1 +Events matched - count: 2 +Events matched - count: 2 +Events matched - count: 2 +Events matched - count: 2 +Events matched - count: 1 +Events matched - count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt index eb4959a63584b..fecf992668e43 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_level.phpt @@ -10,14 +10,16 @@ $poll_ctx = pt_new_stream_poll(); stream_poll_add($poll_ctx, $socket1r, STREAM_POLL_READ, "socket1_data"); stream_poll_add($poll_ctx, $socket1w, STREAM_POLL_WRITE, "socket2_data"); -pt_print_events(stream_poll_wait($poll_ctx, 0)); +pt_expect_events(stream_poll_wait($poll_ctx, 0), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'] +]); fwrite($socket1w, "test data"); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket2_data'], + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'test data'] +]); ?> --EXPECT-- -Events count: 1 -Event[0]: 2, user data: socket2_data -Events count: 2 -Event[0]: 2, user data: socket2_data -Event[1]: 1, user data: socket1_data, read data: 'test data' +Events matched - count: 1 +Events matched - count: 2 diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt index b5c540ab90938..ce6f5922ffe98 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_write.phpt @@ -9,9 +9,10 @@ $poll_ctx = pt_new_stream_poll(); stream_poll_add($poll_ctx, $socket1w, STREAM_POLL_WRITE, "socket_data"); -pt_print_events(stream_poll_wait($poll_ctx, 0)); +pt_expect_events(stream_poll_wait($poll_ctx, 0), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'socket_data'] +]); ?> --EXPECT-- -Events count: 1 -Event[0]: 2, user data: socket_data +Events matched - count: 1 diff --git a/ext/standard/tests/streams/stream_poll_basic_tcp_read_multiple_level.phpt b/ext/standard/tests/streams/stream_poll_basic_tcp_read_multiple_level.phpt index 844abfc4af3cd..d20751c28ea6f 100644 --- a/ext/standard/tests/streams/stream_poll_basic_tcp_read_multiple_level.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_tcp_read_multiple_level.phpt @@ -11,41 +11,31 @@ for ($i = 0; $i < count($servers); $i++) { stream_poll_add($poll_ctx, $servers[$i], STREAM_POLL_READ, "server{$i}_data"); } -pt_print_events(stream_poll_wait($poll_ctx, 0)); +pt_expect_events(stream_poll_wait($poll_ctx, 0), []); + for ($i = 0; $i < count($clients); $i++) { pt_write_sleep($clients[$i], "test $i data"); } -pt_print_events(stream_poll_wait($poll_ctx, 100), true); + +// Build expected events for all 20 connections +$expected_events = []; +for ($i = 0; $i < 20; $i++) { + $expected_events[] = ['events' => STREAM_POLL_READ, 'data' => "server{$i}_data", 'read' => "test $i data"]; +} +pt_expect_events(stream_poll_wait($poll_ctx, 100), $expected_events); + pt_write_sleep($clients[1], "more data"); pt_write_sleep($clients[2], "more data"); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_READ, 'data' => 'server1_data', 'read' => 'more data'], + ['events' => STREAM_POLL_READ, 'data' => 'server2_data', 'read' => 'more data'] +]); + +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); ?> --EXPECT-- -Events count: 0 -Events count: 20 -Event[0]: 1, user data: server0_data, read data: 'test 0 data' -Event[1]: 1, user data: server1_data, read data: 'test 1 data' -Event[2]: 1, user data: server2_data, read data: 'test 2 data' -Event[3]: 1, user data: server3_data, read data: 'test 3 data' -Event[4]: 1, user data: server4_data, read data: 'test 4 data' -Event[5]: 1, user data: server5_data, read data: 'test 5 data' -Event[6]: 1, user data: server6_data, read data: 'test 6 data' -Event[7]: 1, user data: server7_data, read data: 'test 7 data' -Event[8]: 1, user data: server8_data, read data: 'test 8 data' -Event[9]: 1, user data: server9_data, read data: 'test 9 data' -Event[10]: 1, user data: server10_data, read data: 'test 10 data' -Event[11]: 1, user data: server11_data, read data: 'test 11 data' -Event[12]: 1, user data: server12_data, read data: 'test 12 data' -Event[13]: 1, user data: server13_data, read data: 'test 13 data' -Event[14]: 1, user data: server14_data, read data: 'test 14 data' -Event[15]: 1, user data: server15_data, read data: 'test 15 data' -Event[16]: 1, user data: server16_data, read data: 'test 16 data' -Event[17]: 1, user data: server17_data, read data: 'test 17 data' -Event[18]: 1, user data: server18_data, read data: 'test 18 data' -Event[19]: 1, user data: server19_data, read data: 'test 19 data' -Events count: 2 -Event[0]: 1, user data: server1_data, read data: 'more data' -Event[1]: 1, user data: server2_data, read data: 'more data' -Events count: 0 +Events matched - count: 0 +Events matched - count: 20 +Events matched - count: 2 +Events matched - count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt b/ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt index f1038aa416c16..c2bcdb8330bef 100644 --- a/ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_tcp_read_one_shot.phpt @@ -1,5 +1,5 @@ --TEST-- -Stream polling - TCP read write level +Stream polling - TCP read write oneshot --FILE-- STREAM_POLL_READ, 'data' => 'server1_data', 'read' => 'test data'], + ['events' => STREAM_POLL_READ, 'data' => 'server2_data', 'read' => 'test data'] +]); + pt_write_sleep($client1, "more data"); pt_write_sleep($client2, "more data"); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); ?> --EXPECT-- -Events count: 0 -Events count: 2 -Event[0]: 1, user data: server1_data, read data: 'test data' -Event[1]: 1, user data: server2_data, read data: 'test data' -Events count: 0 +Events matched - count: 0 +Events matched - count: 2 +Events matched - count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt b/ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt index 16d562247d19a..7f90997b8f139 100644 --- a/ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_tcp_rw_one_shot.phpt @@ -1,5 +1,5 @@ --TEST-- -Stream polling - TCP read write level +Stream polling - TCP read write oneshot combined --FILE-- STREAM_POLL_WRITE, 'data' => 'client_data'], + ['events' => STREAM_POLL_WRITE, 'data' => 'server_data'] +]); -pt_print_events(stream_poll_wait($poll_ctx, 0)); pt_write_sleep($client, "test data"); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); + pt_write_sleep($client, "test data"); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), []); ?> --EXPECT-- -Events count: 2 -Event[0]: 2, user data: client_data -Event[1]: 2, user data: server_data -Events count: 0 -Events count: 0 +Events matched - count: 2 +Events matched - count: 0 +Events matched - count: 0 diff --git a/ext/standard/tests/streams/stream_poll_basic_tcp_rw_single_level.phpt b/ext/standard/tests/streams/stream_poll_basic_tcp_rw_single_level.phpt index b6734bb2f2cd1..f82bdc4f097de 100644 --- a/ext/standard/tests/streams/stream_poll_basic_tcp_rw_single_level.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_tcp_rw_single_level.phpt @@ -1,5 +1,5 @@ --TEST-- -Stream polling - TCP read write level +Stream polling - TCP read write level combined --FILE-- STREAM_POLL_WRITE, 'data' => 'client_data'], + ['events' => STREAM_POLL_WRITE, 'data' => 'server_data'] +]); -pt_print_events(stream_poll_wait($poll_ctx, 0)); fwrite($client, "test data"); usleep(10000); -pt_print_events(stream_poll_wait($poll_ctx, 100), true); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_WRITE, 'data' => 'client_data'], + ['events' => STREAM_POLL_READ | STREAM_POLL_WRITE, 'data' => 'server_data', 'read' => 'test data'] +]); ?> --EXPECT-- -Events count: 2 -Event[0]: 2, user data: client_data -Event[1]: 2, user data: server_data -Events count: 2 -Event[0]: 2, user data: client_data -Event[1]: 3, user data: server_data, read data: 'test data' +Events matched - count: 2 +Events matched - count: 2 From 8a46cd2ab7eadcef8186773c0e5773f60987e30f Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 23 Aug 2025 18:30:56 +0200 Subject: [PATCH 37/52] poll: support backend specific events in event expectation This is to handle HUP difference in kqueue --- ext/standard/tests/streams/stream_poll.inc | 47 +++++++++++++++++-- ...stream_poll_basic_sock_rw_multi_level.phpt | 7 ++- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/ext/standard/tests/streams/stream_poll.inc b/ext/standard/tests/streams/stream_poll.inc index 2e561f57dd266..208100301cadd 100644 --- a/ext/standard/tests/streams/stream_poll.inc +++ b/ext/standard/tests/streams/stream_poll.inc @@ -93,8 +93,7 @@ function pt_print_events($events, $read_data = false): void { echo "\n"; } } - -function pt_expect_events($events, $expected): void { +function pt_expect_events($events, $expected, $poll_ctx = null): void { if (!is_array($events)) { die("Events must be an array\n"); } @@ -112,6 +111,9 @@ function pt_expect_events($events, $expected): void { return; } + // Get current backend name for backend-specific expectations + $backend_name = $poll_ctx ? stream_poll_backend_name($poll_ctx) : 'unknown'; + // Convert events to comparable format for matching $actual_events = []; foreach ($events as $event) { @@ -127,11 +129,23 @@ function pt_expect_events($events, $expected): void { $actual_events[] = $event_data; } + // Resolve backend-specific expectations + $resolved_expected = []; + foreach ($expected as $exp_event) { + $resolved_event = $exp_event; + + if (isset($exp_event['events']) && is_array($exp_event['events'])) { + $resolved_event['events'] = pt_resolve_backend_specific_value($exp_event['events'], $backend_name); + } + + $resolved_expected[] = $resolved_event; + } + // Try to match each expected event with an actual event $matched = []; $unmatched_expected = []; - foreach ($expected as $exp_idx => $exp_event) { + foreach ($resolved_expected as $exp_idx => $exp_event) { $found_match = false; foreach ($actual_events as $act_idx => $act_event) { @@ -167,10 +181,35 @@ function pt_expect_events($events, $expected): void { echo "Events matched - count: $event_count\n"; } else { echo "Events did not match:\n"; - pt_print_mismatched_events($events, $expected, $matched); + pt_print_mismatched_events($events, $resolved_expected, $matched); } } +function pt_resolve_backend_specific_value($backend_map, $current_backend) { + // Direct backend match + if (isset($backend_map[$current_backend])) { + return $backend_map[$current_backend]; + } + + // Check for multi-backend keys (e.g., "poll|epoll") + foreach ($backend_map as $key => $value) { + if (strpos($key, '|') !== false) { + $backends = array_map('trim', explode('|', $key)); + if (in_array($current_backend, $backends)) { + return $value; + } + } + } + + // Fall back to default + if (isset($backend_map['default'])) { + return $backend_map['default']; + } + + // If no match found, this is an error + die("No backend-specific value found for '$current_backend' and no default specified\n"); +} + function pt_event_flags_to_string($flags): string { $names = []; if ($flags & STREAM_POLL_READ) $names[] = 'READ'; diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt index 31e0405a31824..f455eb40e5806 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt @@ -43,8 +43,11 @@ pt_expect_events(stream_poll_wait($poll_ctx, 100), [ fclose($socket1r); pt_expect_events(stream_poll_wait($poll_ctx, 100), [ - ['events' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'data' => 'socket2_data'] -]); + [ + 'events' => ['kqueue' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'default' => STREAM_POLL_HUP], + 'data' => 'socket2_data' + ] +], $poll_ctx); fclose($socket1w); pt_expect_events(stream_poll_wait($poll_ctx, 100), []); From 38fb0cf7c72de4980ccfde1fb35c732b9740780d Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 23 Aug 2025 20:01:43 +0200 Subject: [PATCH 38/52] poll: extend and fix edge tests --- .../stream_poll_basic_sock_rw_multi_edge.phpt | 7 ++++-- ...stream_poll_basic_sock_rw_single_edge.phpt | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 ext/standard/tests/streams/stream_poll_basic_sock_rw_single_edge.phpt diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt index a8ee73bd9266e..652e670d87074 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt @@ -36,8 +36,11 @@ pt_expect_events(stream_poll_wait($poll_ctx, 100), [ fclose($socket1r); pt_expect_events(stream_poll_wait($poll_ctx, 100), [ - ['events' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'data' => 'socket2_data'] -]); + [ + 'events' => ['kqueue' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'default' => STREAM_POLL_HUP], + 'data' => 'socket2_data' + ] +], $poll_ctx); fclose($socket1w); pt_expect_events(stream_poll_wait($poll_ctx, 100), []); diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_edge.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_edge.phpt new file mode 100644 index 0000000000000..b6be81cec864c --- /dev/null +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_single_edge.phpt @@ -0,0 +1,24 @@ +--TEST-- +Stream polling - socket write / read few time only +--FILE-- + STREAM_POLL_WRITE, 'data' => 'socket2_data'] +]); +fwrite($socket1w, "test data"); +pt_expect_events(stream_poll_wait($poll_ctx, 100), [ + ['events' => STREAM_POLL_READ, 'data' => 'socket1_data', 'read' => 'test data'] +]); + +?> +--EXPECT-- +Events matched - count: 1 +Events matched - count: 2 From 8b5a60269598f9a2279d3701c4393abb932e90dd Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 23 Aug 2025 20:03:39 +0200 Subject: [PATCH 39/52] poll: refactore and optimize fd table --- main/poll/php_poll_internal.h | 32 +++++-- main/poll/poll_backend_poll.c | 12 +-- main/poll/poll_backend_select.c | 142 +++++++++++++++++--------------- main/poll/poll_core.c | 44 ++++++++++ main/poll/poll_fd_table.c | 141 +++++++++++-------------------- 5 files changed, 199 insertions(+), 172 deletions(-) diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h index e7d6785e87b36..c6535cf1ba088 100644 --- a/main/poll/php_poll_internal.h +++ b/main/poll/php_poll_internal.h @@ -16,6 +16,7 @@ #define PHP_POLL_INTERNAL_H #include "php_poll.h" +#include "php_network.h" /* Allocation macros */ #define php_poll_calloc(nmemb, size, persistent) \ @@ -81,23 +82,44 @@ typedef struct php_poll_fd_entry { uint32_t events; void *data; bool active; - uint32_t last_revents; /* For edge-trigger simulation */ + uint32_t last_revents; } php_poll_fd_entry; /* FD tracking table */ typedef struct php_poll_fd_table { - php_poll_fd_entry *entries; - int capacity; - int count; + HashTable entries_ht; bool persistent; } php_poll_fd_table; -/* Poll FD helpers */ +/* Iterator callback function type */ +typedef bool (*php_poll_fd_iterator_func_t)(int fd, php_poll_fd_entry *entry, void *user_data); + +/* Poll FD helpers - clean API with accessor functions */ php_poll_fd_table *php_poll_fd_table_init(int initial_capacity, bool persistent); void php_poll_fd_table_cleanup(php_poll_fd_table *table); php_poll_fd_entry *php_poll_fd_table_find(php_poll_fd_table *table, int fd); php_poll_fd_entry *php_poll_fd_table_get(php_poll_fd_table *table, int fd); void php_poll_fd_table_remove(php_poll_fd_table *table, int fd); + +/* Accessor functions for table properties */ +static inline int php_poll_fd_table_count(php_poll_fd_table *table) +{ + return zend_hash_num_elements(&table->entries_ht); +} + +static inline bool php_poll_fd_table_is_empty(php_poll_fd_table *table) +{ + return zend_hash_num_elements(&table->entries_ht) == 0; +} + +/* New helper functions for improved backend integration */ +void php_poll_fd_table_foreach( + php_poll_fd_table *table, php_poll_fd_iterator_func_t callback, void *user_data); +php_socket_t php_poll_fd_table_get_max_fd(php_poll_fd_table *table); +int php_poll_fd_table_collect_events( + php_poll_fd_table *table, php_poll_event *events, int max_events); + +/* Edge triggering simulation helper */ int php_poll_simulate_edge_trigger(php_poll_fd_table *table, php_poll_event *events, int nfds); /* Error helper functions */ diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 051cfec50616b..94d50fef2ce5e 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -24,8 +24,6 @@ typedef struct { struct pollfd *fds; int fds_capacity; int fds_used; - - /* Use common FD tracking helper */ php_poll_fd_table *fd_table; } poll_backend_data_t; @@ -107,7 +105,6 @@ static zend_result poll_backend_init(php_poll_ctx *ctx) data->fds_capacity = initial_capacity; data->fds_used = 0; - /* Initialize FD tracking using helper */ data->fd_table = php_poll_fd_table_init(initial_capacity, ctx->persistent); if (!data->fd_table) { pefree(data->fds, ctx->persistent); @@ -140,13 +137,11 @@ static zend_result poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; - /* Check if FD already exists using helper */ if (php_poll_fd_table_find(backend_data->fd_table, fd)) { php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); return FAILURE; } - /* Get FD entry using helper */ php_poll_fd_entry *entry = php_poll_fd_table_get(backend_data->fd_table, fd); if (!entry) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); @@ -192,7 +187,7 @@ static zend_result poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t event { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; - /* Find existing entry using helper */ + /* Find existing entry using helper - O(1) instead of O(n) */ 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); @@ -217,7 +212,6 @@ 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; - /* Check if exists using helper */ if (!php_poll_fd_table_find(backend_data->fd_table, fd)) { php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); return FAILURE; @@ -232,7 +226,6 @@ static zend_result poll_backend_remove(php_poll_ctx *ctx, int fd) backend_data->fds_used--; } - /* Remove from tracking using helper */ php_poll_fd_table_remove(backend_data->fd_table, fd); return SUCCESS; @@ -316,8 +309,7 @@ static int poll_backend_get_suitable_max_events(php_poll_ctx *ctx) return -1; } - /* For poll(), we know exactly how many FDs are registered */ - int active_fds = backend_data->fds_used; + int active_fds = php_poll_fd_table_count(backend_data->fd_table); if (active_fds == 0) { return 1; diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c index c1e9de329e57e..0e51bb659dfa4 100644 --- a/main/poll/poll_backend_select.c +++ b/main/poll/poll_backend_select.c @@ -15,30 +15,39 @@ #include "php_poll_internal.h" #include "php_network.h" +#include "php_poll_internal.h" +#include "php_network.h" + typedef struct { fd_set read_fds, write_fds, error_fds; fd_set master_read_fds, master_write_fds, master_error_fds; - - /* Use common FD tracking helper */ php_poll_fd_table *fd_table; - - php_socket_t max_fd; /* Highest fd for select() */ + php_socket_t max_fd; } select_backend_data_t; -/* Update max_fd for select() by scanning all active FDs */ -static void select_update_max_fd(select_backend_data_t *data) +/* Select-specific helper functions */ +static bool find_max_fd_callback(int fd, php_poll_fd_entry *entry, void *user_data) { - data->max_fd = 0; - php_poll_fd_table *table = data->fd_table; - - for (int i = 0; i < table->capacity; i++) { - if (table->entries[i].active) { - php_socket_t sock = (php_socket_t) table->entries[i].fd; - if (sock > data->max_fd) { - data->max_fd = sock; - } - } + php_socket_t *max_fd = (php_socket_t *) user_data; + php_socket_t sock = (php_socket_t) fd; + + if (sock > *max_fd) { + *max_fd = sock; } + return true; +} + +static php_socket_t select_get_max_fd(php_poll_fd_table *table) +{ + php_socket_t max_fd = 0; + php_poll_fd_table_foreach(table, find_max_fd_callback, &max_fd); + return max_fd; +} + +/* Update max_fd for select() */ +static void select_update_max_fd(select_backend_data_t *data) +{ + data->max_fd = select_get_max_fd(data->fd_table); } static zend_result select_backend_init(php_poll_ctx *ctx) @@ -53,7 +62,7 @@ static zend_result select_backend_init(php_poll_ctx *ctx) /* 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; - /* Initialize FD tracking using helper */ + /* Initialize FD tracking using helper - now uses HashTable internally */ data->fd_table = php_poll_fd_table_init(initial_capacity, ctx->persistent); if (!data->fd_table) { pefree(data, ctx->persistent); @@ -92,13 +101,11 @@ static zend_result select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events } #endif - /* Check if FD already exists using helper */ if (php_poll_fd_table_find(backend_data->fd_table, fd)) { php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); return FAILURE; } - /* Get FD entry using helper */ php_poll_fd_entry *entry = php_poll_fd_table_get(backend_data->fd_table, fd); if (!entry) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); @@ -132,17 +139,15 @@ static zend_result select_backend_modify( select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; - /* Find existing entry using helper */ 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; - entry->last_revents = 0; /* Reset on modify */ + entry->last_revents = 0; /* Remove from all sets first */ FD_CLR(sock, &backend_data->master_read_fds); @@ -166,21 +171,17 @@ static zend_result select_backend_remove(php_poll_ctx *ctx, int fd) select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; - /* Check if exists using helper */ if (!php_poll_fd_table_find(backend_data->fd_table, fd)) { php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); return FAILURE; } - /* Remove from fd_sets */ FD_CLR(sock, &backend_data->master_read_fds); FD_CLR(sock, &backend_data->master_write_fds); FD_CLR(sock, &backend_data->master_error_fds); - /* Remove from tracking using helper */ php_poll_fd_table_remove(backend_data->fd_table, fd); - /* Update max_fd if this was the highest */ if (sock == backend_data->max_fd) { select_update_max_fd(backend_data); } @@ -188,31 +189,67 @@ static zend_result select_backend_remove(php_poll_ctx *ctx, int fd) return SUCCESS; } -/* Handle oneshot removal after event */ static void select_handle_oneshot_removal(select_backend_data_t *backend_data, int fd) { php_socket_t sock = (php_socket_t) fd; - /* Remove from fd_sets */ FD_CLR(sock, &backend_data->master_read_fds); FD_CLR(sock, &backend_data->master_write_fds); FD_CLR(sock, &backend_data->master_error_fds); - /* Remove from tracking */ php_poll_fd_table_remove(backend_data->fd_table, fd); - /* Update max_fd if needed */ if (sock == backend_data->max_fd) { select_update_max_fd(backend_data); } } +/* Callback function for processing select() results */ +typedef struct { + select_backend_data_t *backend_data; + php_poll_event *events; + int max_events; + int event_count; +} select_result_context; + +static bool process_select_result_callback(int fd, php_poll_fd_entry *entry, void *user_data) +{ + select_result_context *ctx = (select_result_context *) user_data; + + if (ctx->event_count >= ctx->max_events) { + return false; /* Stop iteration, events array is full */ + } + + php_socket_t sock = (php_socket_t) fd; + uint32_t revents = 0; + + if (FD_ISSET(sock, &ctx->backend_data->read_fds)) { + revents |= PHP_POLL_READ; + } + if (FD_ISSET(sock, &ctx->backend_data->write_fds)) { + revents |= PHP_POLL_WRITE; + } + if (FD_ISSET(sock, &ctx->backend_data->error_fds)) { + revents |= PHP_POLL_ERROR; + } + + if (revents != 0) { + ctx->events[ctx->event_count].fd = fd; + ctx->events[ctx->event_count].events = entry->events; + ctx->events[ctx->event_count].revents = revents; + ctx->events[ctx->event_count].data = entry->data; + ctx->event_count++; + } + + return true; /* Continue iteration */ +} + static int select_backend_wait( php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; - if (backend_data->fd_table->count == 0) { + if (php_poll_fd_table_is_empty(backend_data->fd_table)) { /* No sockets to wait for, but respect timeout */ if (timeout > 0) { #ifdef _WIN32 @@ -248,41 +285,18 @@ static int select_backend_wait( return (result == 0) ? 0 : -1; } - /* Process results */ - int event_count = 0; - php_poll_fd_table *table = backend_data->fd_table; - - for (int i = 0; i < table->capacity && event_count < max_events; i++) { - if (!table->entries[i].active) { - continue; - } - - php_socket_t sock = (php_socket_t) table->entries[i].fd; - uint32_t revents = 0; - - if (FD_ISSET(sock, &backend_data->read_fds)) { - revents |= PHP_POLL_READ; - } - if (FD_ISSET(sock, &backend_data->write_fds)) { - revents |= PHP_POLL_WRITE; - } - if (FD_ISSET(sock, &backend_data->error_fds)) { - revents |= PHP_POLL_ERROR; - } + /* Process results using iteration over active FDs only */ + select_result_context result_ctx = { + .backend_data = backend_data, .events = events, .max_events = max_events, .event_count = 0 + }; - if (revents != 0) { - events[event_count].fd = table->entries[i].fd; - events[event_count].events = table->entries[i].events; - events[event_count].revents = revents; - events[event_count].data = table->entries[i].data; - event_count++; - } - } + php_poll_fd_table_foreach(backend_data->fd_table, process_select_result_callback, &result_ctx); + int event_count = result_ctx.event_count; - /* Apply edge-trigger simulation using helper */ + /* Apply edge-trigger simulation */ int nfds = php_poll_simulate_edge_trigger(backend_data->fd_table, events, event_count); - /* Handle oneshot removals after simulation */ + /* Handle oneshot removals */ for (int i = 0; i < nfds; 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) { @@ -306,11 +320,9 @@ static int select_backend_get_suitable_max_events(php_poll_ctx *ctx) return -1; } - /* For select(), we know exactly how many FDs are registered */ - int active_fds = backend_data->fd_table->count; + int active_fds = php_poll_fd_table_count(backend_data->fd_table); if (active_fds == 0) { - /* No active FDs, return 1 just to not pass empty events */ return 1; } diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index f7fb34f1474d3..e8e3ed7b68129 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -406,3 +406,47 @@ PHPAPI const char *php_poll_error_string(php_poll_error error) return "Unknown error"; } } + +/* Edge-trigger simulation helper */ +int php_poll_simulate_edge_trigger(php_poll_fd_table *table, php_poll_event *events, int nfds) +{ + int filtered_count = 0; + + for (int i = 0; i < nfds; i++) { + php_poll_fd_entry *entry = php_poll_fd_table_find(table, events[i].fd); + if (!entry) { + continue; + } + + uint32_t new_events = events[i].revents; + uint32_t reported_events = 0; + + if (entry->events & PHP_POLL_ET) { + /* Edge-triggered: report edges only */ + if ((new_events & PHP_POLL_READ) && !(entry->last_revents & PHP_POLL_READ)) { + reported_events |= PHP_POLL_READ; + } + if ((new_events & PHP_POLL_WRITE) && !(entry->last_revents & PHP_POLL_WRITE)) { + reported_events |= PHP_POLL_WRITE; + } + /* Always report error and hangup events */ + reported_events |= (new_events & (PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP)); + } else { + /* Level-triggered: report all active events */ + reported_events = new_events; + } + + entry->last_revents = new_events; + + /* Only include this event if we have something to report */ + if (reported_events != 0) { + if (filtered_count != i) { + events[filtered_count] = events[i]; + } + events[filtered_count].revents = reported_events; + filtered_count++; + } + } + + return filtered_count; +} diff --git a/main/poll/poll_fd_table.c b/main/poll/poll_fd_table.c index bcf2ce18cc7b2..cc5e84dba6499 100644 --- a/main/poll/poll_fd_table.c +++ b/main/poll/poll_fd_table.c @@ -14,7 +14,6 @@ #include "php_poll_internal.h" -/* Initialize FD table */ 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); @@ -26,39 +25,36 @@ php_poll_fd_table *php_poll_fd_table_init(int initial_capacity, bool persistent) initial_capacity = 64; } - table->entries = php_poll_calloc(initial_capacity, sizeof(php_poll_fd_entry), persistent); - if (!table->entries) { - pefree(table, persistent); - return NULL; - } - - table->capacity = initial_capacity; - table->count = 0; + _zend_hash_init(&table->entries_ht, initial_capacity, NULL, persistent); table->persistent = persistent; + return table; } -/* Cleanup FD table */ void php_poll_fd_table_cleanup(php_poll_fd_table *table) { if (table) { - pefree(table->entries, table->persistent); + 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); + pefree(entry, table->persistent); + } + ZEND_HASH_FOREACH_END(); + + zend_hash_destroy(&table->entries_ht); pefree(table, table->persistent); } } -/* Find FD entry */ php_poll_fd_entry *php_poll_fd_table_find(php_poll_fd_table *table, int fd) { - for (int i = 0; i < table->capacity; i++) { - if (table->entries[i].active && table->entries[i].fd == fd) { - return &table->entries[i]; - } - } - return NULL; + zval *zv = zend_hash_index_find(&table->entries_ht, (zend_ulong) fd); + return zv ? Z_PTR_P(zv) : NULL; } -/* Get or create FD 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); @@ -66,92 +62,53 @@ php_poll_fd_entry *php_poll_fd_table_get(php_poll_fd_table *table, int fd) return entry; } - /* Find empty slot */ - for (int i = 0; i < table->capacity; i++) { - if (!table->entries[i].active) { - table->entries[i].fd = fd; - table->entries[i].active = true; - table->entries[i].last_revents = 0; - table->count++; - return &table->entries[i]; - } - } - - /* Need to grow the array */ - int new_capacity = table->capacity * 2; - php_poll_fd_entry *new_entries = php_poll_realloc( - table->entries, new_capacity * sizeof(php_poll_fd_entry), table->persistent); - if (!new_entries) { + entry = php_poll_calloc(1, sizeof(php_poll_fd_entry), table->persistent); + if (!entry) { return NULL; } - /* Initialize new entries */ - memset(new_entries + table->capacity, 0, - (new_capacity - table->capacity) * sizeof(php_poll_fd_entry)); - - table->entries = new_entries; + entry->fd = fd; + entry->active = true; + entry->events = 0; + entry->data = NULL; + entry->last_revents = 0; - /* Use first new slot */ - php_poll_fd_entry *new_entry = &table->entries[table->capacity]; - new_entry->fd = fd; - new_entry->active = true; - new_entry->last_revents = 0; - table->count++; + zval zv; + ZVAL_PTR(&zv, entry); + if (!zend_hash_index_add(&table->entries_ht, (zend_ulong) fd, &zv)) { + pefree(entry, table->persistent); + return NULL; + } - table->capacity = new_capacity; - return new_entry; + return entry; } -/* Remove FD entry */ void php_poll_fd_table_remove(php_poll_fd_table *table, int fd) { - php_poll_fd_entry *entry = php_poll_fd_table_find(table, fd); - if (entry) { - entry->active = false; - table->count--; + zval *zv = zend_hash_index_find(&table->entries_ht, (zend_ulong) fd); + if (zv) { + php_poll_fd_entry *entry = Z_PTR_P(zv); + pefree(entry, table->persistent); + zend_hash_index_del(&table->entries_ht, (zend_ulong) fd); } } -/* Edge-trigger simulation helper */ -int php_poll_simulate_edge_trigger(php_poll_fd_table *table, php_poll_event *events, int nfds) -{ - int filtered_count = 0; - - for (int i = 0; i < nfds; i++) { - php_poll_fd_entry *entry = php_poll_fd_table_find(table, events[i].fd); - if (!entry) { - continue; - } - - uint32_t new_events = events[i].revents; - uint32_t reported_events = 0; - - if (entry->events & PHP_POLL_ET) { - /* Edge-triggered: report edges only */ - if ((new_events & PHP_POLL_READ) && !(entry->last_revents & PHP_POLL_READ)) { - reported_events |= PHP_POLL_READ; - } - if ((new_events & PHP_POLL_WRITE) && !(entry->last_revents & PHP_POLL_WRITE)) { - reported_events |= PHP_POLL_WRITE; - } - /* Always report error and hangup events */ - reported_events |= (new_events & (PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP)); - } else { - /* Level-triggered: report all active events */ - reported_events = new_events; - } +/* 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); - entry->last_revents = new_events; - - /* Only include this event if we have something to report */ - if (reported_events != 0) { - if (filtered_count != i) { - events[filtered_count] = events[i]; - } - events[filtered_count].revents = reported_events; - filtered_count++; +/* 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 */ } } - - return filtered_count; + ZEND_HASH_FOREACH_END(); } From 6728e29e57d0c87690c6cffb735e5aab920602db Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 23 Aug 2025 20:45:13 +0200 Subject: [PATCH 40/52] poll: refactore poll logic to use more optimally fd table --- main/poll/poll_backend_poll.c | 228 +++++++++++++++------------------- 1 file changed, 99 insertions(+), 129 deletions(-) diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 94d50fef2ce5e..a6f1e3eaef63a 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -21,10 +21,10 @@ #include typedef struct { - struct pollfd *fds; - int fds_capacity; - int fds_used; php_poll_fd_table *fd_table; + /* Temporary arrays allocated during poll_backend_wait() */ + struct pollfd *temp_fds; + int temp_fds_capacity; } poll_backend_data_t; static uint32_t poll_events_to_native(uint32_t events) @@ -63,28 +63,6 @@ static uint32_t poll_events_from_native(uint32_t native) return events; } -/* Find pollfd slot */ -static struct pollfd *poll_find_pollfd_slot(poll_backend_data_t *data, int fd) -{ - for (int i = 0; i < data->fds_capacity; i++) { - if (data->fds[i].fd == fd) { - return &data->fds[i]; - } - } - return NULL; -} - -/* Get empty pollfd slot */ -static struct pollfd *poll_get_empty_pollfd_slot(poll_backend_data_t *data) -{ - for (int i = 0; i < data->fds_capacity; i++) { - if (data->fds[i].fd == -1) { - return &data->fds[i]; - } - } - return NULL; -} - 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); @@ -93,30 +71,24 @@ static zend_result poll_backend_init(php_poll_ctx *ctx) 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->fds = php_poll_calloc(initial_capacity, sizeof(struct pollfd), ctx->persistent); - if (!data->fds) { + 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->fds_capacity = initial_capacity; - data->fds_used = 0; - data->fd_table = php_poll_fd_table_init(initial_capacity, ctx->persistent); - if (!data->fd_table) { - pefree(data->fds, ctx->persistent); + /* Pre-allocate temporary pollfd array */ + 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; } - - /* Initialize all fds to -1 */ - for (int i = 0; i < initial_capacity; i++) { - data->fds[i].fd = -1; - } + data->temp_fds_capacity = initial_capacity; ctx->backend_data = data; return SUCCESS; @@ -126,8 +98,8 @@ static void poll_backend_cleanup(php_poll_ctx *ctx) { poll_backend_data_t *data = (poll_backend_data_t *) ctx->backend_data; if (data) { - pefree(data->fds, ctx->persistent); php_poll_fd_table_cleanup(data->fd_table); + pefree(data->temp_fds, ctx->persistent); pefree(data, ctx->persistent); ctx->backend_data = NULL; } @@ -151,35 +123,6 @@ static zend_result poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, entry->events = events; entry->data = user_data; - /* Find empty pollfd slot */ - struct pollfd *pfd = poll_get_empty_pollfd_slot(backend_data); - if (!pfd) { - /* Need to grow the pollfd array */ - int new_capacity = backend_data->fds_capacity * 2; - struct pollfd *new_fds = php_poll_realloc( - backend_data->fds, new_capacity * sizeof(struct pollfd), ctx->persistent); - if (!new_fds) { - php_poll_fd_table_remove(backend_data->fd_table, fd); - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); - return FAILURE; - } - - /* Initialize new slots */ - for (int i = backend_data->fds_capacity; i < new_capacity; i++) { - new_fds[i].fd = -1; - } - - backend_data->fds = new_fds; - pfd = &backend_data->fds[backend_data->fds_capacity]; - backend_data->fds_capacity = new_capacity; - } - - /* Set up pollfd */ - pfd->fd = fd; - pfd->events = poll_events_to_native(events & ~(PHP_POLL_ET | PHP_POLL_ONESHOT)); - pfd->revents = 0; - backend_data->fds_used++; - return SUCCESS; } @@ -187,23 +130,15 @@ static zend_result poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t event { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; - /* Find existing entry using helper - O(1) instead of O(n) */ 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; - entry->last_revents = 0; /* Reset on modify */ - - /* Find pollfd and update */ - struct pollfd *pfd = poll_find_pollfd_slot(backend_data, fd); - if (pfd) { - pfd->events = poll_events_to_native(events & ~(PHP_POLL_ET | PHP_POLL_ONESHOT)); - } + entry->last_revents = 0; return SUCCESS; } @@ -217,40 +152,70 @@ static zend_result poll_backend_remove(php_poll_ctx *ctx, int fd) return FAILURE; } - /* Find and clear pollfd */ - struct pollfd *pfd = poll_find_pollfd_slot(backend_data, fd); - if (pfd) { - pfd->fd = -1; - pfd->events = 0; - pfd->revents = 0; - backend_data->fds_used--; - } - php_poll_fd_table_remove(backend_data->fd_table, fd); - return SUCCESS; } -/* Handle oneshot removal after event */ -static void poll_handle_oneshot_removal(poll_backend_data_t *backend_data, int fd) +/* Context for building pollfd array */ +typedef struct { + struct pollfd *fds; + int index; +} poll_build_context; + +/* Callback to build pollfd array from fd_table */ +static bool poll_build_fds_callback(int fd, php_poll_fd_entry *entry, void *user_data) { - /* Mark pollfd as disabled */ - struct pollfd *pfd = poll_find_pollfd_slot(backend_data, fd); - if (pfd) { - pfd->fd = -1; - pfd->events = 0; - pfd->revents = 0; - backend_data->fds_used--; + 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; +} + +/* Context for processing poll results */ +typedef struct { + poll_backend_data_t *backend_data; + struct pollfd *pollfds; + php_poll_event *events; + int max_events; + int event_count; +} poll_result_context; + +/* Callback to process poll results */ +static bool poll_process_results_callback(int fd, php_poll_fd_entry *entry, void *user_data) +{ + poll_result_context *ctx = (poll_result_context *) user_data; + + if (ctx->event_count >= ctx->max_events) { + return false; /* Stop if events array is full */ } - php_poll_fd_table_remove(backend_data->fd_table, fd); + + /* Find the corresponding pollfd entry */ + for (int i = 0; i < php_poll_fd_table_count(ctx->backend_data->fd_table); i++) { + if (ctx->pollfds[i].fd == fd && ctx->pollfds[i].revents != 0) { + ctx->events[ctx->event_count].fd = fd; + ctx->events[ctx->event_count].events = entry->events; + ctx->events[ctx->event_count].revents + = poll_events_from_native(ctx->pollfds[i].revents); + ctx->events[ctx->event_count].data = entry->data; + ctx->event_count++; + break; + } + } + + return true; } static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; - if (backend_data->fds_used == 0) { - /* No FDs to monitor, but respect timeout */ + int fd_count = php_poll_fd_table_count(backend_data->fd_table); + if (fd_count == 0) { if (timeout > 0) { struct timespec ts; ts.tv_sec = timeout / 1000; @@ -260,35 +225,45 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ return 0; } - int nfds = poll(backend_data->fds, backend_data->fds_capacity, timeout); - - if (nfds > 0) { - int event_count = 0; - for (int i = 0; i < backend_data->fds_capacity && event_count < max_events; i++) { - if (backend_data->fds[i].fd != -1 && backend_data->fds[i].revents != 0) { - php_poll_fd_entry *entry - = php_poll_fd_table_find(backend_data->fd_table, backend_data->fds[i].fd); - if (entry) { - events[event_count].fd = backend_data->fds[i].fd; - events[event_count].events = entry->events; - events[event_count].revents - = poll_events_from_native(backend_data->fds[i].revents); - events[event_count].data = entry->data; - event_count++; - } - - backend_data->fds[i].revents = 0; /* Clear for next poll */ - } + /* 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 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); - /* Apply edge-trigger simulation using helper */ + /* Call poll() */ + int nfds = poll(backend_data->temp_fds, fd_count, timeout); + + if (nfds > 0) { + /* Process results */ + poll_result_context result_ctx = { .backend_data = backend_data, + .pollfds = backend_data->temp_fds, + .events = events, + .max_events = max_events, + .event_count = 0 }; + + php_poll_fd_table_foreach( + backend_data->fd_table, poll_process_results_callback, &result_ctx); + int event_count = result_ctx.event_count; + + /* Apply edge-trigger simulation */ nfds = php_poll_simulate_edge_trigger(backend_data->fd_table, events, event_count); - /* Handle oneshot removals after simulation */ + /* Handle oneshot removals */ for (int i = 0; i < nfds; 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) { - poll_handle_oneshot_removal(backend_data, events[i].fd); + php_poll_fd_table_remove(backend_data->fd_table, events[i].fd); } } } @@ -298,7 +273,7 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ static bool poll_backend_is_available(void) { - return true; /* poll() is always available */ + return true; } static int poll_backend_get_suitable_max_events(php_poll_ctx *ctx) @@ -310,12 +285,7 @@ static int poll_backend_get_suitable_max_events(php_poll_ctx *ctx) } int active_fds = php_poll_fd_table_count(backend_data->fd_table); - - if (active_fds == 0) { - return 1; - } - - return active_fds; + return active_fds == 0 ? 1 : active_fds; } const php_poll_backend_ops php_poll_backend_poll_ops = { @@ -329,7 +299,7 @@ const php_poll_backend_ops php_poll_backend_poll_ops = { .wait = poll_backend_wait, .is_available = poll_backend_is_available, .get_suitable_max_events = poll_backend_get_suitable_max_events, - .supports_et = false, /* poll() doesn't support ET natively, but we simulate it */ + .supports_et = false, }; #endif /* HAVE_POLL */ From 5203f901e190dc3f44afa2a8b4efe337ca25646c Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 23 Aug 2025 21:56:36 +0200 Subject: [PATCH 41/52] poll: remove incorrect edge triggering simulation --- ext/standard/tests/streams/stream_poll.inc | 19 +++++--- .../stream_poll_basic_sock_rw_multi_edge.phpt | 5 +++ ...stream_poll_basic_sock_rw_single_edge.phpt | 2 +- main/poll/php_poll_internal.h | 3 -- main/poll/poll_backend_poll.c | 13 ++++-- main/poll/poll_backend_select.c | 17 ++++--- main/poll/poll_core.c | 44 ------------------- 7 files changed, 42 insertions(+), 61 deletions(-) diff --git a/ext/standard/tests/streams/stream_poll.inc b/ext/standard/tests/streams/stream_poll.inc index 208100301cadd..2b09cc1949386 100644 --- a/ext/standard/tests/streams/stream_poll.inc +++ b/ext/standard/tests/streams/stream_poll.inc @@ -1,5 +1,19 @@ --FILE-- --EXPECT-- Events matched - count: 1 -Events matched - count: 2 +Events matched - count: 1 diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h index c6535cf1ba088..0bc3d86e10f70 100644 --- a/main/poll/php_poll_internal.h +++ b/main/poll/php_poll_internal.h @@ -119,9 +119,6 @@ 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); -/* Edge triggering simulation helper */ -int php_poll_simulate_edge_trigger(php_poll_fd_table *table, php_poll_event *events, int nfds); - /* Error helper functions */ php_poll_error php_poll_errno_to_error(int err); diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index a6f1e3eaef63a..67fadb40e035e 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -109,6 +109,11 @@ static zend_result poll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, { 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; @@ -130,6 +135,11 @@ static zend_result poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t event { 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); @@ -256,9 +266,6 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ backend_data->fd_table, poll_process_results_callback, &result_ctx); int event_count = result_ctx.event_count; - /* Apply edge-trigger simulation */ - nfds = php_poll_simulate_edge_trigger(backend_data->fd_table, events, event_count); - /* Handle oneshot removals */ for (int i = 0; i < nfds; i++) { php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, events[i].fd); diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c index 0e51bb659dfa4..db082323dcf98 100644 --- a/main/poll/poll_backend_select.c +++ b/main/poll/poll_backend_select.c @@ -94,6 +94,11 @@ static zend_result select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; + if (events & PHP_POLL_ET) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOSUPPORT); + return FAILURE; + } + #ifdef FD_SETSIZE if (sock >= FD_SETSIZE) { php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); @@ -139,6 +144,11 @@ static zend_result select_backend_modify( select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; php_socket_t sock = (php_socket_t) fd; + 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); @@ -293,18 +303,15 @@ static int select_backend_wait( php_poll_fd_table_foreach(backend_data->fd_table, process_select_result_callback, &result_ctx); int event_count = result_ctx.event_count; - /* Apply edge-trigger simulation */ - int nfds = php_poll_simulate_edge_trigger(backend_data->fd_table, events, event_count); - /* Handle oneshot removals */ - for (int i = 0; i < nfds; i++) { + 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) { select_handle_oneshot_removal(backend_data, events[i].fd); } } - return nfds; + return event_count; } static bool select_backend_is_available(void) diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index e8e3ed7b68129..f7fb34f1474d3 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -406,47 +406,3 @@ PHPAPI const char *php_poll_error_string(php_poll_error error) return "Unknown error"; } } - -/* Edge-trigger simulation helper */ -int php_poll_simulate_edge_trigger(php_poll_fd_table *table, php_poll_event *events, int nfds) -{ - int filtered_count = 0; - - for (int i = 0; i < nfds; i++) { - php_poll_fd_entry *entry = php_poll_fd_table_find(table, events[i].fd); - if (!entry) { - continue; - } - - uint32_t new_events = events[i].revents; - uint32_t reported_events = 0; - - if (entry->events & PHP_POLL_ET) { - /* Edge-triggered: report edges only */ - if ((new_events & PHP_POLL_READ) && !(entry->last_revents & PHP_POLL_READ)) { - reported_events |= PHP_POLL_READ; - } - if ((new_events & PHP_POLL_WRITE) && !(entry->last_revents & PHP_POLL_WRITE)) { - reported_events |= PHP_POLL_WRITE; - } - /* Always report error and hangup events */ - reported_events |= (new_events & (PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP)); - } else { - /* Level-triggered: report all active events */ - reported_events = new_events; - } - - entry->last_revents = new_events; - - /* Only include this event if we have something to report */ - if (reported_events != 0) { - if (filtered_count != i) { - events[filtered_count] = events[i]; - } - events[filtered_count].revents = reported_events; - filtered_count++; - } - } - - return filtered_count; -} From 709568fd0c89d6f7e90c284069c3ab65b244f1f4 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 13:22:51 +0200 Subject: [PATCH 42/52] poll: fix test reporting of unpexpected events when array present --- ext/standard/tests/streams/stream_poll.inc | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/ext/standard/tests/streams/stream_poll.inc b/ext/standard/tests/streams/stream_poll.inc index 2b09cc1949386..a573224135930 100644 --- a/ext/standard/tests/streams/stream_poll.inc +++ b/ext/standard/tests/streams/stream_poll.inc @@ -102,6 +102,7 @@ function pt_print_events($events, $read_data = false): void { echo "\n"; } } + function pt_expect_events($events, $expected, $poll_ctx = null): void { if (!is_array($events)) { die("Events must be an array\n"); @@ -114,15 +115,15 @@ function pt_expect_events($events, $expected, $poll_ctx = null): void { $event_count = count($events); $expected_count = count($expected); + // Get current backend name for backend-specific expectations + $backend_name = $poll_ctx ? stream_poll_backend_name($poll_ctx) : 'unknown'; + if ($event_count !== $expected_count) { echo "Event count mismatch: got $event_count, expected $expected_count\n"; - pt_print_mismatched_events($events, $expected); + pt_print_mismatched_events($events, $expected, [], $backend_name); return; } - // Get current backend name for backend-specific expectations - $backend_name = $poll_ctx ? stream_poll_backend_name($poll_ctx) : 'unknown'; - // Convert events to comparable format for matching $actual_events = []; foreach ($events as $event) { @@ -190,7 +191,7 @@ function pt_expect_events($events, $expected, $poll_ctx = null): void { echo "Events matched - count: $event_count\n"; } else { echo "Events did not match:\n"; - pt_print_mismatched_events($events, $resolved_expected, $matched); + pt_print_mismatched_events($events, $expected, $matched, $backend_name); } } @@ -229,7 +230,7 @@ function pt_event_flags_to_string($flags): string { return empty($names) ? 'NONE' : implode('|', $names); } -function pt_print_mismatched_events($actual_events, $expected_events, $matched = []): void { +function pt_print_mismatched_events($actual_events, $expected_events, $matched = [], $backend_name = null): void { echo "Actual events:\n"; foreach ($actual_events as $i => $event) { $match_status = isset($matched[$i]) ? " [MATCHED]" : " [UNMATCHED]"; @@ -241,7 +242,13 @@ function pt_print_mismatched_events($actual_events, $expected_events, $matched = foreach ($expected_events as $i => $exp_event) { $was_matched = in_array($i, $matched); $match_status = $was_matched ? " [MATCHED]" : " [UNMATCHED]"; - $event_names = pt_event_flags_to_string($exp_event['events']); + + $events_value = $exp_event['events']; + if (is_array($events_value) && $backend_name !== null) { + $events_value = pt_resolve_backend_specific_value($events_value, $backend_name); + } + + $event_names = pt_event_flags_to_string($events_value); echo " Event[$i]: $event_names, user data: " . $exp_event['data']; if (isset($exp_event['read'])) { echo ", read data: '" . $exp_event['read'] . "'"; From 4545a3c47d78767051ba15bcac143d1d9c110598 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 13:46:27 +0200 Subject: [PATCH 43/52] poll: fix, refactore and simplify poll backend --- ext/standard/tests/streams/stream_poll.inc | 2 +- .../stream_poll_basic_sock_rw_multi_edge.phpt | 2 +- ...stream_poll_basic_sock_rw_single_edge.phpt | 5 ++ main/poll/poll_backend_poll.c | 90 ++++++++----------- 4 files changed, 42 insertions(+), 57 deletions(-) diff --git a/ext/standard/tests/streams/stream_poll.inc b/ext/standard/tests/streams/stream_poll.inc index a573224135930..7029395d92b3f 100644 --- a/ext/standard/tests/streams/stream_poll.inc +++ b/ext/standard/tests/streams/stream_poll.inc @@ -227,7 +227,7 @@ function pt_event_flags_to_string($flags): string { if ($flags & STREAM_POLL_ERROR) $names[] = 'ERROR'; if ($flags & STREAM_POLL_HUP) $names[] = 'HUP'; - return empty($names) ? 'NONE' : implode('|', $names); + return empty($names) ? 'NONE:' . $flags : implode('|', $names); } function pt_print_mismatched_events($actual_events, $expected_events, $matched = [], $backend_name = null): void { diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt index cb479840034d5..7f43b98945c65 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt @@ -3,7 +3,7 @@ Stream polling - socket write / read multiple times with edge triggering --SKIPIF-- --FILE-- --FILE-- temp_fds = php_poll_calloc(initial_capacity, sizeof(struct pollfd), ctx->persistent); if (!data->temp_fds) { php_poll_fd_table_cleanup(data->fd_table); @@ -148,7 +149,6 @@ static zend_result poll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t event entry->events = events; entry->data = user_data; - entry->last_revents = 0; return SUCCESS; } @@ -186,40 +186,6 @@ static bool poll_build_fds_callback(int fd, php_poll_fd_entry *entry, void *user return true; } -/* Context for processing poll results */ -typedef struct { - poll_backend_data_t *backend_data; - struct pollfd *pollfds; - php_poll_event *events; - int max_events; - int event_count; -} poll_result_context; - -/* Callback to process poll results */ -static bool poll_process_results_callback(int fd, php_poll_fd_entry *entry, void *user_data) -{ - poll_result_context *ctx = (poll_result_context *) user_data; - - if (ctx->event_count >= ctx->max_events) { - return false; /* Stop if events array is full */ - } - - /* Find the corresponding pollfd entry */ - for (int i = 0; i < php_poll_fd_table_count(ctx->backend_data->fd_table); i++) { - if (ctx->pollfds[i].fd == fd && ctx->pollfds[i].revents != 0) { - ctx->events[ctx->event_count].fd = fd; - ctx->events[ctx->event_count].events = entry->events; - ctx->events[ctx->event_count].revents - = poll_events_from_native(ctx->pollfds[i].revents); - ctx->events[ctx->event_count].data = entry->data; - ctx->event_count++; - break; - } - } - - return true; -} - static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; @@ -254,28 +220,42 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ /* Call poll() */ int nfds = poll(backend_data->temp_fds, fd_count, timeout); - if (nfds > 0) { - /* Process results */ - poll_result_context result_ctx = { .backend_data = backend_data, - .pollfds = backend_data->temp_fds, - .events = events, - .max_events = max_events, - .event_count = 0 }; - - php_poll_fd_table_foreach( - backend_data->fd_table, poll_process_results_callback, &result_ctx); - int event_count = result_ctx.event_count; - - /* Handle oneshot removals */ - for (int i = 0; i < nfds; 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); + if (nfds <= 0) { + return nfds; /* Return 0 for timeout, -1 for error */ + } + + /* Process results - iterate through 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++; } } } - return nfds; + /* 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) From 3f690a1d20e7a28b2d69053e1b07e500e8551a07 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 13:52:37 +0200 Subject: [PATCH 44/52] Make poll always available --- main/poll/poll_backend_poll.c | 8 ++------ main/poll/poll_core.c | 6 +----- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 16682d6e03e96..445685b84c83d 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -14,8 +14,6 @@ #include "php_poll_internal.h" -#ifdef HAVE_POLL - #include #include #include @@ -217,8 +215,8 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ poll_build_context build_ctx = { .fds = backend_data->temp_fds, .index = 0 }; php_poll_fd_table_foreach(backend_data->fd_table, poll_build_fds_callback, &build_ctx); - /* Call poll() */ - int nfds = poll(backend_data->temp_fds, fd_count, timeout); + /* Call poll() or its emulation (Windows) */ + int nfds = php_poll2(backend_data->temp_fds, fd_count, timeout); if (nfds <= 0) { return nfds; /* Return 0 for timeout, -1 for error */ @@ -288,5 +286,3 @@ const php_poll_backend_ops php_poll_backend_poll_ops = { .get_suitable_max_events = poll_backend_get_suitable_max_events, .supports_et = false, }; - -#endif /* HAVE_POLL */ diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index f7fb34f1474d3..02950428e6dcc 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -20,9 +20,7 @@ static int num_registered_backends = 0; /* Forward declarations for backend ops */ -#ifdef HAVE_POLL extern const php_poll_backend_ops php_poll_backend_poll_ops; -#endif #ifdef HAVE_EPOLL extern const php_poll_backend_ops php_poll_backend_epoll_ops; #endif @@ -68,10 +66,8 @@ PHPAPI void php_poll_register_backends(void) } #endif -#ifdef HAVE_POLL - /* Poll is available on Unix-like systems */ + /* Poll or its emulation is always available */ registered_backends[num_registered_backends++] = &php_poll_backend_poll_ops; -#endif /* select() as a fallback */ if (php_poll_backend_select_ops.is_available()) { From f9cf30eb80889f664cce8189bcbb96d3f60e2102 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 13:59:26 +0200 Subject: [PATCH 45/52] poll: remove select backend as it is broken and not needed --- build/php.m4 | 4 +- configure.ac | 1 - main/poll/poll_backend_select.c | 351 -------------------------------- main/poll/poll_core.c | 8 +- win32/build/config.w32 | 2 +- 5 files changed, 4 insertions(+), 362 deletions(-) delete mode 100644 main/poll/poll_backend_select.c diff --git a/build/php.m4 b/build/php.m4 index 4401da03a7b7c..d6c70337baab2 100644 --- a/build/php.m4 +++ b/build/php.m4 @@ -1407,8 +1407,8 @@ AC_DEFUN([PHP_POLL_MECHANISMS], poll_mechanisms="$poll_mechanisms eventport" ]) - dnl Set poll mechanisms including poll and select that are always available - poll_mechanisms="$poll_mechanisms poll select" + dnl Set poll mechanisms including poll that is always available + poll_mechanisms="$poll_mechanisms poll" AC_MSG_RESULT([$poll_mechanisms]) ]) diff --git a/configure.ac b/configure.ac index a70fd1c1208ae..619e85e839a80 100644 --- a/configure.ac +++ b/configure.ac @@ -1683,7 +1683,6 @@ PHP_ADD_SOURCES([main/poll], m4_normalize([ poll_backend_eventport.c poll_backend_kqueue.c poll_backend_poll.c - poll_backend_select.c poll_core.c poll_fd_table.c ]), diff --git a/main/poll/poll_backend_select.c b/main/poll/poll_backend_select.c deleted file mode 100644 index db082323dcf98..0000000000000 --- a/main/poll/poll_backend_select.c +++ /dev/null @@ -1,351 +0,0 @@ -/* - +----------------------------------------------------------------------+ - | Copyright © The PHP Group and Contributors. | - +----------------------------------------------------------------------+ - | This source file is subject to the Modified BSD License that is | - | bundled with this package in the file LICENSE, and is available | - | through the World Wide Web at . | - | | - | SPDX-License-Identifier: BSD-3-Clause | - +----------------------------------------------------------------------+ - | Authors: Jakub Zelenka | - +----------------------------------------------------------------------+ -*/ - -#include "php_poll_internal.h" -#include "php_network.h" - -#include "php_poll_internal.h" -#include "php_network.h" - -typedef struct { - fd_set read_fds, write_fds, error_fds; - fd_set master_read_fds, master_write_fds, master_error_fds; - php_poll_fd_table *fd_table; - php_socket_t max_fd; -} select_backend_data_t; - -/* Select-specific helper functions */ -static bool find_max_fd_callback(int fd, php_poll_fd_entry *entry, void *user_data) -{ - php_socket_t *max_fd = (php_socket_t *) user_data; - php_socket_t sock = (php_socket_t) fd; - - if (sock > *max_fd) { - *max_fd = sock; - } - return true; -} - -static php_socket_t select_get_max_fd(php_poll_fd_table *table) -{ - php_socket_t max_fd = 0; - php_poll_fd_table_foreach(table, find_max_fd_callback, &max_fd); - return max_fd; -} - -/* Update max_fd for select() */ -static void select_update_max_fd(select_backend_data_t *data) -{ - data->max_fd = select_get_max_fd(data->fd_table); -} - -static zend_result select_backend_init(php_poll_ctx *ctx) -{ - select_backend_data_t *data - = php_poll_calloc(1, sizeof(select_backend_data_t), ctx->persistent); - if (!data) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); - 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; - - /* Initialize FD tracking using helper - now uses HashTable internally */ - 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; - } - - FD_ZERO(&data->master_read_fds); - FD_ZERO(&data->master_write_fds); - FD_ZERO(&data->master_error_fds); - data->max_fd = 0; - - ctx->backend_data = data; - return SUCCESS; -} - -static void select_backend_cleanup(php_poll_ctx *ctx) -{ - select_backend_data_t *data = (select_backend_data_t *) ctx->backend_data; - if (data) { - php_poll_fd_table_cleanup(data->fd_table); - pefree(data, ctx->persistent); - ctx->backend_data = NULL; - } -} - -static zend_result select_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *user_data) -{ - select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; - php_socket_t sock = (php_socket_t) fd; - - if (events & PHP_POLL_ET) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOSUPPORT); - return FAILURE; - } - -#ifdef FD_SETSIZE - if (sock >= FD_SETSIZE) { - php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); - return FAILURE; - } -#endif - - if (php_poll_fd_table_find(backend_data->fd_table, fd)) { - php_poll_set_error(ctx, PHP_POLL_ERR_EXISTS); - return FAILURE; - } - - php_poll_fd_entry *entry = php_poll_fd_table_get(backend_data->fd_table, fd); - if (!entry) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); - return FAILURE; - } - - entry->events = events; - entry->data = user_data; - - /* Add to appropriate fd_sets */ - if (events & PHP_POLL_READ) { - FD_SET(sock, &backend_data->master_read_fds); - } - if (events & PHP_POLL_WRITE) { - FD_SET(sock, &backend_data->master_write_fds); - } - /* Always monitor for errors */ - FD_SET(sock, &backend_data->master_error_fds); - - /* Update max_fd */ - if (sock > backend_data->max_fd) { - backend_data->max_fd = sock; - } - - return SUCCESS; -} - -static zend_result select_backend_modify( - php_poll_ctx *ctx, int fd, uint32_t events, void *user_data) -{ - select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; - php_socket_t sock = (php_socket_t) fd; - - 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; - entry->last_revents = 0; - - /* Remove from all sets first */ - FD_CLR(sock, &backend_data->master_read_fds); - FD_CLR(sock, &backend_data->master_write_fds); - FD_CLR(sock, &backend_data->master_error_fds); - - /* Add back based on new events */ - if (events & PHP_POLL_READ) { - FD_SET(sock, &backend_data->master_read_fds); - } - if (events & PHP_POLL_WRITE) { - FD_SET(sock, &backend_data->master_write_fds); - } - FD_SET(sock, &backend_data->master_error_fds); - - return SUCCESS; -} - -static zend_result select_backend_remove(php_poll_ctx *ctx, int fd) -{ - select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; - php_socket_t sock = (php_socket_t) fd; - - if (!php_poll_fd_table_find(backend_data->fd_table, fd)) { - php_poll_set_error(ctx, PHP_POLL_ERR_NOTFOUND); - return FAILURE; - } - - FD_CLR(sock, &backend_data->master_read_fds); - FD_CLR(sock, &backend_data->master_write_fds); - FD_CLR(sock, &backend_data->master_error_fds); - - php_poll_fd_table_remove(backend_data->fd_table, fd); - - if (sock == backend_data->max_fd) { - select_update_max_fd(backend_data); - } - - return SUCCESS; -} - -static void select_handle_oneshot_removal(select_backend_data_t *backend_data, int fd) -{ - php_socket_t sock = (php_socket_t) fd; - - FD_CLR(sock, &backend_data->master_read_fds); - FD_CLR(sock, &backend_data->master_write_fds); - FD_CLR(sock, &backend_data->master_error_fds); - - php_poll_fd_table_remove(backend_data->fd_table, fd); - - if (sock == backend_data->max_fd) { - select_update_max_fd(backend_data); - } -} - -/* Callback function for processing select() results */ -typedef struct { - select_backend_data_t *backend_data; - php_poll_event *events; - int max_events; - int event_count; -} select_result_context; - -static bool process_select_result_callback(int fd, php_poll_fd_entry *entry, void *user_data) -{ - select_result_context *ctx = (select_result_context *) user_data; - - if (ctx->event_count >= ctx->max_events) { - return false; /* Stop iteration, events array is full */ - } - - php_socket_t sock = (php_socket_t) fd; - uint32_t revents = 0; - - if (FD_ISSET(sock, &ctx->backend_data->read_fds)) { - revents |= PHP_POLL_READ; - } - if (FD_ISSET(sock, &ctx->backend_data->write_fds)) { - revents |= PHP_POLL_WRITE; - } - if (FD_ISSET(sock, &ctx->backend_data->error_fds)) { - revents |= PHP_POLL_ERROR; - } - - if (revents != 0) { - ctx->events[ctx->event_count].fd = fd; - ctx->events[ctx->event_count].events = entry->events; - ctx->events[ctx->event_count].revents = revents; - ctx->events[ctx->event_count].data = entry->data; - ctx->event_count++; - } - - return true; /* Continue iteration */ -} - -static int select_backend_wait( - php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) -{ - select_backend_data_t *backend_data = (select_backend_data_t *) ctx->backend_data; - - if (php_poll_fd_table_is_empty(backend_data->fd_table)) { - /* No sockets to wait for, but respect timeout */ - if (timeout > 0) { -#ifdef _WIN32 - Sleep(timeout); -#else - struct timespec ts; - ts.tv_sec = timeout / 1000; - ts.tv_nsec = (timeout % 1000) * 1000000; - nanosleep(&ts, NULL); -#endif - } - return 0; - } - - /* Copy master sets */ - memcpy(&backend_data->read_fds, &backend_data->master_read_fds, sizeof(fd_set)); - memcpy(&backend_data->write_fds, &backend_data->master_write_fds, sizeof(fd_set)); - memcpy(&backend_data->error_fds, &backend_data->master_error_fds, sizeof(fd_set)); - - /* Setup timeout */ - struct timeval tv = { 0 }, *ptv = NULL; - if (timeout >= 0) { - tv.tv_sec = timeout / 1000; - tv.tv_usec = (timeout % 1000) * 1000; - ptv = &tv; - } - - /* Call select() */ - int result = select((int) (backend_data->max_fd + 1), &backend_data->read_fds, - &backend_data->write_fds, &backend_data->error_fds, ptv); - - if (result <= 0) { - return (result == 0) ? 0 : -1; - } - - /* Process results using iteration over active FDs only */ - select_result_context result_ctx = { - .backend_data = backend_data, .events = events, .max_events = max_events, .event_count = 0 - }; - - php_poll_fd_table_foreach(backend_data->fd_table, process_select_result_callback, &result_ctx); - int event_count = result_ctx.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) { - select_handle_oneshot_removal(backend_data, events[i].fd); - } - } - - return event_count; -} - -static bool select_backend_is_available(void) -{ - return true; /* select() is always available */ -} - -static int select_backend_get_suitable_max_events(php_poll_ctx *ctx) -{ - select_backend_data_t *backend_data = (select_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); - - if (active_fds == 0) { - return 1; - } - - return active_fds; -} - -const php_poll_backend_ops php_poll_backend_select_ops = { - .type = PHP_POLL_BACKEND_SELECT, - .name = "select", - .init = select_backend_init, - .cleanup = select_backend_cleanup, - .add = select_backend_add, - .modify = select_backend_modify, - .remove = select_backend_remove, - .wait = select_backend_wait, - .is_available = select_backend_is_available, - .get_suitable_max_events = select_backend_get_suitable_max_events, - .supports_et = false /* select() doesn't support ET natively, but we simulate it */ -}; diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 02950428e6dcc..71bbd37290a23 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -20,7 +20,6 @@ static int num_registered_backends = 0; /* Forward declarations for backend ops */ -extern const php_poll_backend_ops php_poll_backend_poll_ops; #ifdef HAVE_EPOLL extern const php_poll_backend_ops php_poll_backend_epoll_ops; #endif @@ -33,7 +32,7 @@ extern const php_poll_backend_ops php_poll_backend_eventport_ops; #ifdef _WIN32 extern const php_poll_backend_ops php_poll_backend_iocp_ops; #endif -extern const php_poll_backend_ops php_poll_backend_select_ops; +extern const php_poll_backend_ops php_poll_backend_poll_ops; /* Register all available backends */ PHPAPI void php_poll_register_backends(void) @@ -68,11 +67,6 @@ PHPAPI void php_poll_register_backends(void) /* Poll or its emulation is always available */ registered_backends[num_registered_backends++] = &php_poll_backend_poll_ops; - - /* select() as a fallback */ - if (php_poll_backend_select_ops.is_available()) { - registered_backends[num_registered_backends++] = &php_poll_backend_select_ops; - } } /* Get backend operations */ diff --git a/win32/build/config.w32 b/win32/build/config.w32 index b6409063c080f..9fd56b142974f 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -298,7 +298,7 @@ AC_DEFINE('HAVE_STRNLEN', 1); AC_DEFINE('ZEND_CHECK_STACK_LIMIT', 1) -ADD_SOURCES("main/poll", "poll_backend_iocp.c poll_backend_select.c poll_core.c \ +ADD_SOURCES("main/poll", "poll_backend_iocp.c poll_backend_poll.c poll_core.c \ poll_fd_table.c"); ADD_FLAG("CFLAGS_BD_MAIN_POLL", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); From 8b2ebb4e7a50a781ddb80a8642495729f159d845 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 14:36:21 +0200 Subject: [PATCH 46/52] poll: fix compilation issues --- main/poll/poll_backend_poll.c | 4 ---- main/poll/poll_core.c | 8 ++++---- main/poll/poll_fd_table.c | 3 +-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 445685b84c83d..206d79e99fec3 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -14,10 +14,6 @@ #include "php_poll_internal.h" -#include -#include -#include - typedef struct { php_poll_fd_table *fd_table; struct pollfd *temp_fds; diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 71bbd37290a23..1a9c869f9c25b 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -351,11 +351,11 @@ php_poll_error php_poll_errno_to_error(int err) #ifdef ENOSYS case ENOSYS: #endif -#ifdef EOPNOTSUPP - case EOPNOTSUPP: -#endif -#ifdef ENOTSUP +#if ENOTSUP case ENOTSUP: +#endif +#if defined(EOPNOTSUPP) && EOPNOTSUPP != ENOTSUP + case EOPNOTSUPP: #endif return PHP_POLL_ERR_NOSUPPORT; diff --git a/main/poll/poll_fd_table.c b/main/poll/poll_fd_table.c index cc5e84dba6499..841bfcca8912f 100644 --- a/main/poll/poll_fd_table.c +++ b/main/poll/poll_fd_table.c @@ -34,10 +34,9 @@ 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) { if (table) { - zend_ulong fd; zval *zv; - ZEND_HASH_FOREACH_NUM_KEY_VAL(&table->entries_ht, fd, zv) + ZEND_HASH_FOREACH_VAL(&table->entries_ht, zv) { php_poll_fd_entry *entry = Z_PTR_P(zv); pefree(entry, table->persistent); From ee0140731454b98b8422ba1f95c0e5ae0639315f Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 19:31:41 +0200 Subject: [PATCH 47/52] poll: use STREAM_POLL_WRITE|STREAM_POLL_HUP as default for HOP epoll also uses it --- .../tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt | 2 +- .../tests/streams/stream_poll_basic_sock_rw_multi_level.phpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt index 7f43b98945c65..56154f57fc396 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_edge.phpt @@ -42,7 +42,7 @@ pt_expect_events(stream_poll_wait($poll_ctx, 100), [ fclose($socket1r); pt_expect_events(stream_poll_wait($poll_ctx, 100), [ [ - 'events' => ['kqueue' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'default' => STREAM_POLL_HUP], + 'events' => ['default' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'poll' => STREAM_POLL_HUP], 'data' => 'socket2_data' ] ], $poll_ctx); diff --git a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt index f455eb40e5806..b7a05d6ce3904 100644 --- a/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt +++ b/ext/standard/tests/streams/stream_poll_basic_sock_rw_multi_level.phpt @@ -44,7 +44,7 @@ pt_expect_events(stream_poll_wait($poll_ctx, 100), [ fclose($socket1r); pt_expect_events(stream_poll_wait($poll_ctx, 100), [ [ - 'events' => ['kqueue' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'default' => STREAM_POLL_HUP], + 'events' => ['default' => STREAM_POLL_WRITE|STREAM_POLL_HUP, 'poll' => STREAM_POLL_HUP], 'data' => 'socket2_data' ] ], $poll_ctx); From 63a3af4fdeac187028248d16ea6b0789791cb25a Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 19:37:26 +0200 Subject: [PATCH 48/52] poll: use php_pollfd instead of struct pollfd --- main/poll/poll_backend_poll.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 206d79e99fec3..049ebaa5766a9 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -16,7 +16,7 @@ typedef struct { php_poll_fd_table *fd_table; - struct pollfd *temp_fds; + php_pollfd *temp_fds; int temp_fds_capacity; } poll_backend_data_t; @@ -76,7 +76,7 @@ static zend_result poll_backend_init(php_poll_ctx *ctx) return FAILURE; } - data->temp_fds = php_poll_calloc(initial_capacity, sizeof(struct pollfd), ctx->persistent); + data->temp_fds = php_poll_calloc(initial_capacity, sizeof(php_pollfd), ctx->persistent); if (!data->temp_fds) { php_poll_fd_table_cleanup(data->fd_table); pefree(data, ctx->persistent); @@ -160,13 +160,13 @@ static zend_result poll_backend_remove(php_poll_ctx *ctx, int fd) return SUCCESS; } -/* Context for building pollfd array */ +/* Context for building php_pollfd array */ typedef struct { - struct pollfd *fds; + php_pollfd *fds; int index; } poll_build_context; -/* Callback to build pollfd array from fd_table */ +/* Callback to build php_pollfd array from fd_table */ static bool poll_build_fds_callback(int fd, php_poll_fd_entry *entry, void *user_data) { poll_build_context *ctx = (poll_build_context *) user_data; @@ -197,8 +197,8 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ /* 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); + php_pollfd *new_fds = php_poll_realloc( + backend_data->temp_fds, fd_count * sizeof(php_pollfd), ctx->persistent); if (!new_fds) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); return -1; @@ -207,7 +207,7 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ backend_data->temp_fds_capacity = fd_count; } - /* Build pollfd array from fd_table */ + /* Build php_pollfd array from fd_table */ poll_build_context build_ctx = { .fds = backend_data->temp_fds, .index = 0 }; php_poll_fd_table_foreach(backend_data->fd_table, poll_build_fds_callback, &build_ctx); @@ -218,10 +218,10 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ return nfds; /* Return 0 for timeout, -1 for error */ } - /* Process results - iterate through pollfd array directly */ + /* Process results - iterate through php_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]; + php_pollfd *pfd = &backend_data->temp_fds[i]; if (pfd->revents != 0) { php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, pfd->fd); From 1babc8a119c5a9377daee2e27b5868e822816cc4 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 19:38:06 +0200 Subject: [PATCH 49/52] poll: remove select backend constants and fix backend name test --- ext/standard/stream_poll.stub.php | 5 ----- .../tests/streams/stream_poll_backend_name_basic.phpt | 8 ++++---- main/php_poll.h | 1 - 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/ext/standard/stream_poll.stub.php b/ext/standard/stream_poll.stub.php index cd47668ae9c7b..4331c2c425e84 100644 --- a/ext/standard/stream_poll.stub.php +++ b/ext/standard/stream_poll.stub.php @@ -63,11 +63,6 @@ * @cvalue PHP_POLL_BACKEND_EVENTPORT */ const STREAM_POLL_BACKEND_EVENTPORT = UNKNOWN; -/** - * @var int - * @cvalue PHP_POLL_BACKEND_SELECT - */ -const STREAM_POLL_BACKEND_SELECT = UNKNOWN; /** * @var int * @cvalue PHP_POLL_BACKEND_IOCP diff --git a/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt b/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt index 3fb06a19a3367..401350b63f134 100644 --- a/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt +++ b/ext/standard/tests/streams/stream_poll_backend_name_basic.phpt @@ -4,13 +4,13 @@ Stream polling - backend name --EXPECT-- -string(6) "select" -string(6) "select" +string(4) "poll" +string(4) "poll" diff --git a/main/php_poll.h b/main/php_poll.h index dd3317fb996ca..cffffb706ace9 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -35,7 +35,6 @@ typedef enum { PHP_POLL_BACKEND_EPOLL, PHP_POLL_BACKEND_KQUEUE, PHP_POLL_BACKEND_EVENTPORT, - PHP_POLL_BACKEND_SELECT, PHP_POLL_BACKEND_IOCP } php_poll_backend_type; From 337c7ff933a0add21980b387c737c0ac3e726d46 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 19:40:25 +0200 Subject: [PATCH 50/52] poll: add stream poll classes to reflection getClassName test --- .../tests/ReflectionExtension_getClassNames_basic.phpt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt b/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt index 47813255381e4..3db78ba78062d 100644 --- a/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt +++ b/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt @@ -16,5 +16,8 @@ AssertionError Directory RoundingMode StreamBucket +StreamPollContext +StreamPollEvent +StreamPollException __PHP_Incomplete_Class php_user_filter From 95cb52219827800dee104d78e5c32b7054c41ce8 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 20:06:26 +0200 Subject: [PATCH 51/52] poll: update arginfo --- ext/standard/stream_poll_arginfo.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ext/standard/stream_poll_arginfo.h b/ext/standard/stream_poll_arginfo.h index d556adfbc3c22..73c2397654302 100644 --- a/ext/standard/stream_poll_arginfo.h +++ b/ext/standard/stream_poll_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: adcf3f025228764c97ec58f18c44b14ead8d2467 */ + * Stub hash: f7c5f27e1bc65c160bb85639808e1cfc3e64ecd8 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_StreamPollContext___construct, 0, 0, 0) ZEND_END_ARG_INFO() @@ -25,7 +25,6 @@ static void register_stream_poll_symbols(int module_number) REGISTER_LONG_CONSTANT("STREAM_POLL_BACKEND_EPOLL", PHP_POLL_BACKEND_EPOLL, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("STREAM_POLL_BACKEND_KQUEUE", PHP_POLL_BACKEND_KQUEUE, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("STREAM_POLL_BACKEND_EVENTPORT", PHP_POLL_BACKEND_EVENTPORT, CONST_PERSISTENT); - REGISTER_LONG_CONSTANT("STREAM_POLL_BACKEND_SELECT", PHP_POLL_BACKEND_SELECT, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("STREAM_POLL_BACKEND_IOCP", PHP_POLL_BACKEND_IOCP, CONST_PERSISTENT); } From 6979b80953605cb3a9593bbe3ee3ae971a57e6ed Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sun, 24 Aug 2025 20:57:09 +0200 Subject: [PATCH 52/52] poll: introduce php_poll_msleep for win compat --- main/poll/poll_backend_iocp.c | 4 ++-- main/poll/poll_backend_poll.c | 21 +++++++++++++++++---- main/poll/poll_core.c | 2 +- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/main/poll/poll_backend_iocp.c b/main/poll/poll_backend_iocp.c index 96327fd0f63c2..164cb883c23ac 100644 --- a/main/poll/poll_backend_iocp.c +++ b/main/poll/poll_backend_iocp.c @@ -14,7 +14,7 @@ #include "php_poll_internal.h" -#ifdef _WIN32 +#ifdef PHP_WIN32 #include #include @@ -391,4 +391,4 @@ const php_poll_backend_ops php_poll_backend_iocp_ops = { .supports_et = true /* IOCP provides completion-based model which is naturally edge-triggered */ }; -#endif /* _WIN32 */ +#endif /* PHP_WIN32 */ diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 049ebaa5766a9..e8fcef5af72c7 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -180,6 +180,22 @@ static bool poll_build_fds_callback(int fd, php_poll_fd_entry *entry, void *user return true; } +static void php_poll_msleep(int timeout_ms) +{ + if (timeout_ms <= 0) { + return; + } + +#ifdef PHP_WIN32 + Sleep(timeout_ms); +#else + struct timespec ts; + ts.tv_sec = timeout_ms / 1000; + ts.tv_nsec = (timeout_ms % 1000) * 1000000; + nanosleep(&ts, NULL); +#endif +} + static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, int timeout) { poll_backend_data_t *backend_data = (poll_backend_data_t *) ctx->backend_data; @@ -187,10 +203,7 @@ static int poll_backend_wait(php_poll_ctx *ctx, php_poll_event *events, int max_ int fd_count = php_poll_fd_table_count(backend_data->fd_table); if (fd_count == 0) { if (timeout > 0) { - struct timespec ts; - ts.tv_sec = timeout / 1000; - ts.tv_nsec = (timeout % 1000) * 1000000; - nanosleep(&ts, NULL); + php_poll_msleep(timeout); } return 0; } diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index 1a9c869f9c25b..67c739a5270ea 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -29,7 +29,7 @@ extern const php_poll_backend_ops php_poll_backend_kqueue_ops; #ifdef HAVE_EVENT_PORTS extern const php_poll_backend_ops php_poll_backend_eventport_ops; #endif -#ifdef _WIN32 +#ifdef PHP_WIN32 extern const php_poll_backend_ops php_poll_backend_iocp_ops; #endif extern const php_poll_backend_ops php_poll_backend_poll_ops;