From 186fcf188786fe22cb58e30865dfbfe30057c823 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 25 May 2026 14:06:31 +0200 Subject: [PATCH 01/18] Stream hook Introduce function stream_set_hook(callable $hook). The given hook is called just before performing a read or write operation on any stream, and must have the following signature: function (/*resource*/ $fd, StreamOperation $operation). --- ext/standard/basic_functions.c | 12 ++ ext/standard/basic_functions.stub.php | 7 ++ ext/standard/basic_functions_arginfo.h | 19 +++- ext/standard/basic_functions_decl.h | 13 ++- ext/standard/file.h | 1 + ext/standard/streamsfuncs.c | 24 ++++ ext/standard/tests/streams/hooks/001.phpt | 130 ++++++++++++++++++++++ main/php_streams.h | 6 + main/streams/streams.c | 54 +++++++++ 9 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 ext/standard/tests/streams/hooks/001.phpt diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index f8b1cb5c4fdb..4d8f3b0c659f 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -137,6 +137,9 @@ typedef struct { static void user_shutdown_function_dtor(zval *zv); static void user_tick_function_dtor(user_tick_function_entry *tick_function_entry); +// TODO: move elsewhere +PHPAPI zend_class_entry *php_stream_operation_ce; + static const zend_module_dep standard_deps[] = { /* {{{ */ ZEND_MOD_REQUIRED("random") ZEND_MOD_REQUIRED("uri") @@ -339,6 +342,8 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */ BASIC_MINIT_SUBMODULE(stream_errors) BASIC_MINIT_SUBMODULE(user_streams) + php_stream_operation_ce = register_class_StreamOperation(); + php_register_url_stream_wrapper("php", &php_stream_php_wrapper); php_register_url_stream_wrapper("file", &php_plain_files_wrapper); php_register_url_stream_wrapper("glob", &php_glob_stream_wrapper); @@ -424,6 +429,8 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */ /* Default to global filters only */ FG(stream_filters) = NULL; + FG(hook_fcc) = empty_fcall_info_cache; + return SUCCESS; } /* }}} */ @@ -484,6 +491,11 @@ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */ BG(page_uid) = -1; BG(page_gid) = -1; + + if (ZEND_FCC_INITIALIZED(FG(hook_fcc))) { + zend_fcc_dtor(&FG(hook_fcc)); + } + return SUCCESS; } /* }}} */ diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 7fc31e7d80ca..94ee77eaef5a 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -3598,6 +3598,13 @@ function sapi_windows_vt100_support($stream, ?bool $enable = null): bool {} /** @param resource $stream */ function stream_set_chunk_size($stream, int $size): int {} +enum StreamOperation { + case Read; + case Write; +}; + +function stream_set_hook(?callable $hook): ?callable {} + #if (defined(HAVE_SYS_TIME_H) || defined(PHP_WIN32)) /** @param resource $stream */ function stream_set_timeout($stream, int $seconds, int $microseconds = 0): bool {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index c5266c5a877c..28ee397a5242 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 basic_functions.stub.php instead. - * Stub hash: 3b1649a3abb3cfb5cb39d93f30a97765fe862d67 + * Stub hash: 48fe141fe56ba9216148703cc7da09d770835b1c * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) @@ -2014,6 +2014,10 @@ ZEND_END_ARG_INFO() #define arginfo_stream_set_chunk_size arginfo_stream_set_write_buffer +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_set_hook, 0, 1, IS_CALLABLE, 1) + ZEND_ARG_TYPE_INFO(0, hook, IS_CALLABLE, 1) +ZEND_END_ARG_INFO() + #if (defined(HAVE_SYS_TIME_H) || defined(PHP_WIN32)) ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_set_timeout, 0, 2, _IS_BOOL, 0) ZEND_ARG_INFO(0, stream) @@ -2851,6 +2855,7 @@ ZEND_FUNCTION(stream_isatty); ZEND_FUNCTION(sapi_windows_vt100_support); #endif ZEND_FUNCTION(stream_set_chunk_size); +ZEND_FUNCTION(stream_set_hook); #if (defined(HAVE_SYS_TIME_H) || defined(PHP_WIN32)) ZEND_FUNCTION(stream_set_timeout); #endif @@ -3470,6 +3475,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(sapi_windows_vt100_support, arginfo_sapi_windows_vt100_support) #endif ZEND_FE(stream_set_chunk_size, arginfo_stream_set_chunk_size) + ZEND_FE(stream_set_hook, arginfo_stream_set_hook) #if (defined(HAVE_SYS_TIME_H) || defined(PHP_WIN32)) ZEND_FE(stream_set_timeout, arginfo_stream_set_timeout) ZEND_RAW_FENTRY("socket_set_timeout", zif_stream_set_timeout, arginfo_socket_set_timeout, ZEND_ACC_DEPRECATED, NULL, NULL) @@ -4063,3 +4069,14 @@ static zend_class_entry *register_class_RoundingMode(void) return class_entry; } + +static zend_class_entry *register_class_StreamOperation(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("StreamOperation", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "Read", NULL); + + zend_enum_add_case_cstr(class_entry, "Write", NULL); + + return class_entry; +} diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h index 630a4b7e656b..5743839c9ff8 100644 --- a/ext/standard/basic_functions_decl.h +++ b/ext/standard/basic_functions_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit basic_functions.stub.php instead. - * Stub hash: 3b1649a3abb3cfb5cb39d93f30a97765fe862d67 */ + * Stub hash: 48fe141fe56ba9216148703cc7da09d770835b1c */ -#ifndef ZEND_BASIC_FUNCTIONS_DECL_3b1649a3abb3cfb5cb39d93f30a97765fe862d67_H -#define ZEND_BASIC_FUNCTIONS_DECL_3b1649a3abb3cfb5cb39d93f30a97765fe862d67_H +#ifndef ZEND_BASIC_FUNCTIONS_DECL_48fe141fe56ba9216148703cc7da09d770835b1c_H +#define ZEND_BASIC_FUNCTIONS_DECL_48fe141fe56ba9216148703cc7da09d770835b1c_H typedef enum zend_enum_SortDirection { ZEND_ENUM_SortDirection_Ascending = 1, @@ -20,4 +20,9 @@ typedef enum zend_enum_RoundingMode { ZEND_ENUM_RoundingMode_PositiveInfinity = 8, } zend_enum_RoundingMode; -#endif /* ZEND_BASIC_FUNCTIONS_DECL_3b1649a3abb3cfb5cb39d93f30a97765fe862d67_H */ +typedef enum zend_enum_StreamOperation { + ZEND_ENUM_StreamOperation_Read = 1, + ZEND_ENUM_StreamOperation_Write = 2, +} zend_enum_StreamOperation; + +#endif /* ZEND_BASIC_FUNCTIONS_DECL_48fe141fe56ba9216148703cc7da09d770835b1c_H */ diff --git a/ext/standard/file.h b/ext/standard/file.h index 9ba5f5b8b93d..87ee2827078e 100644 --- a/ext/standard/file.h +++ b/ext/standard/file.h @@ -102,6 +102,7 @@ typedef struct { HashTable *wrapper_logged_errors; /* key: wrapper address; value: linked list of error entries */ php_stream_error_state stream_error_state; int pclose_wait; + zend_fcall_info_cache hook_fcc; #ifdef HAVE_GETHOSTBYNAME_R struct hostent tmp_host_info; char *tmp_host_buf; diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index bad645f8668a..4a6516be1666 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -1836,3 +1836,27 @@ PHP_FUNCTION(stream_socket_shutdown) } /* }}} */ #endif + +PHP_FUNCTION(stream_set_hook) +{ + zend_fcall_info fci; + zend_fcall_info_cache fcc; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_FUNC_OR_NULL(fci, fcc) + ZEND_PARSE_PARAMETERS_END(); + + if (ZEND_FCC_INITIALIZED(FG(hook_fcc))) { + zend_get_callable_zval_from_fcc(&FG(hook_fcc), return_value); + zend_fcc_dtor(&FG(hook_fcc)); + } + + if (!ZEND_FCI_INITIALIZED(fci)) { + return; + } + + if (!ZEND_FCC_INITIALIZED(fcc)) { + zend_is_callable_ex(&fci.function_name, NULL, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &fcc, NULL); + } + zend_fcc_dup(&FG(hook_fcc), &fcc); +} diff --git a/ext/standard/tests/streams/hooks/001.phpt b/ext/standard/tests/streams/hooks/001.phpt new file mode 100644 index 000000000000..4a887f5ecff4 --- /dev/null +++ b/ext/standard/tests/streams/hooks/001.phpt @@ -0,0 +1,130 @@ +--TEST-- +Stream hook: use case +--FILE-- +ready[] = new Fiber($main); + + while ($this->ready !== [] || $this->readFds !== [] || $this->writeFds !== []) { + $this->runReadyFibers(); + $this->pollFds(); + } + } + + public function runReadyFibers() + { + while ($this->ready !== []) { + $fiber = array_shift($this->ready); + $fiber->isSuspended() ? $fiber->resume() : $fiber->start(); + } + } + + public function pollFds() + { + if ($this->readFds !== [] || $this->writeFds !== []) { + $read = $this->readFds; + $write = $this->writeFds; + $except = []; + stream_select($read, $write, $except, null); + foreach ($read as $fd) { + $id = (int)$fd; + array_push($this->ready, ...$this->readFibers[$id]); + unset($this->readFds[$id]); + unset($this->readFibers[$id]); + } + foreach ($write as $fd) { + $id = (int)$fd; + array_push($this->ready, ...$this->writeFibers[$id]); + unset($this->writeFds[$id]); + unset($this->writeFibers[$id]); + } + } + } + + public function waitRead($fd) { + $id = (int)$fd; + $this->readFds[$id] = $fd; + $this->readFibers[$id][] = Fiber::getCurrent(); + Fiber::suspend(); + } + + public function waitWrite($fd) { + $id = (int)$fd; + $this->writeFds[$id] = $fd; + $this->writeFibers[$id][] = Fiber::getCurrent(); + Fiber::suspend(); + } + + public function go(callable $fn) { + $this->ready[] = new Fiber($fn); + $this->ready[] = Fiber::getCurrent(); + Fiber::suspend(); + } +} + +$scheduler = new Scheduler(); + +stream_set_hook(function ($fd, StreamOperation $operation) use ($scheduler) { + switch ($operation) { + case StreamOperation::Read: + $scheduler->waitRead($fd); + break; + case StreamOperation::Write: + $scheduler->waitWrite($fd); + break; + } +}); + +function go($fn) { + global $scheduler; + $scheduler->go($fn); +} + +$scheduler->run(function () { + [$client, $server] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); + + go(function () use ($client) { + fwrite($client, "GET / HTTP/1.0\r\n"); + fwrite($client, "Host: localhost\r\n"); + fwrite($client, "\r\n"); + while (!feof($client)) { + echo "< " . trim(fgets($client)) . "\n"; + } + fclose($client); + }); + + go(function () use ($server) { + $headers = []; + while (!feof($server)) { + $line = fgets($server); + if ($line === false) { + break; + } + if ($line === "\r\n") { + break; + } + $headers[] = $line; + } + foreach ($headers as $header) { + echo "> " . trim($header) . "\n"; + } + fwrite($server, "Hello world!\n"); + }); +}); + +?> +--EXPECT-- +> GET / HTTP/1.0 +> Host: localhost +< Hello world! diff --git a/main/php_streams.h b/main/php_streams.h index 7622a7295af3..e18f9fbf4457 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -22,6 +22,7 @@ #include #include "zend.h" #include "zend_stream.h" +#include "ext/standard/basic_functions_decl.h" BEGIN_EXTERN_C() PHPAPI int php_file_le_stream(void); @@ -666,6 +667,11 @@ PHPAPI HashTable *_php_get_stream_filters_hash(void); PHPAPI HashTable *php_get_stream_filters_hash_global(void); extern const php_stream_wrapper_ops *php_stream_user_wrapper_ops; +PHPAPI extern zend_class_entry *php_stream_operation_ce; + +/* Call stream hook if any. Returns true if a hook was called. */ +PHPAPI bool php_stream_call_hook(php_stream *stream, zend_enum_StreamOperation); + static inline bool php_is_stream_path(const char *filename) { const char *p; diff --git a/main/streams/streams.c b/main/streams/streams.c index 171748e6a08a..a97114d72c05 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -30,6 +30,7 @@ #include #include #include "php_streams_int.h" +#include "zend_enum.h" /* {{{ resource and registration code */ /* Global wrapper hash, copied to FG(stream_wrappers) on registration of volatile wrapper */ @@ -432,6 +433,13 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) php_stream_filter_status_t status = PSFS_ERR_FATAL; php_stream_filter *filter; + if (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { + if (stream->eof || (stream->writepos - stream->readpos >= (zend_off_t)to_read_now)) { + break; + } + // TODO: stream closed? + } + /* read a chunk into a bucket */ justread = stream->ops->read(stream, chunk_buf, stream->chunk_size); if (justread < 0 && stream->writepos == stream->readpos) { @@ -559,6 +567,13 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) stream->is_persistent); } + if (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { + if (UNEXPECTED(stream->writepos - stream->readpos >= (zend_off_t)size)) { + return SUCCESS; + } + // TODO: stream closed? + } + justread = stream->ops->read(stream, (char*)stream->readbuf + stream->writepos, stream->readbuflen - stream->writepos ); @@ -612,6 +627,14 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size) } if (!stream->readfilters.head && ((stream->flags & PHP_STREAM_FLAG_NO_BUFFER) || stream->chunk_size == 1)) { + + if (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { + if (UNEXPECTED(stream->writepos > stream->readpos)) { + // TODO: stream closed? + continue; + } + } + toread = stream->ops->read(stream, buf, size); if (toread < 0) { /* Report an error if the read failed and we did not read any data @@ -1043,6 +1066,7 @@ static ssize_t _php_stream_write_buffer(php_stream *stream, const char *buf, siz * current stream->position. This means invalidating the read buffer and then * performing a low-level seek */ if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0 && stream->readpos != stream->writepos) { +seek: stream->readpos = stream->writepos = 0; stream->ops->seek(stream, stream->position, SEEK_SET, &stream->position); @@ -1058,6 +1082,12 @@ static ssize_t _php_stream_write_buffer(php_stream *stream, const char *buf, siz } while (count > 0) { + if (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Write)) { + if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0 && stream->readpos != stream->writepos) { + goto seek; + } + // TODO: eof? closed? + } ssize_t justwrote = stream->ops->write(stream, buf, MIN(chunk_size, count)); if (justwrote <= 0) { /* If we already successfully wrote some bytes and a write error occurred @@ -2517,3 +2547,27 @@ PHPAPI int _php_stream_scandir(const char *dirname, zend_string **namelist[], in return -1; } /* }}} */ + +static zend_object *php_stream_operation_get_case(zend_enum_StreamOperation operation) +{ + switch (operation) { + case ZEND_ENUM_StreamOperation_Read: + return zend_enum_get_case_cstr(php_stream_operation_ce, "Read"); + case ZEND_ENUM_StreamOperation_Write: + return zend_enum_get_case_cstr(php_stream_operation_ce, "Write"); + default: + ZEND_UNREACHABLE(); + } +} + +PHPAPI bool php_stream_call_hook(php_stream *stream, zend_enum_StreamOperation operation) +{ + if (!ZEND_FCC_INITIALIZED(FG(hook_fcc))) { + return false; + } + zval params[2]; + ZVAL_RES(¶ms[0], stream->res); + ZVAL_OBJ(¶ms[1], php_stream_operation_get_case(operation)); + zend_call_known_fcc(&FG(hook_fcc), NULL, 2, params, NULL); + return true; +} From 76dab9bef619b8db2cd13629420de5e5a2b754f0 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 3 Jun 2026 10:03:11 +0200 Subject: [PATCH 02/18] stream_socket_accept() --- ext/standard/streamsfuncs.c | 3 ++ ext/standard/tests/streams/hooks/001.phpt | 59 ++++++++++++++--------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 4a6516be1666..f8236bb1a7f6 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -311,6 +311,9 @@ PHP_FUNCTION(stream_socket_accept) php_stream_error_operation_begin(); + php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read); + // TODO: closed? + if (0 == php_stream_xport_accept(stream, &clistream, zpeername ? &peername : NULL, NULL, NULL, diff --git a/ext/standard/tests/streams/hooks/001.phpt b/ext/standard/tests/streams/hooks/001.phpt index 4a887f5ecff4..312d40c3e1e1 100644 --- a/ext/standard/tests/streams/hooks/001.phpt +++ b/ext/standard/tests/streams/hooks/001.phpt @@ -92,34 +92,45 @@ function go($fn) { } $scheduler->run(function () { - [$client, $server] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); - - go(function () use ($client) { - fwrite($client, "GET / HTTP/1.0\r\n"); - fwrite($client, "Host: localhost\r\n"); - fwrite($client, "\r\n"); - while (!feof($client)) { - echo "< " . trim(fgets($client)) . "\n"; - } - fclose($client); - }); + $server = stream_socket_server('tcp://localhost:0'); + $socket_name = stream_socket_get_name($server, false); + if (!preg_match('/:(\d+)$/', $socket_name, $m)) { + die("Could not extract port from '$socket_name'"); + } + $port = $m[1]; go(function () use ($server) { - $headers = []; - while (!feof($server)) { - $line = fgets($server); - if ($line === false) { - break; + $client = stream_socket_accept($server); + go(function () use ($client) { + $headers = []; + while (!feof($client)) { + $line = fgets($client); + if ($line === false) { + break; + } + if ($line === "\r\n") { + break; + } + $headers[] = $line; } - if ($line === "\r\n") { - break; + foreach ($headers as $header) { + echo "> " . trim($header) . "\n"; } - $headers[] = $line; - } - foreach ($headers as $header) { - echo "> " . trim($header) . "\n"; + fwrite($client, "HTTP/1.0 200 OK\r\n"); + fwrite($client, "\r\n"); + fwrite($client, "Hello world!\n"); + }); + }); + + go(function () use ($port) { + $fd = stream_socket_client("tcp://localhost:$port"); + fwrite($fd, "GET / HTTP/1.0\r\n"); + fwrite($fd, "Host: localhost\r\n"); + fwrite($fd, "\r\n"); + while (!feof($fd)) { + echo "< " . trim(fgets($fd)) . "\n"; } - fwrite($server, "Hello world!\n"); + fclose($fd); }); }); @@ -127,4 +138,6 @@ $scheduler->run(function () { --EXPECT-- > GET / HTTP/1.0 > Host: localhost +< HTTP/1.0 200 OK +< < Hello world! From fc3ea7611d980269114e5ee408150d222cf80c11 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 3 Jun 2026 10:48:34 +0200 Subject: [PATCH 03/18] Explore fclose() handling Closing a stream resource frees the stream itself, so doing that in a hook will result in UAFs in some parent function. Possible solutions: * Deny fclose() during hook invokation * Never access streams after stream operations, or use the resource to check whether the stream was closed * Do not free the php_stream itself in fclose(). Replace ->ops with always-fail handlers, mark as eof. --- ext/standard/streamsfuncs.c | 12 ++- .../tests/streams/hooks/hook-close-fgets.phpt | 16 ++++ .../streams/hooks/hook-close-fwrite.phpt | 15 ++++ .../streams/hooks/{001.phpt => use-case.phpt} | 2 +- main/php_streams.h | 10 ++- main/streams/streams.c | 79 +++++++++++++------ 6 files changed, 106 insertions(+), 28 deletions(-) create mode 100644 ext/standard/tests/streams/hooks/hook-close-fgets.phpt create mode 100644 ext/standard/tests/streams/hooks/hook-close-fwrite.phpt rename ext/standard/tests/streams/hooks/{001.phpt => use-case.phpt} (99%) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index f8236bb1a7f6..ca4ee1fcf336 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -311,8 +311,15 @@ PHP_FUNCTION(stream_socket_accept) php_stream_error_operation_begin(); - php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read); - // TODO: closed? + // ???: Timeout handling? + switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { + case PHP_STREAM_HOOK_STREAM_CLOSED: + RETVAL_FALSE; + goto out; + case PHP_STREAM_HOOK_INVOKED: + case PHP_STREAM_HOOK_NO_HOOK: + break; + } if (0 == php_stream_xport_accept(stream, &clistream, zpeername ? &peername : NULL, @@ -332,6 +339,7 @@ PHP_FUNCTION(stream_socket_accept) RETVAL_FALSE; } +out: php_stream_error_operation_end_for_stream(stream); if (errstr) { diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt new file mode 100644 index 000000000000..8fc9147c77df --- /dev/null +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -0,0 +1,16 @@ +--TEST-- +Stream hook: closed during read hook +--XFAIL-- +UAF in _php_stream_get_line +--FILE-- + +--EXPECTF-- diff --git a/ext/standard/tests/streams/hooks/hook-close-fwrite.phpt b/ext/standard/tests/streams/hooks/hook-close-fwrite.phpt new file mode 100644 index 000000000000..b4fd87e6c6f9 --- /dev/null +++ b/ext/standard/tests/streams/hooks/hook-close-fwrite.phpt @@ -0,0 +1,15 @@ +--TEST-- +Stream hook: closed during write hook +--FILE-- + +--EXPECT-- + diff --git a/ext/standard/tests/streams/hooks/001.phpt b/ext/standard/tests/streams/hooks/use-case.phpt similarity index 99% rename from ext/standard/tests/streams/hooks/001.phpt rename to ext/standard/tests/streams/hooks/use-case.phpt index 312d40c3e1e1..c31a1aeb3aea 100644 --- a/ext/standard/tests/streams/hooks/001.phpt +++ b/ext/standard/tests/streams/hooks/use-case.phpt @@ -139,5 +139,5 @@ $scheduler->run(function () { > GET / HTTP/1.0 > Host: localhost < HTTP/1.0 200 OK -< +< < Hello world! diff --git a/main/php_streams.h b/main/php_streams.h index e18f9fbf4457..dd3f24ed3b70 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -669,8 +669,14 @@ extern const php_stream_wrapper_ops *php_stream_user_wrapper_ops; PHPAPI extern zend_class_entry *php_stream_operation_ce; -/* Call stream hook if any. Returns true if a hook was called. */ -PHPAPI bool php_stream_call_hook(php_stream *stream, zend_enum_StreamOperation); +C23_ENUM(php_stream_hook_result, uint8_t) { + PHP_STREAM_HOOK_NO_HOOK, /* No hook was invoked */ + PHP_STREAM_HOOK_INVOKED, /* Hook was invoked */ + PHP_STREAM_HOOK_STREAM_CLOSED, /* Stream was closed during hook invokation */ +}; + +/* Call stream hook if any */ +PHPAPI php_stream_hook_result php_stream_call_hook(php_stream *stream, zend_enum_StreamOperation); static inline bool php_is_stream_path(const char *filename) { diff --git a/main/streams/streams.c b/main/streams/streams.c index a97114d72c05..86508b36317f 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -433,11 +433,18 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) php_stream_filter_status_t status = PSFS_ERR_FATAL; php_stream_filter *filter; - if (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { - if (stream->eof || (stream->writepos - stream->readpos >= (zend_off_t)to_read_now)) { + switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { + case PHP_STREAM_HOOK_STREAM_CLOSED: + return FAILURE; + case PHP_STREAM_HOOK_INVOKED: + // ???: Should polling mechanisms report streams as + // ready when read buffer is non-empty? + if (stream->eof || (stream->writepos - stream->readpos >= (zend_off_t)to_read_now)) { + goto done; + } + break; + case PHP_STREAM_HOOK_NO_HOOK: break; - } - // TODO: stream closed? } /* read a chunk into a bucket */ @@ -543,6 +550,7 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) } } +done: efree(chunk_buf); return SUCCESS; } else { @@ -567,11 +575,16 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) stream->is_persistent); } - if (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { - if (UNEXPECTED(stream->writepos - stream->readpos >= (zend_off_t)size)) { - return SUCCESS; - } - // TODO: stream closed? + switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { + case PHP_STREAM_HOOK_STREAM_CLOSED: + return FAILURE; + case PHP_STREAM_HOOK_INVOKED: + if (UNEXPECTED(stream->writepos - stream->readpos >= (zend_off_t)size)) { + return SUCCESS; + } + break; + case PHP_STREAM_HOOK_NO_HOOK: + break; } justread = stream->ops->read(stream, (char*)stream->readbuf + stream->writepos, @@ -628,11 +641,16 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size) if (!stream->readfilters.head && ((stream->flags & PHP_STREAM_FLAG_NO_BUFFER) || stream->chunk_size == 1)) { - if (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { - if (UNEXPECTED(stream->writepos > stream->readpos)) { - // TODO: stream closed? - continue; - } + switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { + case PHP_STREAM_HOOK_STREAM_CLOSED: + return didread; + case PHP_STREAM_HOOK_INVOKED: + if (UNEXPECTED(stream->writepos > stream->readpos)) { + continue; + } + break; + case PHP_STREAM_HOOK_NO_HOOK: + break; } toread = stream->ops->read(stream, buf, size); @@ -1082,12 +1100,18 @@ static ssize_t _php_stream_write_buffer(php_stream *stream, const char *buf, siz } while (count > 0) { - if (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Write)) { - if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0 && stream->readpos != stream->writepos) { - goto seek; - } - // TODO: eof? closed? + switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Write)) { + case PHP_STREAM_HOOK_STREAM_CLOSED: + return didwrite; + case PHP_STREAM_HOOK_INVOKED: + if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0 && stream->readpos != stream->writepos) { + goto seek; + } + break; + case PHP_STREAM_HOOK_NO_HOOK: + break; } + ssize_t justwrote = stream->ops->write(stream, buf, MIN(chunk_size, count)); if (justwrote <= 0) { /* If we already successfully wrote some bytes and a write error occurred @@ -2560,14 +2584,23 @@ static zend_object *php_stream_operation_get_case(zend_enum_StreamOperation oper } } -PHPAPI bool php_stream_call_hook(php_stream *stream, zend_enum_StreamOperation operation) +PHPAPI php_stream_hook_result php_stream_call_hook(php_stream *stream, zend_enum_StreamOperation operation) { if (!ZEND_FCC_INITIALIZED(FG(hook_fcc))) { - return false; + return PHP_STREAM_HOOK_NO_HOOK; } + + zend_resource *res = stream->res; + zval params[2]; - ZVAL_RES(¶ms[0], stream->res); + ZVAL_RES(¶ms[0], res); ZVAL_OBJ(¶ms[1], php_stream_operation_get_case(operation)); zend_call_known_fcc(&FG(hook_fcc), NULL, 2, params, NULL); - return true; + + if (res->type == -1) { + // TODO: https://wiki.php.net/rfc/stream_errors + return PHP_STREAM_HOOK_STREAM_CLOSED; + } + + return PHP_STREAM_HOOK_INVOKED; } From 4b88c5b8c03099623d0e9fa797d08349fd98d0f4 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Thu, 4 Jun 2026 16:21:40 +0200 Subject: [PATCH 04/18] Prevent closing stream during hook invokation --- ext/standard/streamsfuncs.c | 10 +--------- .../tests/streams/hooks/hook-close-fgets.phpt | 5 +++-- .../streams/hooks/hook-close-fwrite.phpt | 5 +++-- .../tests/streams/hooks/use-case.phpt | 2 +- main/php_streams.h | 1 - main/streams/streams.c | 19 +++++-------------- 6 files changed, 13 insertions(+), 29 deletions(-) diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index ca4ee1fcf336..80f0a053a71f 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -312,14 +312,7 @@ PHP_FUNCTION(stream_socket_accept) php_stream_error_operation_begin(); // ???: Timeout handling? - switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { - case PHP_STREAM_HOOK_STREAM_CLOSED: - RETVAL_FALSE; - goto out; - case PHP_STREAM_HOOK_INVOKED: - case PHP_STREAM_HOOK_NO_HOOK: - break; - } + php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read); if (0 == php_stream_xport_accept(stream, &clistream, zpeername ? &peername : NULL, @@ -339,7 +332,6 @@ PHP_FUNCTION(stream_socket_accept) RETVAL_FALSE; } -out: php_stream_error_operation_end_for_stream(stream); if (errstr) { diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt index 8fc9147c77df..e86600bc7d83 100644 --- a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -1,7 +1,5 @@ --TEST-- Stream hook: closed during read hook ---XFAIL-- -UAF in _php_stream_get_line --FILE-- --EXPECTF-- +Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d +string(6) " ---EXPECT-- - +--EXPECTF-- +Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d +int(1) diff --git a/ext/standard/tests/streams/hooks/use-case.phpt b/ext/standard/tests/streams/hooks/use-case.phpt index c31a1aeb3aea..85bd6b1e4cb1 100644 --- a/ext/standard/tests/streams/hooks/use-case.phpt +++ b/ext/standard/tests/streams/hooks/use-case.phpt @@ -128,7 +128,7 @@ $scheduler->run(function () { fwrite($fd, "Host: localhost\r\n"); fwrite($fd, "\r\n"); while (!feof($fd)) { - echo "< " . trim(fgets($fd)) . "\n"; + echo trim("< " . fgets($fd)) . "\n"; } fclose($fd); }); diff --git a/main/php_streams.h b/main/php_streams.h index dd3f24ed3b70..059392161839 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -672,7 +672,6 @@ PHPAPI extern zend_class_entry *php_stream_operation_ce; C23_ENUM(php_stream_hook_result, uint8_t) { PHP_STREAM_HOOK_NO_HOOK, /* No hook was invoked */ PHP_STREAM_HOOK_INVOKED, /* Hook was invoked */ - PHP_STREAM_HOOK_STREAM_CLOSED, /* Stream was closed during hook invokation */ }; /* Call stream hook if any */ diff --git a/main/streams/streams.c b/main/streams/streams.c index 86508b36317f..40dca3927a05 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -434,8 +434,6 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) php_stream_filter *filter; switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { - case PHP_STREAM_HOOK_STREAM_CLOSED: - return FAILURE; case PHP_STREAM_HOOK_INVOKED: // ???: Should polling mechanisms report streams as // ready when read buffer is non-empty? @@ -576,8 +574,6 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) } switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { - case PHP_STREAM_HOOK_STREAM_CLOSED: - return FAILURE; case PHP_STREAM_HOOK_INVOKED: if (UNEXPECTED(stream->writepos - stream->readpos >= (zend_off_t)size)) { return SUCCESS; @@ -642,8 +638,6 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size) if (!stream->readfilters.head && ((stream->flags & PHP_STREAM_FLAG_NO_BUFFER) || stream->chunk_size == 1)) { switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { - case PHP_STREAM_HOOK_STREAM_CLOSED: - return didread; case PHP_STREAM_HOOK_INVOKED: if (UNEXPECTED(stream->writepos > stream->readpos)) { continue; @@ -1101,8 +1095,6 @@ static ssize_t _php_stream_write_buffer(php_stream *stream, const char *buf, siz while (count > 0) { switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Write)) { - case PHP_STREAM_HOOK_STREAM_CLOSED: - return didwrite; case PHP_STREAM_HOOK_INVOKED: if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0 && stream->readpos != stream->writepos) { goto seek; @@ -2590,17 +2582,16 @@ PHPAPI php_stream_hook_result php_stream_call_hook(php_stream *stream, zend_enum return PHP_STREAM_HOOK_NO_HOOK; } - zend_resource *res = stream->res; + uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; zval params[2]; - ZVAL_RES(¶ms[0], res); + ZVAL_RES(¶ms[0], stream->res); ZVAL_OBJ(¶ms[1], php_stream_operation_get_case(operation)); zend_call_known_fcc(&FG(hook_fcc), NULL, 2, params, NULL); - if (res->type == -1) { - // TODO: https://wiki.php.net/rfc/stream_errors - return PHP_STREAM_HOOK_STREAM_CLOSED; - } + stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= orig_no_fclose; return PHP_STREAM_HOOK_INVOKED; } From a3b26135078cad6f966cf13956acc09c6494a8a8 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Thu, 4 Jun 2026 19:12:12 +0200 Subject: [PATCH 05/18] Call the hook in php_pollfd_for The stream hook is now a php_pollfd_for() replacement. Stream ops typically call php_pollfd_for() before any blocking operations, to implement timeouts. We can hook here to delegate polling to user-space. TODO: * php_pollfd_for() is not called where there is no timeout. Ensure that we call it when a hook is installed. * Prevent concurrent stream ops (lock / serialize) * Timeouts should be handled by the hook --- ext/openssl/xp_ssl.c | 23 ++-- ext/standard/basic_functions.c | 4 +- ext/standard/basic_functions.stub.php | 7 +- ext/standard/basic_functions_arginfo.h | 16 ++- ext/standard/basic_functions_decl.h | 29 +++- ext/standard/io_poll.c | 2 +- ext/standard/io_poll.h | 22 +++ ext/standard/streamsfuncs.c | 3 - .../tests/streams/hooks/use-case.phpt | 75 +++++------ main/network.c | 19 +-- main/php_network.h | 55 +++++++- main/php_streams.h | 9 +- main/streams/streams.c | 125 ++++++++++-------- main/streams/xp_socket.c | 16 +-- 14 files changed, 252 insertions(+), 153 deletions(-) create mode 100644 ext/standard/io_poll.h diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index a154aa4572f7..d8a7575e0702 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2781,7 +2781,7 @@ static int php_openssl_enable_crypto(php_stream *stream, if (has_timeout) { left_time = php_openssl_subtract_timeval(*timeout, elapsed_time); } - php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ? + php_pollstream_for(stream, sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ? (POLLIN|POLLPRI) : POLLOUT, has_timeout ? &left_time : NULL); } } else { @@ -2957,10 +2957,10 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si */ if (retry) { if (read) { - php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_WRITE) ? + php_pollstream_for(stream, sslsock->s.socket, (err == SSL_ERROR_WANT_WRITE) ? (POLLOUT|POLLPRI) : (POLLIN|POLLPRI), has_timeout ? &left_time : NULL); } else { - php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ? + php_pollstream_for(stream, sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ? (POLLIN|POLLPRI) : (POLLOUT|POLLPRI), has_timeout ? &left_time : NULL); } } @@ -2976,10 +2976,10 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si /* Otherwise, we need to wait again (up to time_left or we get an error) */ if (began_blocked) { if (read) { - php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_WRITE) ? + php_pollstream_for(stream, sslsock->s.socket, (err == SSL_ERROR_WANT_WRITE) ? (POLLOUT|POLLPRI) : (POLLIN|POLLPRI), has_timeout ? &left_time : NULL); } else { - php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ? + php_pollstream_for(stream, sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ? (POLLIN|POLLPRI) : (POLLOUT|POLLPRI), has_timeout ? &left_time : NULL); } } else if (err == SSL_ERROR_WANT_READ) { @@ -3049,7 +3049,6 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{ { php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; #ifdef PHP_WIN32 - int n; #endif unsigned i; @@ -3077,6 +3076,7 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{ #endif if (sslsock->s.socket != SOCK_ERR) { #ifdef PHP_WIN32 + php_pollstream_result res; /* prevent more data from coming in */ shutdown(sslsock->s.socket, SHUT_RD); @@ -3087,8 +3087,8 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{ * but at the same time avoid hanging indefinitely. * */ do { - n = php_pollfd_for_ms(sslsock->s.socket, POLLOUT, 500); - } while (n == -1 && php_socket_errno() == EINTR); + res = php_pollstream_for_ms(stream, sslsock->s.socket, POLLOUT, 500); + } while (res == PHP_POLLSTREAM_ERROR && php_socket_errno() == EINTR); #endif closesocket(sslsock->s.socket); sslsock->s.socket = SOCK_ERR; @@ -3181,7 +3181,8 @@ static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_ } } - php_socket_t clisock = php_network_accept_incoming_ex(sock->s.socket, + php_socket_t clisock = php_network_accept_incoming_ex(stream, + sock->s.socket, xparam->want_textaddr ? &xparam->outputs.textaddr : NULL, xparam->want_addr ? &xparam->outputs.addr : NULL, xparam->want_addr ? &xparam->outputs.addrlen : NULL, @@ -3330,7 +3331,7 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val !(stream->flags & PHP_STREAM_FLAG_NO_IO) && ((MSG_DONTWAIT != 0) || !sslsock->s.is_blocked) ) || - php_pollfd_for(sslsock->s.socket, PHP_POLLREADABLE|POLLPRI, &tv) > 0 + php_pollstream_for(stream, sslsock->s.socket, PHP_POLLREADABLE|POLLPRI, &tv) == PHP_POLLSTREAM_READY ) { /* the poll() call was skipped if the socket is non-blocking (or MSG_DONTWAIT is available) and if the timeout is zero */ /* additionally, we don't use this optimization if SSL is active because in that case, we're not using MSG_DONTWAIT */ @@ -3408,7 +3409,7 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val if (retry) { /* Now, how much time until we time out? */ left_time = php_openssl_subtract_timeval(*timeout, elapsed_time); - if (php_pollfd_for(sslsock->s.socket, PHP_POLLREADABLE|POLLPRI|POLLOUT, has_timeout ? &left_time : NULL) <= 0) { + if (php_pollstream_for(stream, sslsock->s.socket, PHP_POLLREADABLE|POLLPRI|POLLOUT, has_timeout ? &left_time : NULL) < PHP_POLLSTREAM_READY) { retry = 0; alive = 0; }; diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 4d8f3b0c659f..86993f186310 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -138,7 +138,7 @@ static void user_shutdown_function_dtor(zval *zv); static void user_tick_function_dtor(user_tick_function_entry *tick_function_entry); // TODO: move elsewhere -PHPAPI zend_class_entry *php_stream_operation_ce; +PHPAPI zend_class_entry *php_stream_hook_result_ce; static const zend_module_dep standard_deps[] = { /* {{{ */ ZEND_MOD_REQUIRED("random") @@ -342,7 +342,7 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */ BASIC_MINIT_SUBMODULE(stream_errors) BASIC_MINIT_SUBMODULE(user_streams) - php_stream_operation_ce = register_class_StreamOperation(); + php_stream_hook_result_ce = register_class_StreamHookResult(); php_register_url_stream_wrapper("php", &php_stream_php_wrapper); php_register_url_stream_wrapper("file", &php_plain_files_wrapper); diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 94ee77eaef5a..5accead572f5 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -3598,9 +3598,10 @@ function sapi_windows_vt100_support($stream, ?bool $enable = null): bool {} /** @param resource $stream */ function stream_set_chunk_size($stream, int $size): int {} -enum StreamOperation { - case Read; - case Write; +enum StreamHookResult { + case Error; + case Timeout; + case Ready; }; function stream_set_hook(?callable $hook): ?callable {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index 28ee397a5242..ff154dd6229e 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,11 @@ /* This is a generated file, edit basic_functions.stub.php instead. +<<<<<<< HEAD * Stub hash: 48fe141fe56ba9216148703cc7da09d770835b1c +||||||| parent of 6f8cd98087d (Call the hook in php_pollfd_for) + * Stub hash: 7875f65f7ae0c4b2491295c6613afe628db17168 +======= + * Stub hash: 5b3a836c82a9be012550252226392be28e805dce +>>>>>>> 6f8cd98087d (Call the hook in php_pollfd_for) * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) @@ -4070,13 +4076,15 @@ static zend_class_entry *register_class_RoundingMode(void) return class_entry; } -static zend_class_entry *register_class_StreamOperation(void) +static zend_class_entry *register_class_StreamHookResult(void) { - zend_class_entry *class_entry = zend_register_internal_enum("StreamOperation", IS_UNDEF, NULL); + zend_class_entry *class_entry = zend_register_internal_enum("StreamHookResult", IS_UNDEF, NULL); - zend_enum_add_case_cstr(class_entry, "Read", NULL); + zend_enum_add_case_cstr(class_entry, "Error", NULL); - zend_enum_add_case_cstr(class_entry, "Write", NULL); + zend_enum_add_case_cstr(class_entry, "Timeout", NULL); + + zend_enum_add_case_cstr(class_entry, "Ready", NULL); return class_entry; } diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h index 5743839c9ff8..cfce5dbd5501 100644 --- a/ext/standard/basic_functions_decl.h +++ b/ext/standard/basic_functions_decl.h @@ -1,8 +1,22 @@ /* This is a generated file, edit basic_functions.stub.php instead. +<<<<<<< HEAD * Stub hash: 48fe141fe56ba9216148703cc7da09d770835b1c */ +||||||| parent of 6f8cd98087d (Call the hook in php_pollfd_for) + * Stub hash: 7875f65f7ae0c4b2491295c6613afe628db17168 */ +======= + * Stub hash: 5b3a836c82a9be012550252226392be28e805dce */ +>>>>>>> 6f8cd98087d (Call the hook in php_pollfd_for) +<<<<<<< HEAD #ifndef ZEND_BASIC_FUNCTIONS_DECL_48fe141fe56ba9216148703cc7da09d770835b1c_H #define ZEND_BASIC_FUNCTIONS_DECL_48fe141fe56ba9216148703cc7da09d770835b1c_H +||||||| parent of 6f8cd98087d (Call the hook in php_pollfd_for) +#ifndef ZEND_BASIC_FUNCTIONS_DECL_7875f65f7ae0c4b2491295c6613afe628db17168_H +#define ZEND_BASIC_FUNCTIONS_DECL_7875f65f7ae0c4b2491295c6613afe628db17168_H +======= +#ifndef ZEND_BASIC_FUNCTIONS_DECL_5b3a836c82a9be012550252226392be28e805dce_H +#define ZEND_BASIC_FUNCTIONS_DECL_5b3a836c82a9be012550252226392be28e805dce_H +>>>>>>> 6f8cd98087d (Call the hook in php_pollfd_for) typedef enum zend_enum_SortDirection { ZEND_ENUM_SortDirection_Ascending = 1, @@ -20,9 +34,16 @@ typedef enum zend_enum_RoundingMode { ZEND_ENUM_RoundingMode_PositiveInfinity = 8, } zend_enum_RoundingMode; -typedef enum zend_enum_StreamOperation { - ZEND_ENUM_StreamOperation_Read = 1, - ZEND_ENUM_StreamOperation_Write = 2, -} zend_enum_StreamOperation; +typedef enum zend_enum_StreamHookResult { + ZEND_ENUM_StreamHookResult_Error = 1, + ZEND_ENUM_StreamHookResult_Timeout = 2, + ZEND_ENUM_StreamHookResult_Ready = 3, +} zend_enum_StreamHookResult; +<<<<<<< HEAD #endif /* ZEND_BASIC_FUNCTIONS_DECL_48fe141fe56ba9216148703cc7da09d770835b1c_H */ +||||||| parent of 6f8cd98087d (Call the hook in php_pollfd_for) +#endif /* ZEND_BASIC_FUNCTIONS_DECL_7875f65f7ae0c4b2491295c6613afe628db17168_H */ +======= +#endif /* ZEND_BASIC_FUNCTIONS_DECL_5b3a836c82a9be012550252226392be28e805dce_H */ +>>>>>>> 6f8cd98087d (Call the hook in php_pollfd_for) diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c index 837470a9f753..031985fa15da 100644 --- a/ext/standard/io_poll.c +++ b/ext/standard/io_poll.c @@ -22,7 +22,7 @@ /* Class entries */ static zend_class_entry *php_io_poll_backend_class_entry; -static zend_class_entry *php_io_poll_event_class_entry; +zend_class_entry *php_io_poll_event_class_entry; static zend_class_entry *php_io_poll_context_class_entry; static zend_class_entry *php_io_poll_watcher_class_entry; static zend_class_entry *php_io_poll_handle_class_entry; diff --git a/ext/standard/io_poll.h b/ext/standard/io_poll.h new file mode 100644 index 000000000000..81d770de9db2 --- /dev/null +++ b/ext/standard/io_poll.h @@ -0,0 +1,22 @@ +/* + +----------------------------------------------------------------------+ + | 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. | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_POLL_H +#define PHP_IO_POLL_H + +#include "Zend/zend_types.h" + +extern zend_class_entry *php_io_poll_event_class_entry; + +#endif /* PHP_IO_POLL_H */ diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 80f0a053a71f..4a6516be1666 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -311,9 +311,6 @@ PHP_FUNCTION(stream_socket_accept) php_stream_error_operation_begin(); - // ???: Timeout handling? - php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read); - if (0 == php_stream_xport_accept(stream, &clistream, zpeername ? &peername : NULL, NULL, NULL, diff --git a/ext/standard/tests/streams/hooks/use-case.phpt b/ext/standard/tests/streams/hooks/use-case.phpt index 85bd6b1e4cb1..79ab26fd9a3d 100644 --- a/ext/standard/tests/streams/hooks/use-case.phpt +++ b/ext/standard/tests/streams/hooks/use-case.phpt @@ -3,20 +3,24 @@ Stream hook: use case --FILE-- pollContext = new Context(); + } + public function run($main) { $this->ready[] = new Fiber($main); - while ($this->ready !== [] || $this->readFds !== [] || $this->writeFds !== []) { + while ($this->ready !== [] || $this->fds !== []) { $this->runReadyFibers(); $this->pollFds(); } @@ -32,38 +36,34 @@ class Scheduler public function pollFds() { - if ($this->readFds !== [] || $this->writeFds !== []) { - $read = $this->readFds; - $write = $this->writeFds; - $except = []; - stream_select($read, $write, $except, null); - foreach ($read as $fd) { - $id = (int)$fd; - array_push($this->ready, ...$this->readFibers[$id]); - unset($this->readFds[$id]); - unset($this->readFibers[$id]); - } - foreach ($write as $fd) { - $id = (int)$fd; - array_push($this->ready, ...$this->writeFibers[$id]); - unset($this->writeFds[$id]); - unset($this->writeFibers[$id]); + if ($this->fds !== []) { + + $watchers = $this->pollContext->wait(); + + foreach ($watchers as $watcher) { + $id = (int)$watcher->getHandle()->getStream(); + unset($this->fds[$id]); + + $this->ready[] = $watcher->getData(); + + $watcher->remove(); } } } - public function waitRead($fd) { + public function pollFd($fd, array $events) + { $id = (int)$fd; - $this->readFds[$id] = $fd; - $this->readFibers[$id][] = Fiber::getCurrent(); - Fiber::suspend(); - } + if (isset($this->fds[$id])) { + throw new Exception(); + } + + $this->fds[$id] = $id; + $this->pollContext->add(new StreamPollHandle($fd), $events, Fiber::getCurrent()); - public function waitWrite($fd) { - $id = (int)$fd; - $this->writeFds[$id] = $fd; - $this->writeFibers[$id][] = Fiber::getCurrent(); Fiber::suspend(); + + return StreamHookResult::Ready; } public function go(callable $fn) { @@ -75,16 +75,7 @@ class Scheduler $scheduler = new Scheduler(); -stream_set_hook(function ($fd, StreamOperation $operation) use ($scheduler) { - switch ($operation) { - case StreamOperation::Read: - $scheduler->waitRead($fd); - break; - case StreamOperation::Write: - $scheduler->waitWrite($fd); - break; - } -}); +stream_set_hook($scheduler->pollFd(...)); function go($fn) { global $scheduler; @@ -119,6 +110,7 @@ $scheduler->run(function () { fwrite($client, "HTTP/1.0 200 OK\r\n"); fwrite($client, "\r\n"); fwrite($client, "Hello world!\n"); + fclose($client); }); }); @@ -130,7 +122,6 @@ $scheduler->run(function () { while (!feof($fd)) { echo trim("< " . fgets($fd)) . "\n"; } - fclose($fd); }); }); diff --git a/main/network.c b/main/network.c index 90d1716b5582..c6f647032ccf 100644 --- a/main/network.c +++ b/main/network.c @@ -807,7 +807,8 @@ PHPAPI int php_network_get_sock_name(php_socket_t sock, * version of the address will be emalloc'd and returned. * */ -PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, +PHPAPI php_socket_t php_network_accept_incoming_ex(php_stream *stream, + php_socket_t srvsock, zend_string **textaddr, struct sockaddr **addr, socklen_t *addrlen, @@ -818,16 +819,17 @@ PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, ) { php_socket_t clisock = -1; - int error = 0, n; + int error = 0; + php_pollstream_result result; php_sockaddr_storage sa; socklen_t sl; - n = php_pollfd_for(srvsock, PHP_POLLREADABLE, timeout); + result = php_pollstream_for(stream, srvsock, PHP_POLLREADABLE, timeout); - if (n == 0) { + if (result == PHP_POLLSTREAM_TIMEOUT) { error = PHP_TIMEOUT_ERROR_VALUE; - } else if (n == -1) { - error = php_socket_errno(); + } else if (result == PHP_POLLSTREAM_ERROR) { + error = php_socket_errno(); // TODO } else { sl = sizeof(sa); @@ -866,7 +868,8 @@ PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, return clisock; } -PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, +PHPAPI php_socket_t php_network_accept_incoming(php_stream *stream, + php_socket_t srvsock, zend_string **textaddr, struct sockaddr **addr, socklen_t *addrlen, @@ -878,7 +881,7 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, { php_sockvals sockvals = { .mask = tcp_nodelay ? PHP_SOCKVAL_TCP_NODELAY : 0 }; - return php_network_accept_incoming_ex(srvsock, textaddr, addr, addrlen, timeout, error_string, + return php_network_accept_incoming_ex(stream, srvsock, textaddr, addr, addrlen, timeout, error_string, error_code, &sockvals); } diff --git a/main/php_network.h b/main/php_network.h index e6d3009a6c82..bf6fbd63d220 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -16,6 +16,7 @@ #define _PHP_NETWORK_H #include +#include "zend_enum.h" #ifndef PHP_WIN32 # undef closesocket @@ -212,6 +213,54 @@ static inline int php_pollfd_for_ms(php_socket_t fd, int events, int timeout) return n; } +// TODO: C23_ENUM() +typedef enum php_pollstream_result { + PHP_POLLSTREAM_ERROR = -1, + PHP_POLLSTREAM_TIMEOUT = 0, + PHP_POLLSTREAM_READY = 1, +} php_pollstream_result; + +static inline php_pollstream_result php_pollstream_for(php_stream *stream, php_socket_t fd, int events, struct timeval *timeouttv) +{ + zend_object *result = php_stream_call_hook(stream, events); + + if (result == NULL) { + int n = php_pollfd_for(fd, events, timeouttv); + if (n > 0) { + return PHP_POLLSTREAM_READY; + } + if (n == 0) { + return PHP_POLLSTREAM_TIMEOUT; + } + return PHP_POLLSTREAM_ERROR; + } + + const char *name = Z_STRVAL_P(zend_enum_fetch_case_name(result)); + OBJ_RELEASE(result); + + if (!strcmp(name, "Error")) { +#ifdef PHP_WIN32 + WSASetLastError(0); +#else + errno = 0; +#endif + return PHP_POLLSTREAM_ERROR; + } else if (!strcmp(name, "Timeout")) { + return PHP_POLLSTREAM_TIMEOUT; + } else { + return PHP_POLLSTREAM_READY; + } +} + +static inline php_pollstream_result php_pollstream_for_ms(php_stream *stream, php_socket_t fd, int events, int timeout) +{ + struct timeval timeouttv = { + .tv_usec = timeout, + }; + + return php_pollstream_for(stream, fd, events, &timeouttv); +} + /* emit warning and suggestion for unsafe select(2) usage */ PHPAPI void _php_emit_fd_setsize_warning(int max_fd); @@ -315,7 +364,8 @@ PHPAPI php_socket_t php_network_bind_socket_to_local_addr(const char *host, unsi int socktype, long sockopts, zend_string **error_string, int *error_code ); -PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, +PHPAPI php_socket_t php_network_accept_incoming_ex(php_stream *stream, + php_socket_t srvsock, zend_string **textaddr, struct sockaddr **addr, socklen_t *addrlen, @@ -325,7 +375,8 @@ PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, php_sockvals *sockvals ); -PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, +PHPAPI php_socket_t php_network_accept_incoming(php_stream *stream, + php_socket_t srvsock, zend_string **textaddr, struct sockaddr **addr, socklen_t *addrlen, diff --git a/main/php_streams.h b/main/php_streams.h index 059392161839..80f401a39052 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -667,15 +667,10 @@ PHPAPI HashTable *_php_get_stream_filters_hash(void); PHPAPI HashTable *php_get_stream_filters_hash_global(void); extern const php_stream_wrapper_ops *php_stream_user_wrapper_ops; -PHPAPI extern zend_class_entry *php_stream_operation_ce; - -C23_ENUM(php_stream_hook_result, uint8_t) { - PHP_STREAM_HOOK_NO_HOOK, /* No hook was invoked */ - PHP_STREAM_HOOK_INVOKED, /* Hook was invoked */ -}; +PHPAPI extern zend_class_entry *php_stream_hook_result_ce; /* Call stream hook if any */ -PHPAPI php_stream_hook_result php_stream_call_hook(php_stream *stream, zend_enum_StreamOperation); +PHPAPI zend_object *php_stream_call_hook(php_stream *stream, int events); static inline bool php_is_stream_path(const char *filename) { diff --git a/main/streams/streams.c b/main/streams/streams.c index 40dca3927a05..3d7ae358dbf7 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -26,12 +26,19 @@ #include "ext/standard/file.h" #include "ext/standard/basic_functions.h" /* for BG(CurrentStatFile) */ #include "ext/standard/php_string.h" /* for php_memnstr, used by php_stream_get_record() */ +#include "ext/standard/io_poll.h" #include "ext/uri/php_uri.h" #include #include #include "php_streams_int.h" #include "zend_enum.h" +#ifdef HAVE_POLL_H +#include +#elif HAVE_SYS_POLL_H +#include +#endif + /* {{{ resource and registration code */ /* Global wrapper hash, copied to FG(stream_wrappers) on registration of volatile wrapper */ static HashTable url_stream_wrappers_hash; @@ -433,18 +440,6 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) php_stream_filter_status_t status = PSFS_ERR_FATAL; php_stream_filter *filter; - switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { - case PHP_STREAM_HOOK_INVOKED: - // ???: Should polling mechanisms report streams as - // ready when read buffer is non-empty? - if (stream->eof || (stream->writepos - stream->readpos >= (zend_off_t)to_read_now)) { - goto done; - } - break; - case PHP_STREAM_HOOK_NO_HOOK: - break; - } - /* read a chunk into a bucket */ justread = stream->ops->read(stream, chunk_buf, stream->chunk_size); if (justread < 0 && stream->writepos == stream->readpos) { @@ -548,7 +543,6 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) } } -done: efree(chunk_buf); return SUCCESS; } else { @@ -573,16 +567,6 @@ PHPAPI zend_result _php_stream_fill_read_buffer(php_stream *stream, size_t size) stream->is_persistent); } - switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { - case PHP_STREAM_HOOK_INVOKED: - if (UNEXPECTED(stream->writepos - stream->readpos >= (zend_off_t)size)) { - return SUCCESS; - } - break; - case PHP_STREAM_HOOK_NO_HOOK: - break; - } - justread = stream->ops->read(stream, (char*)stream->readbuf + stream->writepos, stream->readbuflen - stream->writepos ); @@ -636,17 +620,6 @@ PHPAPI ssize_t _php_stream_read(php_stream *stream, char *buf, size_t size) } if (!stream->readfilters.head && ((stream->flags & PHP_STREAM_FLAG_NO_BUFFER) || stream->chunk_size == 1)) { - - switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Read)) { - case PHP_STREAM_HOOK_INVOKED: - if (UNEXPECTED(stream->writepos > stream->readpos)) { - continue; - } - break; - case PHP_STREAM_HOOK_NO_HOOK: - break; - } - toread = stream->ops->read(stream, buf, size); if (toread < 0) { /* Report an error if the read failed and we did not read any data @@ -1078,7 +1051,6 @@ static ssize_t _php_stream_write_buffer(php_stream *stream, const char *buf, siz * current stream->position. This means invalidating the read buffer and then * performing a low-level seek */ if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0 && stream->readpos != stream->writepos) { -seek: stream->readpos = stream->writepos = 0; stream->ops->seek(stream, stream->position, SEEK_SET, &stream->position); @@ -1094,16 +1066,6 @@ static ssize_t _php_stream_write_buffer(php_stream *stream, const char *buf, siz } while (count > 0) { - switch (php_stream_call_hook(stream, ZEND_ENUM_StreamOperation_Write)) { - case PHP_STREAM_HOOK_INVOKED: - if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0 && stream->readpos != stream->writepos) { - goto seek; - } - break; - case PHP_STREAM_HOOK_NO_HOOK: - break; - } - ssize_t justwrote = stream->ops->write(stream, buf, MIN(chunk_size, count)); if (justwrote <= 0) { /* If we already successfully wrote some bytes and a write error occurred @@ -2564,34 +2526,81 @@ PHPAPI int _php_stream_scandir(const char *dirname, zend_string **namelist[], in } /* }}} */ -static zend_object *php_stream_operation_get_case(zend_enum_StreamOperation operation) +// TODO: move to io_poll.c +static void php_pollfd_events_to_io_poll_events(zend_array *dest, int events) { - switch (operation) { - case ZEND_ENUM_StreamOperation_Read: - return zend_enum_get_case_cstr(php_stream_operation_ce, "Read"); - case ZEND_ENUM_StreamOperation_Write: - return zend_enum_get_case_cstr(php_stream_operation_ce, "Write"); - default: - ZEND_UNREACHABLE(); + zval zv; + + ZEND_ASSERT(!(events & ~(POLLIN|POLLPRI|POLLOUT|POLLERR|POLLHUP))); + + if (events & POLLIN) { + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Read")); + zend_hash_next_index_insert(dest, &zv); + } + if (events & POLLPRI) { + /* TODO: This event is set in a few places, but there is no equivalent in Io\Poll */ + } + if (events & POLLOUT) { + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Write")); + zend_hash_next_index_insert(dest, &zv); + } + if (events & POLLERR) { + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Error")); + zend_hash_next_index_insert(dest, &zv); + } + if (events & POLLHUP) { + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "HangUp")); + zend_hash_next_index_insert(dest, &zv); } } -PHPAPI php_stream_hook_result php_stream_call_hook(php_stream *stream, zend_enum_StreamOperation operation) +// TODO: return a ZEND_ENUM_ +PHPAPI zend_object *php_stream_call_hook(php_stream *stream, int events) { if (!ZEND_FCC_INITIALIZED(FG(hook_fcc))) { - return PHP_STREAM_HOOK_NO_HOOK; + return NULL; } uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; + zend_array *events_array = zend_new_array(0); + php_pollfd_events_to_io_poll_events(events_array, events); + + // TODO: timeout zval params[2]; ZVAL_RES(¶ms[0], stream->res); - ZVAL_OBJ(¶ms[1], php_stream_operation_get_case(operation)); - zend_call_known_fcc(&FG(hook_fcc), NULL, 2, params, NULL); + ZVAL_ARR(¶ms[1], events_array); + + zval return_value; + zend_call_known_fcc(&FG(hook_fcc), &return_value, 2, params, NULL); + + zend_array_release(events_array); + + if (EG(exception)) { + goto error; + } + if (UNEXPECTED(Z_TYPE(return_value) != IS_OBJECT + || Z_OBJCE(return_value) != php_stream_hook_result_ce)) { + zend_type_error("stream hook must return an instance of %s, %s returned", + ZSTR_VAL(php_stream_hook_result_ce->name), + zend_zval_type_name(&return_value)); + goto error; + } stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; stream->flags |= orig_no_fclose; - return PHP_STREAM_HOOK_INVOKED; + return Z_OBJ(return_value); + +error: + zval_ptr_dtor(&return_value); + + stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= orig_no_fclose; + + zend_object *result = zend_enum_get_case_cstr(php_stream_hook_result_ce, "Error"); + GC_ADDREF(result); + + return result; } diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index d18b0e901957..ec8ecdd1ab71 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -91,14 +91,14 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun sock->timeout_event = false; do { - retval = php_pollfd_for(sock->socket, POLLOUT, ptimeout); + retval = php_pollstream_for(stream, sock->socket, POLLOUT, ptimeout); - if (retval == 0) { + if (retval == PHP_POLLSTREAM_TIMEOUT) { sock->timeout_event = true; break; } - if (retval > 0) { + if (retval == PHP_POLLSTREAM_READY) { /* writable now; retry */ goto retry; } @@ -150,12 +150,12 @@ static void php_sock_stream_wait_for_data(php_stream *stream, php_netstream_data } while(1) { - retval = php_pollfd_for(sock->socket, PHP_POLLREADABLE, ptimeout); + retval = php_pollstream_for(stream, sock->socket, PHP_POLLREADABLE, ptimeout); - if (retval == 0) + if (retval == PHP_POLLSTREAM_TIMEOUT) sock->timeout_event = true; - if (retval >= 0) + if (retval == PHP_POLLSTREAM_READY) break; if (php_socket_errno() != EINTR) @@ -372,7 +372,7 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void !(stream->flags & PHP_STREAM_FLAG_NO_IO) && ((MSG_DONTWAIT != 0) || !sock->is_blocked) ) || - php_pollfd_for(sock->socket, PHP_POLLREADABLE|POLLPRI, &tv) > 0 + php_pollstream_for(stream, sock->socket, PHP_POLLREADABLE|POLLPRI, &tv) == PHP_POLLSTREAM_READY ) { /* the poll() call was skipped if the socket is non-blocking (or MSG_DONTWAIT is available) and if the timeout is zero */ #ifdef PHP_WIN32 @@ -978,7 +978,7 @@ static inline int php_tcp_sockop_accept(php_stream *stream, php_netstream_data_t } } - php_socket_t clisock = php_network_accept_incoming_ex(sock->socket, + php_socket_t clisock = php_network_accept_incoming_ex(stream, sock->socket, xparam->want_textaddr ? &xparam->outputs.textaddr : NULL, xparam->want_addr ? &xparam->outputs.addr : NULL, xparam->want_addr ? &xparam->outputs.addrlen : NULL, From 00f595198cb9860fe67eebc01c40190d85369ae8 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 5 Jun 2026 10:16:46 +0200 Subject: [PATCH 06/18] Cleanup --- ext/standard/basic_functions.c | 15 +- ext/standard/basic_functions.h | 1 + ext/standard/basic_functions.stub.php | 8 - ext/standard/basic_functions_arginfo.h | 27 +-- ext/standard/basic_functions_decl.h | 34 +--- ext/standard/config.m4 | 1 + ext/standard/file.h | 8 +- ext/standard/io_hooks.c | 164 ++++++++++++++++++ ext/standard/io_hooks.h | 30 ++++ ext/standard/io_hooks.stub.php | 27 +++ ext/standard/io_hooks_arginfo.h | 74 ++++++++ ext/standard/io_poll.c | 16 +- ext/standard/io_poll.h | 8 +- ext/standard/streamsfuncs.c | 23 --- .../tests/streams/hooks/hook-close-fgets.phpt | 26 ++- .../streams/hooks/hook-close-fwrite.phpt | 16 -- .../tests/streams/hooks/use-case.phpt | 32 ++-- main/php_network.h | 12 +- main/php_streams.h | 5 - main/streams/streams.c | 86 --------- 20 files changed, 380 insertions(+), 233 deletions(-) create mode 100644 ext/standard/io_hooks.c create mode 100644 ext/standard/io_hooks.h create mode 100644 ext/standard/io_hooks.stub.php create mode 100644 ext/standard/io_hooks_arginfo.h delete mode 100644 ext/standard/tests/streams/hooks/hook-close-fwrite.phpt diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 86993f186310..61124484cb6d 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -137,9 +137,6 @@ typedef struct { static void user_shutdown_function_dtor(zval *zv); static void user_tick_function_dtor(user_tick_function_entry *tick_function_entry); -// TODO: move elsewhere -PHPAPI zend_class_entry *php_stream_hook_result_ce; - static const zend_module_dep standard_deps[] = { /* {{{ */ ZEND_MOD_REQUIRED("random") ZEND_MOD_REQUIRED("uri") @@ -341,8 +338,7 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */ BASIC_MINIT_SUBMODULE(stream_errors) BASIC_MINIT_SUBMODULE(user_streams) - - php_stream_hook_result_ce = register_class_StreamHookResult(); + BASIC_MINIT_SUBMODULE(io_hooks) php_register_url_stream_wrapper("php", &php_stream_php_wrapper); php_register_url_stream_wrapper("file", &php_plain_files_wrapper); @@ -429,7 +425,8 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */ /* Default to global filters only */ FG(stream_filters) = NULL; - FG(hook_fcc) = empty_fcall_info_cache; + ZVAL_UNDEF(&FG(io_hooks)); + FG(io_hooks_poll_fcc) = empty_fcall_info_cache; return SUCCESS; } @@ -492,8 +489,10 @@ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */ BG(page_uid) = -1; BG(page_gid) = -1; - if (ZEND_FCC_INITIALIZED(FG(hook_fcc))) { - zend_fcc_dtor(&FG(hook_fcc)); + if (!Z_ISUNDEF(FG(io_hooks))) { + zval_ptr_dtor(&FG(io_hooks)); + ZVAL_UNDEF(&FG(io_hooks)); + zend_fcc_dtor(&FG(io_hooks_poll_fcc)); } return SUCCESS; diff --git a/ext/standard/basic_functions.h b/ext/standard/basic_functions.h index 4d5a4c02aec0..83b8e4d62ca3 100644 --- a/ext/standard/basic_functions.h +++ b/ext/standard/basic_functions.h @@ -43,6 +43,7 @@ PHP_MINFO_FUNCTION(basic); ZEND_API void php_get_highlight_struct(zend_syntax_highlighter_ini *syntax_highlighter_ini); PHP_MINIT_FUNCTION(poll); +PHP_MINIT_FUNCTION(io_hooks); PHP_MINIT_FUNCTION(user_filters); PHP_RSHUTDOWN_FUNCTION(user_filters); PHP_RSHUTDOWN_FUNCTION(browscap); diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 5accead572f5..7fc31e7d80ca 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -3598,14 +3598,6 @@ function sapi_windows_vt100_support($stream, ?bool $enable = null): bool {} /** @param resource $stream */ function stream_set_chunk_size($stream, int $size): int {} -enum StreamHookResult { - case Error; - case Timeout; - case Ready; -}; - -function stream_set_hook(?callable $hook): ?callable {} - #if (defined(HAVE_SYS_TIME_H) || defined(PHP_WIN32)) /** @param resource $stream */ function stream_set_timeout($stream, int $seconds, int $microseconds = 0): bool {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index ff154dd6229e..c5266c5a877c 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,11 +1,5 @@ /* This is a generated file, edit basic_functions.stub.php instead. -<<<<<<< HEAD - * Stub hash: 48fe141fe56ba9216148703cc7da09d770835b1c -||||||| parent of 6f8cd98087d (Call the hook in php_pollfd_for) - * Stub hash: 7875f65f7ae0c4b2491295c6613afe628db17168 -======= - * Stub hash: 5b3a836c82a9be012550252226392be28e805dce ->>>>>>> 6f8cd98087d (Call the hook in php_pollfd_for) + * Stub hash: 3b1649a3abb3cfb5cb39d93f30a97765fe862d67 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) @@ -2020,10 +2014,6 @@ ZEND_END_ARG_INFO() #define arginfo_stream_set_chunk_size arginfo_stream_set_write_buffer -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_set_hook, 0, 1, IS_CALLABLE, 1) - ZEND_ARG_TYPE_INFO(0, hook, IS_CALLABLE, 1) -ZEND_END_ARG_INFO() - #if (defined(HAVE_SYS_TIME_H) || defined(PHP_WIN32)) ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_set_timeout, 0, 2, _IS_BOOL, 0) ZEND_ARG_INFO(0, stream) @@ -2861,7 +2851,6 @@ ZEND_FUNCTION(stream_isatty); ZEND_FUNCTION(sapi_windows_vt100_support); #endif ZEND_FUNCTION(stream_set_chunk_size); -ZEND_FUNCTION(stream_set_hook); #if (defined(HAVE_SYS_TIME_H) || defined(PHP_WIN32)) ZEND_FUNCTION(stream_set_timeout); #endif @@ -3481,7 +3470,6 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(sapi_windows_vt100_support, arginfo_sapi_windows_vt100_support) #endif ZEND_FE(stream_set_chunk_size, arginfo_stream_set_chunk_size) - ZEND_FE(stream_set_hook, arginfo_stream_set_hook) #if (defined(HAVE_SYS_TIME_H) || defined(PHP_WIN32)) ZEND_FE(stream_set_timeout, arginfo_stream_set_timeout) ZEND_RAW_FENTRY("socket_set_timeout", zif_stream_set_timeout, arginfo_socket_set_timeout, ZEND_ACC_DEPRECATED, NULL, NULL) @@ -4075,16 +4063,3 @@ static zend_class_entry *register_class_RoundingMode(void) return class_entry; } - -static zend_class_entry *register_class_StreamHookResult(void) -{ - zend_class_entry *class_entry = zend_register_internal_enum("StreamHookResult", IS_UNDEF, NULL); - - zend_enum_add_case_cstr(class_entry, "Error", NULL); - - zend_enum_add_case_cstr(class_entry, "Timeout", NULL); - - zend_enum_add_case_cstr(class_entry, "Ready", NULL); - - return class_entry; -} diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h index cfce5dbd5501..630a4b7e656b 100644 --- a/ext/standard/basic_functions_decl.h +++ b/ext/standard/basic_functions_decl.h @@ -1,22 +1,8 @@ /* This is a generated file, edit basic_functions.stub.php instead. -<<<<<<< HEAD - * Stub hash: 48fe141fe56ba9216148703cc7da09d770835b1c */ -||||||| parent of 6f8cd98087d (Call the hook in php_pollfd_for) - * Stub hash: 7875f65f7ae0c4b2491295c6613afe628db17168 */ -======= - * Stub hash: 5b3a836c82a9be012550252226392be28e805dce */ ->>>>>>> 6f8cd98087d (Call the hook in php_pollfd_for) + * Stub hash: 3b1649a3abb3cfb5cb39d93f30a97765fe862d67 */ -<<<<<<< HEAD -#ifndef ZEND_BASIC_FUNCTIONS_DECL_48fe141fe56ba9216148703cc7da09d770835b1c_H -#define ZEND_BASIC_FUNCTIONS_DECL_48fe141fe56ba9216148703cc7da09d770835b1c_H -||||||| parent of 6f8cd98087d (Call the hook in php_pollfd_for) -#ifndef ZEND_BASIC_FUNCTIONS_DECL_7875f65f7ae0c4b2491295c6613afe628db17168_H -#define ZEND_BASIC_FUNCTIONS_DECL_7875f65f7ae0c4b2491295c6613afe628db17168_H -======= -#ifndef ZEND_BASIC_FUNCTIONS_DECL_5b3a836c82a9be012550252226392be28e805dce_H -#define ZEND_BASIC_FUNCTIONS_DECL_5b3a836c82a9be012550252226392be28e805dce_H ->>>>>>> 6f8cd98087d (Call the hook in php_pollfd_for) +#ifndef ZEND_BASIC_FUNCTIONS_DECL_3b1649a3abb3cfb5cb39d93f30a97765fe862d67_H +#define ZEND_BASIC_FUNCTIONS_DECL_3b1649a3abb3cfb5cb39d93f30a97765fe862d67_H typedef enum zend_enum_SortDirection { ZEND_ENUM_SortDirection_Ascending = 1, @@ -34,16 +20,4 @@ typedef enum zend_enum_RoundingMode { ZEND_ENUM_RoundingMode_PositiveInfinity = 8, } zend_enum_RoundingMode; -typedef enum zend_enum_StreamHookResult { - ZEND_ENUM_StreamHookResult_Error = 1, - ZEND_ENUM_StreamHookResult_Timeout = 2, - ZEND_ENUM_StreamHookResult_Ready = 3, -} zend_enum_StreamHookResult; - -<<<<<<< HEAD -#endif /* ZEND_BASIC_FUNCTIONS_DECL_48fe141fe56ba9216148703cc7da09d770835b1c_H */ -||||||| parent of 6f8cd98087d (Call the hook in php_pollfd_for) -#endif /* ZEND_BASIC_FUNCTIONS_DECL_7875f65f7ae0c4b2491295c6613afe628db17168_H */ -======= -#endif /* ZEND_BASIC_FUNCTIONS_DECL_5b3a836c82a9be012550252226392be28e805dce_H */ ->>>>>>> 6f8cd98087d (Call the hook in php_pollfd_for) +#endif /* ZEND_BASIC_FUNCTIONS_DECL_3b1649a3abb3cfb5cb39d93f30a97765fe862d67_H */ diff --git a/ext/standard/config.m4 b/ext/standard/config.m4 index 67c36b93ba34..2b6cb062d5cb 100644 --- a/ext/standard/config.m4 +++ b/ext/standard/config.m4 @@ -418,6 +418,7 @@ PHP_NEW_EXTENSION([standard], m4_normalize([ image.c incomplete_class.c info.c + io_hooks.c io_poll.c iptc.c levenshtein.c diff --git a/ext/standard/file.h b/ext/standard/file.h index 87ee2827078e..019481fded50 100644 --- a/ext/standard/file.h +++ b/ext/standard/file.h @@ -15,7 +15,9 @@ #ifndef FILE_H #define FILE_H -#include "php_network.h" +#ifdef HAVE_GETHOSTBYNAME_R +# include +#endif PHP_MINIT_FUNCTION(file); PHP_MSHUTDOWN_FUNCTION(file); @@ -102,7 +104,8 @@ typedef struct { HashTable *wrapper_logged_errors; /* key: wrapper address; value: linked list of error entries */ php_stream_error_state stream_error_state; int pclose_wait; - zend_fcall_info_cache hook_fcc; + zval io_hooks; + zend_fcall_info_cache io_hooks_poll_fcc; #ifdef HAVE_GETHOSTBYNAME_R struct hostent tmp_host_info; char *tmp_host_buf; @@ -118,5 +121,6 @@ extern PHPAPI int file_globals_id; extern PHPAPI php_file_globals file_globals; #endif +#include "php_network.h" #endif /* FILE_H */ diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c new file mode 100644 index 000000000000..c7dba492ede7 --- /dev/null +++ b/ext/standard/io_hooks.c @@ -0,0 +1,164 @@ +/* + +----------------------------------------------------------------------+ + | 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. | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "zend_enum.h" +#include "ext/standard/file.h" +#include "ext/standard/io_poll.h" +#include "ext/standard/io_hooks.h" +#include "io_hooks_arginfo.h" + +#ifdef HAVE_POLL_H +#include +#elif HAVE_SYS_POLL_H +#include +#endif + +static zend_class_entry *php_io_hooks_poll_info_ce; +PHPAPI zend_class_entry *php_io_hooks_poll_result_ce; +static zend_class_entry *php_io_hooks_hooks_ce; + +static void php_pollfd_events_to_io_poll_events(zend_array *dest, int events) +{ + zval zv; + + if (events & POLLIN) { + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Read")); + zend_hash_next_index_insert(dest, &zv); + } + if (events & POLLOUT) { + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Write")); + zend_hash_next_index_insert(dest, &zv); + } + if (events & POLLERR) { + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Error")); + zend_hash_next_index_insert(dest, &zv); + } + if (events & POLLHUP) { + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "HangUp")); + zend_hash_next_index_insert(dest, &zv); + } +} + +// TODO: return a ZEND_ENUM_* +PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, const struct timeval *timeout) +{ + uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; + + // TODO: optimize poll_info object creation+init. Maybe reuse it too (e.g. if RC=1) + zval poll_info; + object_init_ex(&poll_info, php_io_hooks_poll_info_ce); + + zval handle; + php_stream_poll_handle_from_stream(&handle, stream); + zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ(poll_info), + "handle", sizeof("handle") - 1, &handle); + zval_ptr_dtor(&handle); + + zval events_arr; + array_init(&events_arr); + php_pollfd_events_to_io_poll_events(Z_ARRVAL(events_arr), events); + zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ(poll_info), + "events", sizeof("events") - 1, &events_arr); + zval_ptr_dtor(&events_arr); + + zend_long timeout_ms; + if (timeout == NULL) { + timeout_ms = -1; + } else { + timeout_ms = (zend_long)timeout->tv_sec * 1000 + (zend_long)timeout->tv_usec / 1000; + } + zend_update_property_long(php_io_hooks_poll_info_ce, Z_OBJ(poll_info), + "timeout_ms", sizeof("timeout_ms") - 1, timeout_ms); + + zval retval; + ZVAL_UNDEF(&retval); + zend_call_known_fcc(&FG(io_hooks_poll_fcc), &retval, 1, &poll_info, NULL); + zval_ptr_dtor(&poll_info); + + if (EG(exception)) { + goto return_error; + } + + if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT || Z_OBJCE(retval) != php_io_hooks_poll_result_ce)) { + zend_type_error("%s::poll() must return %s, %s returned", + ZSTR_VAL(php_io_hooks_hooks_ce->name), + ZSTR_VAL(php_io_hooks_poll_result_ce->name), + zend_zval_type_name(&retval)); + goto return_error; + } + + stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= orig_no_fclose; + + return Z_OBJ(retval); + +return_error: + zval_ptr_dtor(&retval); + + stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; + stream->flags |= orig_no_fclose; + + zend_object *err = zend_enum_get_case_cstr(php_io_hooks_poll_result_ce, "Error"); + GC_ADDREF(err); + + return err; +} + +PHP_FUNCTION(Io_Hooks_set_hooks) +{ + zval *hooks_obj = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(hooks_obj, php_io_hooks_hooks_ce) + ZEND_PARSE_PARAMETERS_END(); + + if (!Z_ISUNDEF(FG(io_hooks))) { + ZVAL_COPY(return_value, &FG(io_hooks)); + zval_ptr_dtor(&FG(io_hooks)); + zend_fcc_dtor(&FG(io_hooks_poll_fcc)); + } + + if (hooks_obj == NULL || Z_TYPE_P(hooks_obj) == IS_NULL) { + ZVAL_UNDEF(&FG(io_hooks)); + return; + } + + ZVAL_COPY(&FG(io_hooks), hooks_obj); + + zend_string *method_name = zend_string_init("poll", sizeof("poll") - 1, false); + zend_object *obj = Z_OBJ_P(hooks_obj); + zend_function *fn = obj->handlers->get_method(&obj, method_name, NULL); + zend_string_release(method_name); + ZEND_ASSERT(fn != NULL); + + FG(io_hooks_poll_fcc) = (zend_fcall_info_cache){ + .function_handler = fn, + .object = obj, + .called_scope = obj->ce, + }; + GC_ADDREF(obj); +} + +PHP_MINIT_FUNCTION(io_hooks) +{ + php_io_hooks_poll_info_ce = register_class_Io_Hooks_PollInfo(); + php_io_hooks_poll_result_ce = register_class_Io_Hooks_PollResult(); + php_io_hooks_hooks_ce = register_class_Io_Hooks_Hooks(); + + zend_register_functions(NULL, ext_functions, NULL, type); + + return SUCCESS; +} diff --git a/ext/standard/io_hooks.h b/ext/standard/io_hooks.h new file mode 100644 index 000000000000..55483870ac21 --- /dev/null +++ b/ext/standard/io_hooks.h @@ -0,0 +1,30 @@ +/* + +----------------------------------------------------------------------+ + | 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. | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_HOOKS_H +#define PHP_IO_HOOKS_H + +#include "main/php.h" +#include "Zend/zend_types.h" +#include "ext/standard/file.h" + +PHPAPI extern zend_class_entry *php_io_hooks_poll_result_ce; + +#define PHP_HAS_IO_POLL_HOOK() ZEND_FCC_INITIALIZED(FG(io_hooks_poll_fcc)) + +PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, const struct timeval *timeout); + +PHP_MINIT_FUNCTION(io_hooks); + +#endif /* PHP_IO_HOOKS_H */ diff --git a/ext/standard/io_hooks.stub.php b/ext/standard/io_hooks.stub.php new file mode 100644 index 000000000000..e7e97df03e80 --- /dev/null +++ b/ext/standard/io_hooks.stub.php @@ -0,0 +1,27 @@ +stream = stream; + intern->handle_data = data; + + GC_ADDREF(stream->res); +} + /* Handle interface internal only */ static int php_stream_poll_handle_implement_interface(zend_class_entry *interface, zend_class_entry *implementor) { diff --git a/ext/standard/io_poll.h b/ext/standard/io_poll.h index 81d770de9db2..47b2a9fa4093 100644 --- a/ext/standard/io_poll.h +++ b/ext/standard/io_poll.h @@ -16,7 +16,13 @@ #define PHP_IO_POLL_H #include "Zend/zend_types.h" +#include "main/php.h" -extern zend_class_entry *php_io_poll_event_class_entry; +PHPAPI extern zend_class_entry *php_io_poll_event_class_entry; +PHPAPI extern zend_class_entry *php_stream_poll_handle_class_entry; + +PHPAPI zend_result php_io_poll_events_to_event_enums(uint32_t events, zval *event_enums); + +PHPAPI void php_stream_poll_handle_from_stream(zval *dest, php_stream *stream); #endif /* PHP_IO_POLL_H */ diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 4a6516be1666..de71b11cb847 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -1837,26 +1837,3 @@ PHP_FUNCTION(stream_socket_shutdown) /* }}} */ #endif -PHP_FUNCTION(stream_set_hook) -{ - zend_fcall_info fci; - zend_fcall_info_cache fcc; - - ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_FUNC_OR_NULL(fci, fcc) - ZEND_PARSE_PARAMETERS_END(); - - if (ZEND_FCC_INITIALIZED(FG(hook_fcc))) { - zend_get_callable_zval_from_fcc(&FG(hook_fcc), return_value); - zend_fcc_dtor(&FG(hook_fcc)); - } - - if (!ZEND_FCI_INITIALIZED(fci)) { - return; - } - - if (!ZEND_FCC_INITIALIZED(fcc)) { - zend_is_callable_ex(&fci.function_name, NULL, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL, &fcc, NULL); - } - zend_fcc_dup(&FG(hook_fcc), &fcc); -} diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt index e86600bc7d83..612b2c7c13f1 100644 --- a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -3,13 +3,27 @@ Stream hook: closed during read hook --FILE-- handle->getStream()); + Io\Hooks\set_hooks(null); + return PollResult::Ready; + } +} + +Io\Hooks\set_hooks(new CloseOnceHooks()); +var_dump(fgets($client)); ?> --EXPECTF-- Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d diff --git a/ext/standard/tests/streams/hooks/hook-close-fwrite.phpt b/ext/standard/tests/streams/hooks/hook-close-fwrite.phpt deleted file mode 100644 index c4a6f8213473..000000000000 --- a/ext/standard/tests/streams/hooks/hook-close-fwrite.phpt +++ /dev/null @@ -1,16 +0,0 @@ ---TEST-- -Stream hook: closed during write hook ---FILE-- - ---EXPECTF-- -Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d -int(1) diff --git a/ext/standard/tests/streams/hooks/use-case.phpt b/ext/standard/tests/streams/hooks/use-case.phpt index 79ab26fd9a3d..854c2952e049 100644 --- a/ext/standard/tests/streams/hooks/use-case.phpt +++ b/ext/standard/tests/streams/hooks/use-case.phpt @@ -3,20 +3,21 @@ Stream hook: use case --FILE-- pollContext = new Context(); } - public function run($main) + public function run($main): void { $this->ready[] = new Fiber($main); @@ -26,7 +27,7 @@ class Scheduler } } - public function runReadyFibers() + public function runReadyFibers(): void { while ($this->ready !== []) { $fiber = array_shift($this->ready); @@ -34,10 +35,9 @@ class Scheduler } } - public function pollFds() + public function pollFds(): void { if ($this->fds !== []) { - $watchers = $this->pollContext->wait(); foreach ($watchers as $watcher) { @@ -51,22 +51,24 @@ class Scheduler } } - public function pollFd($fd, array $events) + public function poll(PollInfo $info): PollResult { - $id = (int)$fd; + $stream = $info->handle->getStream(); + $id = (int)$stream; if (isset($this->fds[$id])) { throw new Exception(); } $this->fds[$id] = $id; - $this->pollContext->add(new StreamPollHandle($fd), $events, Fiber::getCurrent()); + $this->pollContext->add($info->handle, $info->events, Fiber::getCurrent()); Fiber::suspend(); - return StreamHookResult::Ready; + return PollResult::Ready; } - public function go(callable $fn) { + public function go(callable $fn): void + { $this->ready[] = new Fiber($fn); $this->ready[] = Fiber::getCurrent(); Fiber::suspend(); @@ -75,7 +77,7 @@ class Scheduler $scheduler = new Scheduler(); -stream_set_hook($scheduler->pollFd(...)); +Io\Hooks\set_hooks($scheduler); function go($fn) { global $scheduler; diff --git a/main/php_network.h b/main/php_network.h index bf6fbd63d220..43fd33842779 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -17,6 +17,7 @@ #include #include "zend_enum.h" +#include "ext/standard/io_hooks.h" #ifndef PHP_WIN32 # undef closesocket @@ -222,9 +223,7 @@ typedef enum php_pollstream_result { static inline php_pollstream_result php_pollstream_for(php_stream *stream, php_socket_t fd, int events, struct timeval *timeouttv) { - zend_object *result = php_stream_call_hook(stream, events); - - if (result == NULL) { + if (!PHP_HAS_IO_POLL_HOOK()) { int n = php_pollfd_for(fd, events, timeouttv); if (n > 0) { return PHP_POLLSTREAM_READY; @@ -235,6 +234,8 @@ static inline php_pollstream_result php_pollstream_for(php_stream *stream, php_s return PHP_POLLSTREAM_ERROR; } + zend_object *result = php_io_hooks_poll_stream(stream, events, timeouttv); + const char *name = Z_STRVAL_P(zend_enum_fetch_case_name(result)); OBJ_RELEASE(result); @@ -252,10 +253,11 @@ static inline php_pollstream_result php_pollstream_for(php_stream *stream, php_s } } -static inline php_pollstream_result php_pollstream_for_ms(php_stream *stream, php_socket_t fd, int events, int timeout) +static inline php_pollstream_result php_pollstream_for_ms(php_stream *stream, php_socket_t fd, int events, int timeout_ms) { struct timeval timeouttv = { - .tv_usec = timeout, + .tv_sec = timeout_ms / 1000, + .tv_usec = (timeout_ms % 1000) * 1000, }; return php_pollstream_for(stream, fd, events, &timeouttv); diff --git a/main/php_streams.h b/main/php_streams.h index 80f401a39052..756fbaab8d94 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -22,7 +22,6 @@ #include #include "zend.h" #include "zend_stream.h" -#include "ext/standard/basic_functions_decl.h" BEGIN_EXTERN_C() PHPAPI int php_file_le_stream(void); @@ -667,10 +666,6 @@ PHPAPI HashTable *_php_get_stream_filters_hash(void); PHPAPI HashTable *php_get_stream_filters_hash_global(void); extern const php_stream_wrapper_ops *php_stream_user_wrapper_ops; -PHPAPI extern zend_class_entry *php_stream_hook_result_ce; - -/* Call stream hook if any */ -PHPAPI zend_object *php_stream_call_hook(php_stream *stream, int events); static inline bool php_is_stream_path(const char *filename) { diff --git a/main/streams/streams.c b/main/streams/streams.c index 3d7ae358dbf7..9e223048f343 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -26,18 +26,10 @@ #include "ext/standard/file.h" #include "ext/standard/basic_functions.h" /* for BG(CurrentStatFile) */ #include "ext/standard/php_string.h" /* for php_memnstr, used by php_stream_get_record() */ -#include "ext/standard/io_poll.h" #include "ext/uri/php_uri.h" #include #include #include "php_streams_int.h" -#include "zend_enum.h" - -#ifdef HAVE_POLL_H -#include -#elif HAVE_SYS_POLL_H -#include -#endif /* {{{ resource and registration code */ /* Global wrapper hash, copied to FG(stream_wrappers) on registration of volatile wrapper */ @@ -2526,81 +2518,3 @@ PHPAPI int _php_stream_scandir(const char *dirname, zend_string **namelist[], in } /* }}} */ -// TODO: move to io_poll.c -static void php_pollfd_events_to_io_poll_events(zend_array *dest, int events) -{ - zval zv; - - ZEND_ASSERT(!(events & ~(POLLIN|POLLPRI|POLLOUT|POLLERR|POLLHUP))); - - if (events & POLLIN) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Read")); - zend_hash_next_index_insert(dest, &zv); - } - if (events & POLLPRI) { - /* TODO: This event is set in a few places, but there is no equivalent in Io\Poll */ - } - if (events & POLLOUT) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Write")); - zend_hash_next_index_insert(dest, &zv); - } - if (events & POLLERR) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Error")); - zend_hash_next_index_insert(dest, &zv); - } - if (events & POLLHUP) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "HangUp")); - zend_hash_next_index_insert(dest, &zv); - } -} - -// TODO: return a ZEND_ENUM_ -PHPAPI zend_object *php_stream_call_hook(php_stream *stream, int events) -{ - if (!ZEND_FCC_INITIALIZED(FG(hook_fcc))) { - return NULL; - } - - uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; - stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; - - zend_array *events_array = zend_new_array(0); - php_pollfd_events_to_io_poll_events(events_array, events); - - // TODO: timeout - zval params[2]; - ZVAL_RES(¶ms[0], stream->res); - ZVAL_ARR(¶ms[1], events_array); - - zval return_value; - zend_call_known_fcc(&FG(hook_fcc), &return_value, 2, params, NULL); - - zend_array_release(events_array); - - if (EG(exception)) { - goto error; - } - if (UNEXPECTED(Z_TYPE(return_value) != IS_OBJECT - || Z_OBJCE(return_value) != php_stream_hook_result_ce)) { - zend_type_error("stream hook must return an instance of %s, %s returned", - ZSTR_VAL(php_stream_hook_result_ce->name), - zend_zval_type_name(&return_value)); - goto error; - } - - stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; - stream->flags |= orig_no_fclose; - - return Z_OBJ(return_value); - -error: - zval_ptr_dtor(&return_value); - - stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; - stream->flags |= orig_no_fclose; - - zend_object *result = zend_enum_get_case_cstr(php_stream_hook_result_ce, "Error"); - GC_ADDREF(result); - - return result; -} From 3d83fe3207fb33f3c24fc4f1a302be343246836d Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 8 Jun 2026 10:54:19 +0200 Subject: [PATCH 07/18] Prepare API for curl: Watch multiple handlers at once --- ext/standard/io_hooks.c | 7 +-- ext/standard/io_hooks.stub.php | 11 ++-- .../tests/streams/hooks/hook-close-fgets.phpt | 10 +++- .../tests/streams/hooks/use-case.phpt | 57 ++++++++++++------- main/php_network.h | 20 +++---- 5 files changed, 60 insertions(+), 45 deletions(-) diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c index c7dba492ede7..40d937309135 100644 --- a/ext/standard/io_hooks.c +++ b/ext/standard/io_hooks.c @@ -51,7 +51,6 @@ static void php_pollfd_events_to_io_poll_events(zend_array *dest, int events) } } -// TODO: return a ZEND_ENUM_* PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, const struct timeval *timeout) { uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; @@ -106,15 +105,13 @@ PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, con return Z_OBJ(retval); return_error: + ZEND_ASSERT(EG(exception)); zval_ptr_dtor(&retval); stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; stream->flags |= orig_no_fclose; - zend_object *err = zend_enum_get_case_cstr(php_io_hooks_poll_result_ce, "Error"); - GC_ADDREF(err); - - return err; + return NULL; } PHP_FUNCTION(Io_Hooks_set_hooks) diff --git a/ext/standard/io_hooks.stub.php b/ext/standard/io_hooks.stub.php index e7e97df03e80..94bd2d072249 100644 --- a/ext/standard/io_hooks.stub.php +++ b/ext/standard/io_hooks.stub.php @@ -13,14 +13,15 @@ final class PollInfo { public int $timeout_ms; } - enum PollResult { - case Error; - case Timeout; - case Ready; + final class PollResult { + public Handle $handle; + /* @var Io\Poll\Event[] */ + public array $events; + public bool $timeout; } interface Hooks { - public function poll(PollInfo $info): PollResult; + public function poll(PollInfo ...$info): PollResult; } function set_hooks(?Hooks $hooks): ?Hooks {} diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt index 612b2c7c13f1..d4d74d61d254 100644 --- a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -15,10 +15,14 @@ fclose($conn); fclose($server); class CloseOnceHooks implements Hooks { - public function poll(PollInfo $info): PollResult { - fclose($info->handle->getStream()); + public function poll(PollInfo ...$infos): PollResult { + fclose($infos[0]->handle->getStream()); Io\Hooks\set_hooks(null); - return PollResult::Ready; + $result = new PollResult(); + $result->handle = $infos[0]->handle; + $result->events = $infos[0]->events; + $result->timeout = false; + return $result; } } diff --git a/ext/standard/tests/streams/hooks/use-case.phpt b/ext/standard/tests/streams/hooks/use-case.phpt index 854c2952e049..3d1ea63e8775 100644 --- a/ext/standard/tests/streams/hooks/use-case.phpt +++ b/ext/standard/tests/streams/hooks/use-case.phpt @@ -11,6 +11,7 @@ class Scheduler implements Hooks private Context $pollContext; private array $fds = []; private array $ready = []; + private array $fiberWatchers = []; // spl_object_id(fiber) => Watcher[] public function __construct() { @@ -19,7 +20,7 @@ class Scheduler implements Hooks public function run($main): void { - $this->ready[] = new Fiber($main); + $this->ready[] = [new Fiber($main), null]; while ($this->ready !== [] || $this->fds !== []) { $this->runReadyFibers(); @@ -30,8 +31,8 @@ class Scheduler implements Hooks public function runReadyFibers(): void { while ($this->ready !== []) { - $fiber = array_shift($this->ready); - $fiber->isSuspended() ? $fiber->resume() : $fiber->start(); + [$fiber, $resumeValue] = array_shift($this->ready); + $fiber->isSuspended() ? $fiber->resume($resumeValue) : $fiber->start(); } } @@ -41,36 +42,54 @@ class Scheduler implements Hooks $watchers = $this->pollContext->wait(); foreach ($watchers as $watcher) { - $id = (int)$watcher->getHandle()->getStream(); - unset($this->fds[$id]); + [$fiber] = $watcher->getData(); + $fiberId = spl_object_id($fiber); - $this->ready[] = $watcher->getData(); + if (!isset($this->fiberWatchers[$fiberId])) { + continue; // another watcher from the same poll() already handled this fiber + } + + // Remove all watchers registered for this fiber (including the one that fired) + foreach ($this->fiberWatchers[$fiberId] as $w) { + $id = (int)$w->getHandle()->getStream(); + unset($this->fds[$id]); + $w->remove(); + } + unset($this->fiberWatchers[$fiberId]); - $watcher->remove(); + $this->ready[] = [$fiber, [$watcher->getHandle(), $watcher->getTriggeredEvents()]]; } } } - public function poll(PollInfo $info): PollResult + public function poll(PollInfo ...$infos): PollResult { - $stream = $info->handle->getStream(); - $id = (int)$stream; - if (isset($this->fds[$id])) { - throw new Exception(); + $fiber = Fiber::getCurrent(); + $fiberId = spl_object_id($fiber); + $this->fiberWatchers[$fiberId] = []; + + foreach ($infos as $info) { + $id = (int)$info->handle->getStream(); + if (isset($this->fds[$id])) { + throw new Exception(); + } + $this->fds[$id] = $id; + $this->fiberWatchers[$fiberId][] = $this->pollContext->add($info->handle, $info->events, [$fiber]); } - $this->fds[$id] = $id; - $this->pollContext->add($info->handle, $info->events, Fiber::getCurrent()); - - Fiber::suspend(); + [$readyHandle, $readyEvents] = Fiber::suspend(); - return PollResult::Ready; + $result = new PollResult(); + $result->handle = $readyHandle; + $result->events = $readyEvents; + $result->timeout = false; + return $result; } public function go(callable $fn): void { - $this->ready[] = new Fiber($fn); - $this->ready[] = Fiber::getCurrent(); + $this->ready[] = [new Fiber($fn), null]; + $this->ready[] = [Fiber::getCurrent(), null]; Fiber::suspend(); } } diff --git a/main/php_network.h b/main/php_network.h index 43fd33842779..ce5c47330096 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -235,22 +235,16 @@ static inline php_pollstream_result php_pollstream_for(php_stream *stream, php_s } zend_object *result = php_io_hooks_poll_stream(stream, events, timeouttv); + if (result == NULL) { + return PHP_POLLSTREAM_ERROR; + } - const char *name = Z_STRVAL_P(zend_enum_fetch_case_name(result)); + zval rv; + zval *timeout_prop = zend_read_property(php_io_hooks_poll_result_ce, result, "timeout", sizeof("timeout") - 1, /* silent */ 1, &rv); + bool timed_out = zend_is_true(timeout_prop); OBJ_RELEASE(result); - if (!strcmp(name, "Error")) { -#ifdef PHP_WIN32 - WSASetLastError(0); -#else - errno = 0; -#endif - return PHP_POLLSTREAM_ERROR; - } else if (!strcmp(name, "Timeout")) { - return PHP_POLLSTREAM_TIMEOUT; - } else { - return PHP_POLLSTREAM_READY; - } + return timed_out ? PHP_POLLSTREAM_TIMEOUT : PHP_POLLSTREAM_READY; } static inline php_pollstream_result php_pollstream_for_ms(php_stream *stream, php_socket_t fd, int events, int timeout_ms) From 7dbc7b8b74f144a7a2739d17bdb2155378f0173c Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 8 Jun 2026 11:34:58 +0200 Subject: [PATCH 08/18] Timeout support and simplify poll() again --- ext/standard/io_hooks.stub.php | 4 +- .../tests/streams/hooks/hook-close-fgets.phpt | 9 +- .../tests/streams/hooks/use-case.phpt | 116 +++++++++++++----- 3 files changed, 92 insertions(+), 37 deletions(-) diff --git a/ext/standard/io_hooks.stub.php b/ext/standard/io_hooks.stub.php index 94bd2d072249..baddbc18bc41 100644 --- a/ext/standard/io_hooks.stub.php +++ b/ext/standard/io_hooks.stub.php @@ -21,7 +21,9 @@ final class PollResult { } interface Hooks { - public function poll(PollInfo ...$info): PollResult; + public function poll(PollInfo $info): PollResult; + /* @return PollResult[] Empty when $timeout_ms is exceeded */ + public function poll_multi(?int $timeout_ms, PollInfo ...$info): array; } function set_hooks(?Hooks $hooks): ?Hooks {} diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt index d4d74d61d254..913cf2be8430 100644 --- a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -15,15 +15,16 @@ fclose($conn); fclose($server); class CloseOnceHooks implements Hooks { - public function poll(PollInfo ...$infos): PollResult { - fclose($infos[0]->handle->getStream()); + public function poll(PollInfo $info): PollResult { + fclose($info->handle->getStream()); Io\Hooks\set_hooks(null); $result = new PollResult(); - $result->handle = $infos[0]->handle; - $result->events = $infos[0]->events; + $result->handle = $info->handle; + $result->events = $info->events; $result->timeout = false; return $result; } + public function poll_multi(?int $timeout_ms, PollInfo ...$info): array { throw new \Exception("poll_multi not implemented"); } } Io\Hooks\set_hooks(new CloseOnceHooks()); diff --git a/ext/standard/tests/streams/hooks/use-case.phpt b/ext/standard/tests/streams/hooks/use-case.phpt index 3d1ea63e8775..42a69c6e697e 100644 --- a/ext/standard/tests/streams/hooks/use-case.phpt +++ b/ext/standard/tests/streams/hooks/use-case.phpt @@ -11,7 +11,8 @@ class Scheduler implements Hooks private Context $pollContext; private array $fds = []; private array $ready = []; - private array $fiberWatchers = []; // spl_object_id(fiber) => Watcher[] + private array $fiberWatchers = []; // fiberId => Watcher[] + private array $fiberDeadlines = []; // fiberId => [deadline_ns, fiber, PollInfo] public function __construct() { @@ -22,7 +23,7 @@ class Scheduler implements Hooks { $this->ready[] = [new Fiber($main), null]; - while ($this->ready !== [] || $this->fds !== []) { + while ($this->ready !== [] || $this->fds !== [] || $this->fiberDeadlines !== []) { $this->runReadyFibers(); $this->pollFds(); } @@ -38,54 +39,105 @@ class Scheduler implements Hooks public function pollFds(): void { - if ($this->fds !== []) { - $watchers = $this->pollContext->wait(); + if ($this->fds === [] && $this->fiberDeadlines === []) { + return; + } - foreach ($watchers as $watcher) { - [$fiber] = $watcher->getData(); - $fiberId = spl_object_id($fiber); + // Compute wait() timeout from the nearest deadline + $timeoutSec = null; + $timeoutUsec = 0; + $now = hrtime(true); + foreach ($this->fiberDeadlines as [$deadline]) { + $remaining = $deadline - $now; + if ($remaining <= 0) { + $timeoutSec = 0; + $timeoutUsec = 0; + break; + } + $remainingUsec = intdiv($remaining, 1_000); + if ($timeoutSec === null || $remainingUsec < $timeoutSec * 1_000_000 + $timeoutUsec) { + $timeoutSec = intdiv($remainingUsec, 1_000_000); + $timeoutUsec = $remainingUsec % 1_000_000; + } + } - if (!isset($this->fiberWatchers[$fiberId])) { - continue; // another watcher from the same poll() already handled this fiber - } + $watchers = $this->pollContext->wait($timeoutSec, $timeoutUsec); - // Remove all watchers registered for this fiber (including the one that fired) - foreach ($this->fiberWatchers[$fiberId] as $w) { - $id = (int)$w->getHandle()->getStream(); - unset($this->fds[$id]); - $w->remove(); - } - unset($this->fiberWatchers[$fiberId]); + // Handle ready handles + foreach ($watchers as $watcher) { + [$fiber] = $watcher->getData(); + $fiberId = spl_object_id($fiber); + + if (!isset($this->fiberWatchers[$fiberId])) { + continue; + } + + $this->removeAllFiberWatchers($fiberId); + unset($this->fiberDeadlines[$fiberId]); + + $result = new PollResult(); + $result->handle = $watcher->getHandle(); + $result->events = $watcher->getTriggeredEvents(); + $result->timeout = false; + + $this->ready[] = [$fiber, $result]; + } - $this->ready[] = [$fiber, [$watcher->getHandle(), $watcher->getTriggeredEvents()]]; + // Handle expired deadlines + $now = hrtime(true); + foreach ($this->fiberDeadlines as $fiberId => [$deadline, $fiber, $info]) { + if ($deadline > $now) { + continue; } + + $this->removeAllFiberWatchers($fiberId); + unset($this->fiberDeadlines[$fiberId]); + + $result = new PollResult(); + $result->handle = $info->handle; + $result->events = $info->events; + $result->timeout = true; + + $this->ready[] = [$fiber, $result]; + } + } + + private function removeAllFiberWatchers(int $fiberId): void + { + foreach ($this->fiberWatchers[$fiberId] as $w) { + unset($this->fds[(int)$w->getHandle()->getStream()]); + $w->remove(); } + unset($this->fiberWatchers[$fiberId]); } - public function poll(PollInfo ...$infos): PollResult + public function poll(PollInfo $info): PollResult { $fiber = Fiber::getCurrent(); $fiberId = spl_object_id($fiber); - $this->fiberWatchers[$fiberId] = []; - foreach ($infos as $info) { - $id = (int)$info->handle->getStream(); - if (isset($this->fds[$id])) { - throw new Exception(); - } - $this->fds[$id] = $id; - $this->fiberWatchers[$fiberId][] = $this->pollContext->add($info->handle, $info->events, [$fiber]); + if ($info->timeout_ms >= 0) { + $deadline = hrtime(true) + $info->timeout_ms * 1_000_000; + $this->fiberDeadlines[$fiberId] = [$deadline, $fiber, $info]; } - [$readyHandle, $readyEvents] = Fiber::suspend(); + $id = (int)$info->handle->getStream(); + if (isset($this->fds[$id])) { + throw new \Exception("Handle already registered"); + } + $this->fds[$id] = $id; + $this->fiberWatchers[$fiberId] = [$this->pollContext->add($info->handle, $info->events, [$fiber])]; - $result = new PollResult(); - $result->handle = $readyHandle; - $result->events = $readyEvents; - $result->timeout = false; + /** @var PollResult $result */ + $result = Fiber::suspend(); return $result; } + public function poll_multi(?int $timeout_ms, PollInfo ...$infos): array + { + throw new \Exception("poll_multi not implemented"); + } + public function go(callable $fn): void { $this->ready[] = [new Fiber($fn), null]; From 71b10918fa53c620663f5c4564dfab3423771ecd Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 8 Jun 2026 13:11:01 +0200 Subject: [PATCH 09/18] curl_exec() support --- ext/curl/curl_private.h | 3 + ext/curl/curl_socket_handle.stub.php | 14 + ext/curl/curl_socket_handle_arginfo.h | 13 + ext/curl/interface.c | 313 +++++++++++++++++- ext/curl/tests/curl_exec_io_hook.phpt | 51 +++ ext/standard/basic_functions.c | 2 + ext/standard/file.h | 1 + ext/standard/io_hooks.c | 25 +- ext/standard/io_hooks.h | 1 + ext/standard/io_hooks_arginfo.h | 32 +- ext/standard/io_poll.c | 2 +- ext/standard/io_poll.h | 1 + .../tests/streams/hooks/scheduler.inc | 182 ++++++++++ .../tests/streams/hooks/use-case.phpt | 147 +------- 14 files changed, 629 insertions(+), 158 deletions(-) create mode 100644 ext/curl/curl_socket_handle.stub.php create mode 100644 ext/curl/curl_socket_handle_arginfo.h create mode 100644 ext/curl/tests/curl_exec_io_hook.phpt create mode 100644 ext/standard/tests/streams/hooks/scheduler.inc diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h index 77b0628ee42a..e195faf95178 100644 --- a/ext/curl/curl_private.h +++ b/ext/curl/curl_private.h @@ -114,6 +114,9 @@ typedef struct { zval private_data; /* CurlShareHandle object set using CURLOPT_SHARE. */ struct _php_curlsh *share; + /* Socket/timer state during curl_exec (NULL outside exec) */ + HashTable *io_sockets; /* curl_socket_t -> int events */ + long io_timer_ms; /* timer value from TIMERFUNCTION, -1 = disabled */ zend_object std; } php_curl; diff --git a/ext/curl/curl_socket_handle.stub.php b/ext/curl/curl_socket_handle.stub.php new file mode 100644 index 000000000000..68469010cfd2 --- /dev/null +++ b/ext/curl/curl_socket_handle.stub.php @@ -0,0 +1,14 @@ +create_object = php_curl_socket_handle_create_object; + memcpy(&php_curl_socket_handle_object_handlers, &std_object_handlers, + sizeof(zend_object_handlers)); + php_curl_socket_handle_object_handlers.offset = XtOffsetOf(php_poll_handle_object, std); + php_curl_socket_handle_object_handlers.free_obj = php_poll_handle_object_free; + php_curl_socket_handle_ce->default_object_handlers = &php_curl_socket_handle_object_handlers; + return SUCCESS; } /* }}} */ @@ -1058,6 +1080,8 @@ void init_curl_handle(php_curl *ch) zend_hash_init(&ch->to_free->slist, 4, NULL, curl_free_slist, 0); ZVAL_UNDEF(&ch->postfields); + ch->io_sockets = NULL; + ch->io_timer_ms = -1; } /* }}} */ @@ -2309,6 +2333,291 @@ PHP_FUNCTION(curl_setopt_array) } /* }}} */ +/* Io\Curl\SocketHandle: wraps a curl_socket_t as an Io\Poll\Handle */ + +typedef struct { + curl_socket_t socket; +} php_curl_socket_handle_data; + +static php_socket_t php_curl_socket_handle_get_fd(php_poll_handle_object *handle) +{ + return (php_socket_t)((php_curl_socket_handle_data *)handle->handle_data)->socket; +} + +static int php_curl_socket_handle_is_valid(php_poll_handle_object *handle) +{ + return handle->handle_data != NULL && + ((php_curl_socket_handle_data *)handle->handle_data)->socket != CURL_SOCKET_BAD; +} + +static void php_curl_socket_handle_cleanup(php_poll_handle_object *handle) +{ + efree(handle->handle_data); + handle->handle_data = NULL; +} + +static php_poll_handle_ops php_curl_socket_handle_ops = { + .get_fd = php_curl_socket_handle_get_fd, + .is_valid = php_curl_socket_handle_is_valid, + .cleanup = php_curl_socket_handle_cleanup, +}; + +static zend_object *php_curl_socket_handle_create_object(zend_class_entry *ce) +{ + php_poll_handle_object *intern = php_poll_handle_object_create( + sizeof(php_poll_handle_object), ce, &php_curl_socket_handle_ops); + intern->std.handlers = &php_curl_socket_handle_object_handlers; + return &intern->std; +} + +static void php_curl_socket_handle_from_fd(zval *dest, curl_socket_t s) +{ + object_init_ex(dest, php_curl_socket_handle_ce); + php_poll_handle_object *h = PHP_POLL_HANDLE_OBJ_FROM_ZV(dest); + php_curl_socket_handle_data *data = emalloc(sizeof(php_curl_socket_handle_data)); + data->socket = s; + h->handle_data = data; +} + +/* CURLMOPT_SOCKETFUNCTION: log socket+events in ch->io_sockets */ +static int php_curl_socket_callback(CURL *easy, curl_socket_t s, int what, void *userp, void *socketp) +{ + php_curl *ch = (php_curl *)userp; + + if (what == CURL_POLL_REMOVE) { + if (ch->io_sockets) { + zend_hash_index_del(ch->io_sockets, (zend_ulong)s); + } + } else { + if (!ch->io_sockets) { + ch->io_sockets = emalloc(sizeof(HashTable)); + zend_hash_init(ch->io_sockets, 4, NULL, NULL, 0); + } + zval zv; + ZVAL_LONG(&zv, what); + zend_hash_index_update(ch->io_sockets, (zend_ulong)s, &zv); + } + return 0; +} + +/* CURLMOPT_TIMERFUNCTION: record the requested timeout */ +static int php_curl_timer_callback(CURLM *multi, long timeout_ms, void *userp) +{ + php_curl *ch = (php_curl *)userp; + ch->io_timer_ms = timeout_ms; + return 0; +} + +/* Translate CURL_POLL_* to Io\Poll\Event[] array */ +static void php_curl_poll_events_to_zval(int what, zval *dest) +{ + array_init(dest); + if (what & CURL_POLL_IN) { + zval zv; + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Read")); + zend_hash_next_index_insert(Z_ARRVAL_P(dest), &zv); + } + if (what & CURL_POLL_OUT) { + zval zv; + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Write")); + zend_hash_next_index_insert(Z_ARRVAL_P(dest), &zv); + } +} + +/* Translate PollResult::$events back to CURL_CSELECT_* flags */ +static int php_curl_poll_result_to_curl_events(zval *events_arr) +{ + int curl_events = 0; + zval *event; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(events_arr), event) { + if (Z_TYPE_P(event) != IS_OBJECT) continue; + zend_string *name = Z_STR_P(zend_enum_fetch_case_name(Z_OBJ_P(event))); + if (zend_string_equals_literal(name, "Read")) curl_events |= CURL_CSELECT_IN; + if (zend_string_equals_literal(name, "Write")) curl_events |= CURL_CSELECT_OUT; + if (zend_string_equals_literal(name, "Error")) curl_events |= CURL_CSELECT_ERR; + } ZEND_HASH_FOREACH_END(); + return curl_events; +} + +/* Main multi-based exec loop */ +static CURLcode php_curl_exec_multi(php_curl *ch) +{ + CURLcode result = CURLE_OK; + CURLM *multi = curl_multi_init(); + curl_multi_setopt(multi, CURLMOPT_SOCKETFUNCTION, php_curl_socket_callback); + curl_multi_setopt(multi, CURLMOPT_SOCKETDATA, ch); + curl_multi_setopt(multi, CURLMOPT_TIMERFUNCTION, php_curl_timer_callback); + curl_multi_setopt(multi, CURLMOPT_TIMERDATA, ch); + + curl_multi_add_handle(multi, ch->cp); + + int still_running; + curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &still_running); + + while (still_running) { + /* If timer is zero, fire immediately again without waiting */ + if (ch->io_timer_ms == 0) { + curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &still_running); + continue; + } + + if (PHP_HAS_IO_POLL_HOOK()) { + /* Count active sockets */ + uint32_t n = ch->io_sockets ? zend_hash_num_elements(ch->io_sockets) : 0; + + /* Build argv: [timeout_ms_or_null, PollInfo...] */ + zval *params = safe_emalloc(n, sizeof(zval), sizeof(zval)); + + if (ch->io_timer_ms < 0) { + ZVAL_NULL(¶ms[0]); + } else { + ZVAL_LONG(¶ms[0], ch->io_timer_ms); + } + + uint32_t i = 1; + if (n > 0) { + zend_ulong sock_ulong; + zval *events_zv; + ZEND_HASH_FOREACH_NUM_KEY_VAL(ch->io_sockets, sock_ulong, events_zv) { + zval *poll_info = ¶ms[i++]; + object_init_ex(poll_info, php_io_hooks_poll_info_ce); + + zval handle; + php_curl_socket_handle_from_fd(&handle, (curl_socket_t)sock_ulong); + zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ_P(poll_info), + "handle", sizeof("handle") - 1, &handle); + zval_ptr_dtor(&handle); + + zval evts; + php_curl_poll_events_to_zval((int)Z_LVAL_P(events_zv), &evts); + zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ_P(poll_info), + "events", sizeof("events") - 1, &evts); + zval_ptr_dtor(&evts); + + /* timeout_ms per PollInfo: -1 (we use global timeout via params[0]) */ + zend_update_property_long(php_io_hooks_poll_info_ce, Z_OBJ_P(poll_info), + "timeout_ms", sizeof("timeout_ms") - 1, -1); + } ZEND_HASH_FOREACH_END(); + } + + zval retval; + ZVAL_UNDEF(&retval); + zend_call_known_fcc(&FG(io_hooks_poll_multi_fcc), &retval, 1 + n, params, NULL); + + for (uint32_t j = 0; j < 1 + n; j++) { + zval_ptr_dtor(¶ms[j]); + } + efree(params); + + if (EG(exception)) { + zval_ptr_dtor(&retval); + break; + } + + if (zend_hash_num_elements(Z_ARRVAL(retval)) > 0) { + /* Drive ready sockets */ + zval *result; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(retval), result) { + if (Z_TYPE_P(result) != IS_OBJECT || + Z_OBJCE_P(result) != php_io_hooks_poll_result_ce) continue; + + zval rv; + zval *handle_prop = zend_read_property(php_io_hooks_poll_result_ce, + Z_OBJ_P(result), "handle", sizeof("handle") - 1, 1, &rv); + if (Z_TYPE_P(handle_prop) != IS_OBJECT) continue; + + php_poll_handle_object *hobj = + PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(Z_OBJ_P(handle_prop)); + php_socket_t fd = php_poll_handle_get_fd(hobj); + + zval *events_prop = zend_read_property(php_io_hooks_poll_result_ce, + Z_OBJ_P(result), "events", sizeof("events") - 1, 1, &rv); + int curl_events = (Z_TYPE_P(events_prop) == IS_ARRAY) + ? php_curl_poll_result_to_curl_events(events_prop) : 0; + + curl_multi_socket_action(multi, (curl_socket_t)fd, curl_events, &still_running); + } ZEND_HASH_FOREACH_END(); + } else { + /* Empty array = timeout */ + curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &still_running); + } + zval_ptr_dtor(&retval); + } else { + /* No hook: select() on the sockets logged by the socket callback */ + fd_set rfds, wfds, efds; + FD_ZERO(&rfds); FD_ZERO(&wfds); FD_ZERO(&efds); + php_socket_t max_fd = 0; + bool have_fds = false; + + if (ch->io_sockets) { + zend_ulong sock_ulong; + zval *ev; + ZEND_HASH_FOREACH_NUM_KEY_VAL(ch->io_sockets, sock_ulong, ev) { + curl_socket_t s = (curl_socket_t)sock_ulong; + int what = (int)Z_LVAL_P(ev); + if (what & CURL_POLL_IN) FD_SET(s, &rfds); + if (what & CURL_POLL_OUT) FD_SET(s, &wfds); + FD_SET(s, &efds); + if ((php_socket_t)s > max_fd) max_fd = (php_socket_t)s; + have_fds = true; + } ZEND_HASH_FOREACH_END(); + } + + long wait_ms = ch->io_timer_ms >= 0 ? ch->io_timer_ms : 1000; + struct timeval tv = { + .tv_sec = wait_ms / 1000, + .tv_usec = (wait_ms % 1000) * 1000, + }; + + int n = have_fds ? select((int)max_fd + 1, &rfds, &wfds, &efds, &tv) : 0; + + if (n > 0 && ch->io_sockets) { + /* Collect ready sockets before calling socket_action (avoids hash re-entry) */ + curl_socket_t ready_s[16]; + int ready_ev[16]; + int ready_n = 0; + zend_ulong sock_ulong; + zval *ev; + ZEND_HASH_FOREACH_NUM_KEY_VAL(ch->io_sockets, sock_ulong, ev) { + curl_socket_t s = (curl_socket_t)sock_ulong; + int ce = 0; + if (FD_ISSET(s, &rfds)) ce |= CURL_CSELECT_IN; + if (FD_ISSET(s, &wfds)) ce |= CURL_CSELECT_OUT; + if (FD_ISSET(s, &efds)) ce |= CURL_CSELECT_ERR; + if (ce && ready_n < 16) { ready_s[ready_n] = s; ready_ev[ready_n++] = ce; } + } ZEND_HASH_FOREACH_END(); + for (int k = 0; k < ready_n; k++) { + curl_multi_socket_action(multi, ready_s[k], ready_ev[k], &still_running); + } + } else { + curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &still_running); + } + } + + /* Check for completed messages */ + CURLMsg *msg; + int msgs_in_queue; + while ((msg = curl_multi_info_read(multi, &msgs_in_queue)) != NULL) { + if (msg->msg == CURLMSG_DONE && msg->easy_handle == ch->cp) { + result = msg->data.result; + still_running = 0; + } + } + } + + curl_multi_remove_handle(multi, ch->cp); + curl_multi_cleanup(multi); + + if (ch->io_sockets) { + zend_hash_destroy(ch->io_sockets); + efree(ch->io_sockets); + ch->io_sockets = NULL; + } + ch->io_timer_ms = -1; + + return result; +} + /* {{{ _php_curl_cleanup_handle(ch) Cleanup an execution phase */ void _php_curl_cleanup_handle(php_curl *ch) @@ -2341,7 +2650,7 @@ PHP_FUNCTION(curl_exec) _php_curl_cleanup_handle(ch); - error = curl_easy_perform(ch->cp); + error = php_curl_exec_multi(ch); SAVE_CURL_ERROR(ch, error); if (error != CURLE_OK) { diff --git a/ext/curl/tests/curl_exec_io_hook.phpt b/ext/curl/tests/curl_exec_io_hook.phpt new file mode 100644 index 000000000000..6bf6f4466f05 --- /dev/null +++ b/ext/curl/tests/curl_exec_io_hook.phpt @@ -0,0 +1,51 @@ +--TEST-- +curl_exec() integrates with io_hooks scheduler +--EXTENSIONS-- +curl +--FILE-- +go($fn); +} + +$scheduler->run(function () { + $server = stream_socket_server('tcp://127.0.0.1:0'); + $addr = stream_socket_get_name($server, false); + + go(function () use ($server) { + $conn = stream_socket_accept($server, 5); + $request = ''; + while (!str_ends_with($request, "\r\n\r\n")) { + $chunk = fread($conn, 1024); + if ($chunk === false || $chunk === '') break; + $request .= $chunk; + } + fwrite($conn, "HTTP/1.0 200 OK\r\nContent-Length: 12\r\n\r\nHello, curl!"); + fclose($conn); + }); + + go(function () use ($addr) { + $ch = curl_init("http://$addr/"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + $body = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno === 0) { + echo "OK: $body\n"; + } else { + echo "curl error $errno: " . curl_error($ch) . "\n"; + } + }); +}); + +?> +--EXPECT-- +OK: Hello, curl! diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 61124484cb6d..ab7b7c1cf45e 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -427,6 +427,7 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */ ZVAL_UNDEF(&FG(io_hooks)); FG(io_hooks_poll_fcc) = empty_fcall_info_cache; + FG(io_hooks_poll_multi_fcc) = empty_fcall_info_cache; return SUCCESS; } @@ -493,6 +494,7 @@ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */ zval_ptr_dtor(&FG(io_hooks)); ZVAL_UNDEF(&FG(io_hooks)); zend_fcc_dtor(&FG(io_hooks_poll_fcc)); + zend_fcc_dtor(&FG(io_hooks_poll_multi_fcc)); } return SUCCESS; diff --git a/ext/standard/file.h b/ext/standard/file.h index 019481fded50..afd728996f8b 100644 --- a/ext/standard/file.h +++ b/ext/standard/file.h @@ -106,6 +106,7 @@ typedef struct { int pclose_wait; zval io_hooks; zend_fcall_info_cache io_hooks_poll_fcc; + zend_fcall_info_cache io_hooks_poll_multi_fcc; #ifdef HAVE_GETHOSTBYNAME_R struct hostent tmp_host_info; char *tmp_host_buf; diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c index 40d937309135..d2942cffe25d 100644 --- a/ext/standard/io_hooks.c +++ b/ext/standard/io_hooks.c @@ -25,7 +25,7 @@ #include #endif -static zend_class_entry *php_io_hooks_poll_info_ce; +PHPAPI zend_class_entry *php_io_hooks_poll_info_ce; PHPAPI zend_class_entry *php_io_hooks_poll_result_ce; static zend_class_entry *php_io_hooks_hooks_ce; @@ -135,14 +135,27 @@ PHP_FUNCTION(Io_Hooks_set_hooks) ZVAL_COPY(&FG(io_hooks), hooks_obj); - zend_string *method_name = zend_string_init("poll", sizeof("poll") - 1, false); zend_object *obj = Z_OBJ_P(hooks_obj); - zend_function *fn = obj->handlers->get_method(&obj, method_name, NULL); - zend_string_release(method_name); - ZEND_ASSERT(fn != NULL); + + zend_string *poll_name = zend_string_init("poll", sizeof("poll") - 1, false); + zend_function *poll_fn = obj->handlers->get_method(&obj, poll_name, NULL); + zend_string_release(poll_name); + ZEND_ASSERT(poll_fn != NULL); FG(io_hooks_poll_fcc) = (zend_fcall_info_cache){ - .function_handler = fn, + .function_handler = poll_fn, + .object = obj, + .called_scope = obj->ce, + }; + GC_ADDREF(obj); + + zend_string *poll_multi_name = zend_string_init("poll_multi", sizeof("poll_multi") - 1, false); + zend_function *poll_multi_fn = obj->handlers->get_method(&obj, poll_multi_name, NULL); + zend_string_release(poll_multi_name); + ZEND_ASSERT(poll_multi_fn != NULL); + + FG(io_hooks_poll_multi_fcc) = (zend_fcall_info_cache){ + .function_handler = poll_multi_fn, .object = obj, .called_scope = obj->ce, }; diff --git a/ext/standard/io_hooks.h b/ext/standard/io_hooks.h index 55483870ac21..04f2f23396b3 100644 --- a/ext/standard/io_hooks.h +++ b/ext/standard/io_hooks.h @@ -19,6 +19,7 @@ #include "Zend/zend_types.h" #include "ext/standard/file.h" +PHPAPI extern zend_class_entry *php_io_hooks_poll_info_ce; PHPAPI extern zend_class_entry *php_io_hooks_poll_result_ce; #define PHP_HAS_IO_POLL_HOOK() ZEND_FCC_INITIALIZED(FG(io_hooks_poll_fcc)) diff --git a/ext/standard/io_hooks_arginfo.h b/ext/standard/io_hooks_arginfo.h index cc40ad9e9e8d..d0514b619a14 100644 --- a/ext/standard/io_hooks_arginfo.h +++ b/ext/standard/io_hooks_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit io_hooks.stub.php instead. - * Stub hash: 454316a49190e58ab640e337fd8292eb949a2c5e */ + * Stub hash: 86abce168edd66424958da19478649fc38f059e6 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_Io_Hooks_set_hooks, 0, 1, Io\\Hooks\\Hooks, 1) ZEND_ARG_OBJ_INFO(0, hooks, Io\\Hooks\\Hooks, 1) @@ -9,6 +9,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Hooks_Hooks_poll, 0, 1, ZEND_ARG_OBJ_INFO(0, info, Io\\Hooks\\PollInfo, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Hooks_Hooks_poll_multi, 0, 1, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, timeout_ms, IS_LONG, 1) + ZEND_ARG_VARIADIC_OBJ_INFO(0, info, Io\\Hooks\\PollInfo, 0) +ZEND_END_ARG_INFO() + ZEND_FUNCTION(Io_Hooks_set_hooks); static const zend_function_entry ext_functions[] = { @@ -18,6 +23,7 @@ static const zend_function_entry ext_functions[] = { static const zend_function_entry class_Io_Hooks_Hooks_methods[] = { ZEND_RAW_FENTRY("poll", NULL, arginfo_class_Io_Hooks_Hooks_poll, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("poll_multi", NULL, arginfo_class_Io_Hooks_Hooks_poll_multi, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) ZEND_FE_END }; @@ -52,13 +58,29 @@ static zend_class_entry *register_class_Io_Hooks_PollInfo(void) static zend_class_entry *register_class_Io_Hooks_PollResult(void) { - zend_class_entry *class_entry = zend_register_internal_enum("Io\\Hooks\\PollResult", IS_UNDEF, NULL); + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Hooks", "PollResult", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL); - zend_enum_add_case_cstr(class_entry, "Error", NULL); + zval property_handle_default_value; + ZVAL_UNDEF(&property_handle_default_value); + zend_string *property_handle_name = zend_string_init("handle", sizeof("handle") - 1, true); + zend_string *property_handle_class_Io_Poll_Handle = zend_string_init("Io\\Poll\\Handle", sizeof("Io\\Poll\\Handle")-1, 1); + zend_declare_typed_property(class_entry, property_handle_name, &property_handle_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_handle_class_Io_Poll_Handle, 0, 0)); + zend_string_release_ex(property_handle_name, true); - zend_enum_add_case_cstr(class_entry, "Timeout", NULL); + zval property_events_default_value; + ZVAL_UNDEF(&property_events_default_value); + zend_string *property_events_name = zend_string_init("events", sizeof("events") - 1, true); + zend_declare_typed_property(class_entry, property_events_name, &property_events_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); + zend_string_release_ex(property_events_name, true); - zend_enum_add_case_cstr(class_entry, "Ready", NULL); + zval property_timeout_default_value; + ZVAL_UNDEF(&property_timeout_default_value); + zend_string *property_timeout_name = zend_string_init("timeout", sizeof("timeout") - 1, true); + zend_declare_typed_property(class_entry, property_timeout_name, &property_timeout_default_value, ZEND_ACC_PUBLIC, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_BOOL)); + zend_string_release_ex(property_timeout_name, true); return class_entry; } diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c index 97fcd277292e..dc8ce4c21891 100644 --- a/ext/standard/io_poll.c +++ b/ext/standard/io_poll.c @@ -25,7 +25,7 @@ static zend_class_entry *php_io_poll_backend_class_entry; zend_class_entry *php_io_poll_event_class_entry; static zend_class_entry *php_io_poll_context_class_entry; static zend_class_entry *php_io_poll_watcher_class_entry; -static zend_class_entry *php_io_poll_handle_class_entry; +PHPAPI zend_class_entry *php_io_poll_handle_class_entry; static zend_class_entry *php_io_exception_class_entry; static zend_class_entry *php_io_poll_exception_class_entry; static zend_class_entry *php_io_poll_failed_backend_unavailable_class_entry; diff --git a/ext/standard/io_poll.h b/ext/standard/io_poll.h index 47b2a9fa4093..1d9e19d918ec 100644 --- a/ext/standard/io_poll.h +++ b/ext/standard/io_poll.h @@ -19,6 +19,7 @@ #include "main/php.h" PHPAPI extern zend_class_entry *php_io_poll_event_class_entry; +PHPAPI extern zend_class_entry *php_io_poll_handle_class_entry; PHPAPI extern zend_class_entry *php_stream_poll_handle_class_entry; PHPAPI zend_result php_io_poll_events_to_event_enums(uint32_t events, zval *event_enums); diff --git a/ext/standard/tests/streams/hooks/scheduler.inc b/ext/standard/tests/streams/hooks/scheduler.inc new file mode 100644 index 000000000000..b4037a13022f --- /dev/null +++ b/ext/standard/tests/streams/hooks/scheduler.inc @@ -0,0 +1,182 @@ + spl_object_id(Handle) (for poll() handles only) + private array $fiberWatchers = []; // fiberId -> [[Watcher, fdKey|null], ...] + private array $fiberDeadlines = []; // fiberId -> [deadline_ns, fiber, PollInfo|null] + private array $fiberIsMulti = []; // fiberId -> true (for poll_multi fibers) + + public function __construct() + { + $this->pollContext = new Context(); + } + + public function run(callable $main): void + { + $this->ready[] = [new Fiber($main), null]; + + while ($this->ready !== [] || $this->fiberWatchers !== [] || $this->fiberDeadlines !== []) { + $this->runReadyFibers(); + $this->pollFds(); + } + } + + public function runReadyFibers(): void + { + while ($this->ready !== []) { + [$fiber, $resumeValue] = array_shift($this->ready); + $fiber->isSuspended() ? $fiber->resume($resumeValue) : $fiber->start(); + } + } + + public function pollFds(): void + { + if ($this->fiberWatchers === [] && $this->fiberDeadlines === []) { + return; + } + + // Compute wait() timeout from the nearest deadline + $timeoutSec = null; + $timeoutUsec = 0; + $now = hrtime(true); + foreach ($this->fiberDeadlines as [$deadline]) { + $remaining = $deadline - $now; + if ($remaining <= 0) { + $timeoutSec = 0; + $timeoutUsec = 0; + break; + } + $remainingUsec = intdiv($remaining, 1_000); + if ($timeoutSec === null || $remainingUsec < $timeoutSec * 1_000_000 + $timeoutUsec) { + $timeoutSec = intdiv($remainingUsec, 1_000_000); + $timeoutUsec = $remainingUsec % 1_000_000; + } + } + + $watchers = $this->pollContext->wait($timeoutSec, $timeoutUsec); + + // Group ready PollResults by fiber before resuming anyone + $fiberResults = []; + foreach ($watchers as $watcher) { + [$fiber] = $watcher->getData(); + $fiberId = spl_object_id($fiber); + if (!isset($this->fiberWatchers[$fiberId])) { + continue; + } + $result = new PollResult(); + $result->handle = $watcher->getHandle(); + $result->events = $watcher->getTriggeredEvents(); + $result->timeout = false; + $fiberResults[$fiberId] ??= [$fiber, []]; + $fiberResults[$fiberId][1][] = $result; + } + + foreach ($fiberResults as $fiberId => [$fiber, $results]) { + if (!isset($this->fiberWatchers[$fiberId])) { + continue; + } + $this->removeAllFiberWatchers($fiberId); + unset($this->fiberDeadlines[$fiberId]); + + if ($this->fiberIsMulti[$fiberId] ?? false) { + unset($this->fiberIsMulti[$fiberId]); + $this->ready[] = [$fiber, $results]; // array of PollResult + } else { + $this->ready[] = [$fiber, $results[0]]; // single PollResult + } + } + + // Handle expired deadlines + $now = hrtime(true); + foreach ($this->fiberDeadlines as $fiberId => [$deadline, $fiber, $info]) { + if ($deadline > $now) { + continue; + } + $this->removeAllFiberWatchers($fiberId); + unset($this->fiberDeadlines[$fiberId]); + + if ($this->fiberIsMulti[$fiberId] ?? false) { + unset($this->fiberIsMulti[$fiberId]); + $this->ready[] = [$fiber, []]; // empty array = timeout for poll_multi + } else { + $result = new PollResult(); + $result->handle = $info->handle; + $result->events = $info->events; + $result->timeout = true; + $this->ready[] = [$fiber, $result]; + } + } + } + + private function removeAllFiberWatchers(int $fiberId): void + { + foreach ($this->fiberWatchers[$fiberId] as [$w, $fdKey]) { + if ($fdKey !== null) { + unset($this->handles[$fdKey]); + } + $w->remove(); + } + unset($this->fiberWatchers[$fiberId]); + } + + public function poll(PollInfo $info): PollResult + { + $fiber = Fiber::getCurrent(); + $fiberId = spl_object_id($fiber); + + if ($info->timeout_ms >= 0) { + $deadline = hrtime(true) + $info->timeout_ms * 1_000_000; + $this->fiberDeadlines[$fiberId] = [$deadline, $fiber, $info]; + } + + $id = spl_object_id($info->handle); + if (isset($this->handles[$id])) { + throw new \Exception("Handle already registered"); + } + $this->handles[$id] = $id; + $this->fiberWatchers[$fiberId] = [ + [$this->pollContext->add($info->handle, $info->events, [$fiber]), $id], + ]; + + /** @var PollResult $result */ + $result = Fiber::suspend(); + return $result; + } + + public function poll_multi(?int $timeout_ms, PollInfo ...$infos): array + { + $fiber = Fiber::getCurrent(); + $fiberId = spl_object_id($fiber); + $this->fiberWatchers[$fiberId] = []; + $this->fiberIsMulti[$fiberId] = true; + + if ($timeout_ms !== null && $timeout_ms >= 0) { + $deadline = hrtime(true) + $timeout_ms * 1_000_000; + $this->fiberDeadlines[$fiberId] = [$deadline, $fiber, null]; + } + + foreach ($infos as $info) { + $this->fiberWatchers[$fiberId][] = [ + $this->pollContext->add($info->handle, $info->events, [$fiber]), + null, + ]; + } + + /** @var PollResult[] $results */ + $results = Fiber::suspend(); + return $results ?? []; + } + + public function go(callable $fn): void + { + $this->ready[] = [new Fiber($fn), null]; + $this->ready[] = [Fiber::getCurrent(), null]; + Fiber::suspend(); + } +} diff --git a/ext/standard/tests/streams/hooks/use-case.phpt b/ext/standard/tests/streams/hooks/use-case.phpt index 42a69c6e697e..fabcc1a330b6 100644 --- a/ext/standard/tests/streams/hooks/use-case.phpt +++ b/ext/standard/tests/streams/hooks/use-case.phpt @@ -3,154 +3,13 @@ Stream hook: use case --FILE-- Watcher[] - private array $fiberDeadlines = []; // fiberId => [deadline_ns, fiber, PollInfo] - - public function __construct() - { - $this->pollContext = new Context(); - } - - public function run($main): void - { - $this->ready[] = [new Fiber($main), null]; - - while ($this->ready !== [] || $this->fds !== [] || $this->fiberDeadlines !== []) { - $this->runReadyFibers(); - $this->pollFds(); - } - } - - public function runReadyFibers(): void - { - while ($this->ready !== []) { - [$fiber, $resumeValue] = array_shift($this->ready); - $fiber->isSuspended() ? $fiber->resume($resumeValue) : $fiber->start(); - } - } - - public function pollFds(): void - { - if ($this->fds === [] && $this->fiberDeadlines === []) { - return; - } - - // Compute wait() timeout from the nearest deadline - $timeoutSec = null; - $timeoutUsec = 0; - $now = hrtime(true); - foreach ($this->fiberDeadlines as [$deadline]) { - $remaining = $deadline - $now; - if ($remaining <= 0) { - $timeoutSec = 0; - $timeoutUsec = 0; - break; - } - $remainingUsec = intdiv($remaining, 1_000); - if ($timeoutSec === null || $remainingUsec < $timeoutSec * 1_000_000 + $timeoutUsec) { - $timeoutSec = intdiv($remainingUsec, 1_000_000); - $timeoutUsec = $remainingUsec % 1_000_000; - } - } - - $watchers = $this->pollContext->wait($timeoutSec, $timeoutUsec); - - // Handle ready handles - foreach ($watchers as $watcher) { - [$fiber] = $watcher->getData(); - $fiberId = spl_object_id($fiber); - - if (!isset($this->fiberWatchers[$fiberId])) { - continue; - } - - $this->removeAllFiberWatchers($fiberId); - unset($this->fiberDeadlines[$fiberId]); - - $result = new PollResult(); - $result->handle = $watcher->getHandle(); - $result->events = $watcher->getTriggeredEvents(); - $result->timeout = false; - - $this->ready[] = [$fiber, $result]; - } - - // Handle expired deadlines - $now = hrtime(true); - foreach ($this->fiberDeadlines as $fiberId => [$deadline, $fiber, $info]) { - if ($deadline > $now) { - continue; - } - - $this->removeAllFiberWatchers($fiberId); - unset($this->fiberDeadlines[$fiberId]); - - $result = new PollResult(); - $result->handle = $info->handle; - $result->events = $info->events; - $result->timeout = true; - - $this->ready[] = [$fiber, $result]; - } - } - - private function removeAllFiberWatchers(int $fiberId): void - { - foreach ($this->fiberWatchers[$fiberId] as $w) { - unset($this->fds[(int)$w->getHandle()->getStream()]); - $w->remove(); - } - unset($this->fiberWatchers[$fiberId]); - } - - public function poll(PollInfo $info): PollResult - { - $fiber = Fiber::getCurrent(); - $fiberId = spl_object_id($fiber); - - if ($info->timeout_ms >= 0) { - $deadline = hrtime(true) + $info->timeout_ms * 1_000_000; - $this->fiberDeadlines[$fiberId] = [$deadline, $fiber, $info]; - } - - $id = (int)$info->handle->getStream(); - if (isset($this->fds[$id])) { - throw new \Exception("Handle already registered"); - } - $this->fds[$id] = $id; - $this->fiberWatchers[$fiberId] = [$this->pollContext->add($info->handle, $info->events, [$fiber])]; - - /** @var PollResult $result */ - $result = Fiber::suspend(); - return $result; - } - - public function poll_multi(?int $timeout_ms, PollInfo ...$infos): array - { - throw new \Exception("poll_multi not implemented"); - } - - public function go(callable $fn): void - { - $this->ready[] = [new Fiber($fn), null]; - $this->ready[] = [Fiber::getCurrent(), null]; - Fiber::suspend(); - } -} +include __DIR__ . '/scheduler.inc'; $scheduler = new Scheduler(); - Io\Hooks\set_hooks($scheduler); -function go($fn) { +function go(callable $fn): void +{ global $scheduler; $scheduler->go($fn); } From 06e3596aa4494de7a08f2142e377e029b237ee8f Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Wed, 24 Jun 2026 17:22:38 +0200 Subject: [PATCH 10/18] Prevent concurrent access to streams Rationale: * We can't ensure consistency of internal structures when a stream is accessed concurrency, at least for the same direction. * It may be possible to accept concurrent accesses from different directions (read and write) * Valid use-cases for concurrent accesses in the same direction are unknown. This would require synchronization from the concumer. --- ext/standard/io_hooks.c | 12 ++---- .../tests/streams/hooks/hook-close-fgets.phpt | 15 +++---- .../streams/hooks/hook-concurrent-access.phpt | 40 +++++++++++++++++++ main/php_streams.h | 9 +++++ 4 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 ext/standard/tests/streams/hooks/hook-concurrent-access.phpt diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c index d2942cffe25d..9b03b165ca00 100644 --- a/ext/standard/io_hooks.c +++ b/ext/standard/io_hooks.c @@ -53,9 +53,6 @@ static void php_pollfd_events_to_io_poll_events(zend_array *dest, int events) PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, const struct timeval *timeout) { - uint32_t orig_no_fclose = stream->flags & PHP_STREAM_FLAG_NO_FCLOSE; - stream->flags |= PHP_STREAM_FLAG_NO_FCLOSE; - // TODO: optimize poll_info object creation+init. Maybe reuse it too (e.g. if RC=1) zval poll_info; object_init_ex(&poll_info, php_io_hooks_poll_info_ce); @@ -84,7 +81,10 @@ PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, con zval retval; ZVAL_UNDEF(&retval); + ZEND_ASSERT(!(stream->flags & PHP_STREAM_FLAG_BEING_POLLED)); + stream->flags |= PHP_STREAM_FLAG_BEING_POLLED; zend_call_known_fcc(&FG(io_hooks_poll_fcc), &retval, 1, &poll_info, NULL); + stream->flags &= ~PHP_STREAM_FLAG_BEING_POLLED; zval_ptr_dtor(&poll_info); if (EG(exception)) { @@ -99,18 +99,12 @@ PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, con goto return_error; } - stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; - stream->flags |= orig_no_fclose; - return Z_OBJ(retval); return_error: ZEND_ASSERT(EG(exception)); zval_ptr_dtor(&retval); - stream->flags &= ~PHP_STREAM_FLAG_NO_FCLOSE; - stream->flags |= orig_no_fclose; - return NULL; } diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt index 913cf2be8430..d70ee40d9360 100644 --- a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -1,5 +1,5 @@ --TEST-- -Stream hook: closed during read hook +Stream hook: fclose() in poll() throws concurrent access error --FILE-- handle->getStream()); - Io\Hooks\set_hooks(null); $result = new PollResult(); $result->handle = $info->handle; $result->events = $info->events; @@ -28,9 +27,11 @@ class CloseOnceHooks implements Hooks { } Io\Hooks\set_hooks(new CloseOnceHooks()); -var_dump(fgets($client)); +try { + fgets($client); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} ?> ---EXPECTF-- -Warning: fclose(): cannot close the provided stream, as it must not be manually closed in %s on line %d -string(6) "handle->getStream()); + } catch (Error $e) { + echo $e->getMessage() . "\n"; + } + $result = new PollResult(); + $result->handle = $info->handle; + $result->events = $info->events; + $result->timeout = false; + return $result; + } + public function poll_multi(?int $timeout_ms, PollInfo ...$info): array { + throw new \Exception("poll_multi not implemented"); + } +} + +Io\Hooks\set_hooks(new ConcurrentHook()); +var_dump(fgets($client)); +?> +--EXPECT-- +Concurrent access to a stream +string(6) "hello +" diff --git a/main/php_streams.h b/main/php_streams.h index 756fbaab8d94..4eac49e27399 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -189,6 +189,11 @@ struct _php_stream_wrapper { #define PHP_STREAM_FLAG_NO_IO 0x400 +/* Set while the stream is being polled via the io hook (poll/poll_multi). + * Any attempt to use the stream as a parameter during this window is a + * concurrent-access bug and will throw an Error. */ +#define PHP_STREAM_FLAG_BEING_POLLED 0x800 + #define PHP_STREAM_FLAG_WAS_WRITTEN 0x80000000 struct _php_stream { @@ -306,6 +311,10 @@ static zend_always_inline bool php_stream_zend_parse_arg_into_stream( * as we want to be able to specify the argument number in the type error */ if (EXPECTED(res->type == php_file_le_stream() || res->type == php_file_le_pstream())) { *destination_stream_ptr = (php_stream*)res->ptr; + if (UNEXPECTED((*destination_stream_ptr)->flags & PHP_STREAM_FLAG_BEING_POLLED)) { + zend_throw_error(NULL, "Concurrent access to a stream"); + return false; + } return true; } else { zend_argument_type_error(arg_num, "must be an open stream resource"); From ecc3309fa032018f043df25366d5405f2dcd2775 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 26 Jun 2026 15:20:41 +0200 Subject: [PATCH 11/18] Context: Add ability to watch weakly * Introduce StreamPollWeakHandle, which holds its stream weakly * Context stops watching any Handle whose stream is collected * Introduce Context::onWatcherRemoved(?callable $callback). Callback is invoked when a weak handle is removed automatically. * Generalized to Curl's SocketHandle -> SocketWeakHandle --- ext/curl/curl_private.h | 3 +- ext/curl/curl_socket_handle.stub.php | 2 +- ext/curl/curl_socket_handle_arginfo.h | 6 +- ext/curl/interface.c | 93 ++++-- ext/standard/io_hooks.c | 3 +- ext/standard/io_hooks.stub.php | 2 - ext/standard/io_hooks_arginfo.h | 3 +- ext/standard/io_poll.c | 292 ++++++++++++++++-- ext/standard/io_poll.h | 3 + ext/standard/io_poll.stub.php | 17 + ext/standard/io_poll_arginfo.h | 37 ++- ext/standard/io_poll_decl.h | 8 +- ...ll_weakhandle_context_destroyed_first.phpt | 24 ++ .../poll/poll_weakhandle_getstream_null.phpt | 21 ++ .../poll/poll_weakhandle_manual_remove.phpt | 34 ++ .../poll_weakhandle_multiple_contexts.phpt | 36 +++ .../poll_weakhandle_on_watcher_removed.phpt | 34 ++ .../poll_weakhandle_readd_after_remove.phpt | 28 ++ .../tests/poll/poll_weakhandle_stream_gc.phpt | 39 +++ .../tests/streams/hooks/use-case.phpt | 1 - main/php_poll.h | 4 +- main/php_streams.h | 5 + main/poll/poll_handle.c | 6 + main/streams/streams.c | 7 + 24 files changed, 650 insertions(+), 58 deletions(-) create mode 100644 ext/standard/tests/poll/poll_weakhandle_context_destroyed_first.phpt create mode 100644 ext/standard/tests/poll/poll_weakhandle_getstream_null.phpt create mode 100644 ext/standard/tests/poll/poll_weakhandle_manual_remove.phpt create mode 100644 ext/standard/tests/poll/poll_weakhandle_multiple_contexts.phpt create mode 100644 ext/standard/tests/poll/poll_weakhandle_on_watcher_removed.phpt create mode 100644 ext/standard/tests/poll/poll_weakhandle_readd_after_remove.phpt create mode 100644 ext/standard/tests/poll/poll_weakhandle_stream_gc.phpt diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h index e195faf95178..3922eb451b56 100644 --- a/ext/curl/curl_private.h +++ b/ext/curl/curl_private.h @@ -115,7 +115,8 @@ typedef struct { /* CurlShareHandle object set using CURLOPT_SHARE. */ struct _php_curlsh *share; /* Socket/timer state during curl_exec (NULL outside exec) */ - HashTable *io_sockets; /* curl_socket_t -> int events */ + HashTable *io_sockets; /* curl_socket_t -> int events */ + HashTable *io_socket_handles; /* curl_socket_t -> zend_object* (SocketWeakHandle, no refcount) */ long io_timer_ms; /* timer value from TIMERFUNCTION, -1 = disabled */ zend_object std; } php_curl; diff --git a/ext/curl/curl_socket_handle.stub.php b/ext/curl/curl_socket_handle.stub.php index 68469010cfd2..eaa1a019ea30 100644 --- a/ext/curl/curl_socket_handle.stub.php +++ b/ext/curl/curl_socket_handle.stub.php @@ -10,5 +10,5 @@ * @strict-properties * @not-serializable */ - final class SocketHandle implements Handle {} + final class SocketWeakHandle implements Handle {} } diff --git a/ext/curl/curl_socket_handle_arginfo.h b/ext/curl/curl_socket_handle_arginfo.h index 9d96c03345d9..b502b5ce92fc 100644 --- a/ext/curl/curl_socket_handle_arginfo.h +++ b/ext/curl/curl_socket_handle_arginfo.h @@ -1,11 +1,11 @@ /* This is a generated file, edit curl_socket_handle.stub.php instead. - * Stub hash: 089af547d4ad6a7939ef611b7131e38f0bf9357b */ + * Stub hash: 1aefd753a56696f9723cdb95277da5e86514a363 */ -static zend_class_entry *register_class_Io_Curl_SocketHandle(zend_class_entry *class_entry_Io_Poll_Handle) +static zend_class_entry *register_class_Io_Curl_SocketWeakHandle(zend_class_entry *class_entry_Io_Poll_Handle) { zend_class_entry ce, *class_entry; - INIT_NS_CLASS_ENTRY(ce, "Io\\Curl", "SocketHandle", NULL); + INIT_NS_CLASS_ENTRY(ce, "Io\\Curl", "SocketWeakHandle", NULL); class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); zend_class_implements(class_entry, 1, class_entry_Io_Poll_Handle); diff --git a/ext/curl/interface.c b/ext/curl/interface.c index c4b2e9724c91..c1f6f99bb12b 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -43,7 +43,6 @@ #include "ext/standard/file.h" #include "ext/standard/url.h" #include "ext/standard/io_poll.h" -#include "ext/standard/io_hooks.h" #include "main/php_poll.h" #include "curl_private.h" #include "curl_socket_handle_arginfo.h" @@ -221,7 +220,7 @@ zend_class_entry *curl_ce; zend_class_entry *curl_share_ce; zend_class_entry *curl_share_persistent_ce; static zend_object_handlers curl_object_handlers; -static zend_class_entry *php_curl_socket_handle_ce; +static zend_class_entry *php_curl_socket_weak_handle_ce; static zend_object_handlers php_curl_socket_handle_object_handlers; static zend_object *php_curl_socket_handle_create_object(zend_class_entry *ce); @@ -390,13 +389,13 @@ PHP_MINIT_FUNCTION(curl) curlfile_register_class(); - php_curl_socket_handle_ce = register_class_Io_Curl_SocketHandle(php_io_poll_handle_class_entry); - php_curl_socket_handle_ce->create_object = php_curl_socket_handle_create_object; + php_curl_socket_weak_handle_ce = register_class_Io_Curl_SocketWeakHandle(php_io_poll_handle_class_entry); + php_curl_socket_weak_handle_ce->create_object = php_curl_socket_handle_create_object; memcpy(&php_curl_socket_handle_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); - php_curl_socket_handle_object_handlers.offset = XtOffsetOf(php_poll_handle_object, std); + php_curl_socket_handle_object_handlers.offset = offsetof(php_poll_handle_object, std); php_curl_socket_handle_object_handlers.free_obj = php_poll_handle_object_free; - php_curl_socket_handle_ce->default_object_handlers = &php_curl_socket_handle_object_handlers; + php_curl_socket_weak_handle_ce->default_object_handlers = &php_curl_socket_handle_object_handlers; return SUCCESS; } @@ -1081,6 +1080,7 @@ void init_curl_handle(php_curl *ch) zend_hash_init(&ch->to_free->slist, 4, NULL, curl_free_slist, 0); ZVAL_UNDEF(&ch->postfields); ch->io_sockets = NULL; + ch->io_socket_handles = NULL; ch->io_timer_ms = -1; } @@ -2333,27 +2333,46 @@ PHP_FUNCTION(curl_setopt_array) } /* }}} */ -/* Io\Curl\SocketHandle: wraps a curl_socket_t as an Io\Poll\Handle */ +/* Io\Curl\SocketWeakHandle: wraps a curl_socket_t as an Io\Poll\Handle without owning it. + * When curl closes the socket, the socket field is zeroed via CURLOPT_CLOSESOCKETFUNCTION. */ typedef struct { - curl_socket_t socket; + curl_socket_t socket; /* CURL_SOCKET_BAD when closed; no fd ownership */ + HashTable *io_socket_handles; /* points to ch->io_socket_handles; NULL when socket is closed */ } php_curl_socket_handle_data; static php_socket_t php_curl_socket_handle_get_fd(php_poll_handle_object *handle) { - return (php_socket_t)((php_curl_socket_handle_data *)handle->handle_data)->socket; + php_curl_socket_handle_data *data = handle->handle_data; + return data ? (php_socket_t)data->socket : (php_socket_t)CURL_SOCKET_BAD; } static int php_curl_socket_handle_is_valid(php_poll_handle_object *handle) { - return handle->handle_data != NULL && - ((php_curl_socket_handle_data *)handle->handle_data)->socket != CURL_SOCKET_BAD; + php_curl_socket_handle_data *data = handle->handle_data; + return data && data->socket != CURL_SOCKET_BAD; } static void php_curl_socket_handle_cleanup(php_poll_handle_object *handle) { - efree(handle->handle_data); - handle->handle_data = NULL; + php_curl_socket_handle_data *data = handle->handle_data; + if (data) { + if (data->socket != CURL_SOCKET_BAD && data->io_socket_handles) { + zend_hash_index_del(data->io_socket_handles, (zend_ulong)data->socket); + } + efree(data); + handle->handle_data = NULL; + } +} + +static void php_curl_socket_handle_notify(php_poll_handle_object *handle) +{ + php_curl_socket_handle_data *data = handle->handle_data; + data->socket = CURL_SOCKET_BAD; + data->io_socket_handles = NULL; + if (handle->watching) { + php_io_poll_handle_remove_from_all_contexts(&handle->std); + } } static php_poll_handle_ops php_curl_socket_handle_ops = { @@ -2370,13 +2389,45 @@ static zend_object *php_curl_socket_handle_create_object(zend_class_entry *ce) return &intern->std; } -static void php_curl_socket_handle_from_fd(zval *dest, curl_socket_t s) +/* CURLOPT_CLOSESOCKETFUNCTION: zero the SocketWeakHandle for this socket, then close it */ +static int php_curl_close_socket_callback(void *clientp, curl_socket_t item) { - object_init_ex(dest, php_curl_socket_handle_ce); + php_curl *ch = clientp; + if (ch->io_socket_handles) { + zval *hzv = zend_hash_index_find(ch->io_socket_handles, (zend_ulong)item); + if (hzv) { + php_poll_handle_object *h = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ((zend_object *)Z_PTR_P(hzv)); + php_curl_socket_handle_notify(h); + zend_hash_index_del(ch->io_socket_handles, (zend_ulong)item); + } + } + return closesocket(item); +} + +/* Singleton factory: returns existing SocketWeakHandle for this socket, or creates one */ +static void php_curl_socket_handle_from_fd(php_curl *ch, zval *dest, curl_socket_t s) +{ + if (ch->io_socket_handles) { + zval *existing = zend_hash_index_find(ch->io_socket_handles, (zend_ulong)s); + if (existing) { + ZVAL_OBJ_COPY(dest, (zend_object *)Z_PTR_P(existing)); + return; + } + } else { + ch->io_socket_handles = emalloc(sizeof(HashTable)); + zend_hash_init(ch->io_socket_handles, 4, NULL, NULL, 0); + } + + object_init_ex(dest, php_curl_socket_weak_handle_ce); php_poll_handle_object *h = PHP_POLL_HANDLE_OBJ_FROM_ZV(dest); php_curl_socket_handle_data *data = emalloc(sizeof(php_curl_socket_handle_data)); data->socket = s; + data->io_socket_handles = ch->io_socket_handles; h->handle_data = data; + + zval ptr_zv; + ZVAL_PTR(&ptr_zv, Z_OBJ_P(dest)); + zend_hash_index_update(ch->io_socket_handles, (zend_ulong)s, &ptr_zv); } /* CURLMOPT_SOCKETFUNCTION: log socket+events in ch->io_sockets */ @@ -2446,6 +2497,8 @@ static CURLcode php_curl_exec_multi(php_curl *ch) CURLM *multi = curl_multi_init(); curl_multi_setopt(multi, CURLMOPT_SOCKETFUNCTION, php_curl_socket_callback); curl_multi_setopt(multi, CURLMOPT_SOCKETDATA, ch); + curl_easy_setopt(ch->cp, CURLOPT_CLOSESOCKETFUNCTION, php_curl_close_socket_callback); + curl_easy_setopt(ch->cp, CURLOPT_CLOSESOCKETDATA, ch); curl_multi_setopt(multi, CURLMOPT_TIMERFUNCTION, php_curl_timer_callback); curl_multi_setopt(multi, CURLMOPT_TIMERDATA, ch); @@ -2483,7 +2536,7 @@ static CURLcode php_curl_exec_multi(php_curl *ch) object_init_ex(poll_info, php_io_hooks_poll_info_ce); zval handle; - php_curl_socket_handle_from_fd(&handle, (curl_socket_t)sock_ulong); + php_curl_socket_handle_from_fd(ch, &handle, (curl_socket_t)sock_ulong); zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ_P(poll_info), "handle", sizeof("handle") - 1, &handle); zval_ptr_dtor(&handle); @@ -2577,8 +2630,7 @@ static CURLcode php_curl_exec_multi(php_curl *ch) int ready_ev[16]; int ready_n = 0; zend_ulong sock_ulong; - zval *ev; - ZEND_HASH_FOREACH_NUM_KEY_VAL(ch->io_sockets, sock_ulong, ev) { + ZEND_HASH_FOREACH_NUM_KEY(ch->io_sockets, sock_ulong) { curl_socket_t s = (curl_socket_t)sock_ulong; int ce = 0; if (FD_ISSET(s, &rfds)) ce |= CURL_CSELECT_IN; @@ -2613,6 +2665,11 @@ static CURLcode php_curl_exec_multi(php_curl *ch) efree(ch->io_sockets); ch->io_sockets = NULL; } + if (ch->io_socket_handles) { + zend_hash_destroy(ch->io_socket_handles); + efree(ch->io_socket_handles); + ch->io_socket_handles = NULL; + } ch->io_timer_ms = -1; return result; diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c index 9b03b165ca00..d85ffb1b08a5 100644 --- a/ext/standard/io_hooks.c +++ b/ext/standard/io_hooks.c @@ -58,7 +58,7 @@ PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, con object_init_ex(&poll_info, php_io_hooks_poll_info_ce); zval handle; - php_stream_poll_handle_from_stream(&handle, stream); + php_stream_poll_weak_handle_from_stream(&handle, stream); zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ(poll_info), "handle", sizeof("handle") - 1, &handle); zval_ptr_dtor(&handle); @@ -154,6 +154,7 @@ PHP_FUNCTION(Io_Hooks_set_hooks) .called_scope = obj->ce, }; GC_ADDREF(obj); + } PHP_MINIT_FUNCTION(io_hooks) diff --git a/ext/standard/io_hooks.stub.php b/ext/standard/io_hooks.stub.php index baddbc18bc41..ae48d64f2f48 100644 --- a/ext/standard/io_hooks.stub.php +++ b/ext/standard/io_hooks.stub.php @@ -4,8 +4,6 @@ namespace Io\Hooks { - use Io\Poll\Handle; - final class PollInfo { public Handle $handle; /* @var Io\Poll\Event[] */ diff --git a/ext/standard/io_hooks_arginfo.h b/ext/standard/io_hooks_arginfo.h index d0514b619a14..7da3897b4f93 100644 --- a/ext/standard/io_hooks_arginfo.h +++ b/ext/standard/io_hooks_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit io_hooks.stub.php instead. - * Stub hash: 86abce168edd66424958da19478649fc38f059e6 */ + * Stub hash: b741447fd9d275264617736e8130562298da3d45 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_Io_Hooks_set_hooks, 0, 1, Io\\Hooks\\Hooks, 1) ZEND_ARG_OBJ_INFO(0, hooks, Io\\Hooks\\Hooks, 1) @@ -14,6 +14,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Hooks_Hooks_poll_multi, ZEND_ARG_VARIADIC_OBJ_INFO(0, info, Io\\Hooks\\PollInfo, 0) ZEND_END_ARG_INFO() + ZEND_FUNCTION(Io_Hooks_set_hooks); static const zend_function_entry ext_functions[] = { diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c index dc8ce4c21891..9e750a2b34b2 100644 --- a/ext/standard/io_poll.c +++ b/ext/standard/io_poll.c @@ -44,6 +44,10 @@ static zend_object_handlers php_io_poll_context_object_handlers; static zend_object_handlers php_io_poll_watcher_object_handlers; static zend_object_handlers php_io_poll_handle_object_handlers; +/* Forward declarations */ +typedef struct php_io_poll_context_object php_io_poll_context_object; +PHPAPI void php_io_poll_handle_remove_from_all_contexts(zend_object *handle_obj); + /* Watcher object structure */ typedef struct php_io_poll_watcher_object { php_poll_handle_object *handle; @@ -51,16 +55,17 @@ typedef struct php_io_poll_watcher_object { uint32_t triggered_events; zval data; bool active; - php_poll_ctx *poll_ctx; /* Back reference to poll context */ + php_io_poll_context_object *context; /* back-pointer, no refcount */ zend_object std; } php_io_poll_watcher_object; /* Context object structure */ -typedef struct php_io_poll_context_object { +struct php_io_poll_context_object { php_poll_ctx *ctx; HashTable *watchers; /* Maps handle pointer -> watcher object */ + zend_fcall_info_cache on_watcher_removed_fcc; zend_object std; -} php_io_poll_context_object; +}; /* Stream poll handle specific data */ typedef struct php_stream_poll_handle_data { @@ -232,6 +237,126 @@ PHPAPI void php_stream_poll_handle_from_stream(zval *dest, php_stream *stream) GC_ADDREF(stream->res); } +/* StreamPollWeakHandle: like StreamPollHandle but does not hold a refcount on the stream. + * When the stream is freed, the stream pointer is zeroed and the Handle is + * removed from any Context (php_stream_poll_weak_handle_notify). */ + +static zend_class_entry *php_stream_poll_weak_handle_class_entry; + +typedef struct { + php_stream *stream; /* NULL when stream has been closed; no refcount held */ +} php_stream_poll_weak_handle_data; + +static php_socket_t php_stream_poll_weak_handle_get_fd(php_poll_handle_object *handle) +{ + php_stream_poll_weak_handle_data *data = handle->handle_data; + if (!data || !data->stream) { + return SOCK_ERR; + } + php_socket_t fd; + if (php_stream_cast(data->stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, + (void *) &fd, 1) != SUCCESS || fd == -1) { + return SOCK_ERR; + } + return fd; +} + +static int php_stream_poll_weak_handle_is_valid(php_poll_handle_object *handle) +{ + php_stream_poll_weak_handle_data *data = handle->handle_data; + return data && data->stream && !php_stream_eof(data->stream); +} + +static void php_stream_poll_weak_handle_cleanup(php_poll_handle_object *handle) +{ + php_stream_poll_weak_handle_data *data = handle->handle_data; + if (data) { + if (data->stream) { + data->stream->weak_poll_handle = NULL; + } + efree(data); + handle->handle_data = NULL; + } +} + +PHPAPI void php_stream_poll_weak_handle_notify(zend_object *handle_obj) +{ + php_poll_handle_object *handle = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(handle_obj); + php_stream_poll_weak_handle_data *data = handle->handle_data; + data->stream = NULL; + if (handle->watching) { + php_io_poll_handle_remove_from_all_contexts(handle_obj); + } +} + +static php_poll_handle_ops php_stream_poll_weak_handle_ops = { + .get_fd = php_stream_poll_weak_handle_get_fd, + .is_valid = php_stream_poll_weak_handle_is_valid, + .cleanup = php_stream_poll_weak_handle_cleanup, +}; + +static zend_object *php_stream_poll_weak_handle_create_object(zend_class_entry *ce) +{ + php_poll_handle_object *intern = php_poll_handle_object_create( + sizeof(php_poll_handle_object), ce, &php_stream_poll_weak_handle_ops); + intern->std.handlers = &php_io_poll_handle_object_handlers; + return &intern->std; +} + +PHPAPI void php_stream_poll_weak_handle_from_stream(zval *dest, php_stream *stream) +{ + if (stream->weak_poll_handle) { + ZVAL_OBJ_COPY(dest, stream->weak_poll_handle); + return; + } + + object_init_ex(dest, php_stream_poll_weak_handle_class_entry); + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(dest); + + php_stream_poll_weak_handle_data *data = emalloc(sizeof(php_stream_poll_weak_handle_data)); + data->stream = stream; + intern->handle_data = data; + + stream->weak_poll_handle = Z_OBJ_P(dest); +} + +PHP_METHOD(StreamPollWeakHandle, __construct) +{ + zend_throw_error(NULL, + "Direct instantiation of StreamPollWeakHandle is not allowed, use StreamPollWeakHandle::create instead"); +} + +PHP_METHOD(StreamPollWeakHandle, create) +{ + zval *stream_zv; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_RESOURCE(stream_zv) + ZEND_PARSE_PARAMETERS_END(); + + php_stream *stream = (php_stream *) zend_fetch_resource2( + Z_RES_P(stream_zv), "stream", php_file_le_stream(), php_file_le_pstream()); + if (!stream) { + RETURN_THROWS(); + } + + php_stream_poll_weak_handle_from_stream(return_value, stream); +} + +PHP_METHOD(StreamPollWeakHandle, getStream) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis()); + php_stream_poll_weak_handle_data *data = intern->handle_data; + + if (!data || !data->stream) { + RETURN_NULL(); + } + + GC_ADDREF(data->stream->res); + php_stream_to_zval(data->stream, return_value); +} + /* Handle interface internal only */ static int php_stream_poll_handle_implement_interface(zend_class_entry *interface, zend_class_entry *implementor) { @@ -263,7 +388,6 @@ static zend_object *php_io_poll_watcher_create_object(zend_class_entry *ce) intern->watched_events = 0; intern->triggered_events = 0; intern->active = false; - intern->poll_ctx = NULL; ZVAL_NULL(&intern->data); return &intern->std; @@ -278,10 +402,19 @@ static zend_object *php_io_poll_context_create_object(zend_class_entry *ce) intern->ctx = NULL; intern->watchers = NULL; + intern->on_watcher_removed_fcc = empty_fcall_info_cache; return &intern->std; } +/* Utility functions */ + +static zend_always_inline zend_ulong php_io_poll_compute_ptr_key(void *ptr) +{ + zend_ulong key = (zend_ulong) (uintptr_t) ptr; + return (key >> 3) | (key << ((sizeof(key) * 8) - 3)); +} + /* Object Destruction Functions */ static void php_io_poll_watcher_free_object(zend_object *obj) @@ -304,8 +437,12 @@ static void php_io_poll_context_free_object(zend_object *obj) if (intern->watchers) { ZEND_HASH_FOREACH_VAL(intern->watchers, zval *zv) { php_io_poll_watcher_object *watcher = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(Z_OBJ_P(zv)); + if (watcher->active && watcher->handle && watcher->handle->watching) { + zend_ulong ctx_key = php_io_poll_compute_ptr_key(intern); + zend_hash_index_del(watcher->handle->watching, ctx_key); + } watcher->active = false; - watcher->poll_ctx = NULL; + watcher->context = NULL; } ZEND_HASH_FOREACH_END(); } @@ -318,6 +455,10 @@ static void php_io_poll_context_free_object(zend_object *obj) efree(intern->watchers); } + if (ZEND_FCC_INITIALIZED(intern->on_watcher_removed_fcc)) { + zend_fcc_dtor(&intern->on_watcher_removed_fcc); + } + zend_object_std_dtor(&intern->std); } @@ -346,22 +487,23 @@ static HashTable *php_io_poll_context_get_gc(zend_object *obj, zval **table, int } ZEND_HASH_FOREACH_END(); } + if (ZEND_FCC_INITIALIZED(intern->on_watcher_removed_fcc)) { + if (intern->on_watcher_removed_fcc.object) { + zend_get_gc_buffer_add_obj(gc_buffer, intern->on_watcher_removed_fcc.object); + } + if (intern->on_watcher_removed_fcc.closure) { + zend_get_gc_buffer_add_obj(gc_buffer, intern->on_watcher_removed_fcc.closure); + } + } + zend_get_gc_buffer_use(gc_buffer, table, n); return NULL; } -/* Utility functions */ - -static zend_always_inline zend_ulong php_io_poll_compute_ptr_key(void *ptr) -{ - zend_ulong key = (zend_ulong) (uintptr_t) ptr; - return (key >> 3) | (key << ((sizeof(key) * 8) - 3)); -} - static zend_result php_io_poll_watcher_modify_events( php_io_poll_watcher_object *watcher, uint32_t events) { - if (!watcher->active || !watcher->poll_ctx) { + if (!watcher->active || !watcher->context) { zend_throw_exception( php_io_poll_inactive_watcher_class_entry, "Cannot modify inactive watcher", 0); return FAILURE; @@ -375,8 +517,8 @@ static zend_result php_io_poll_watcher_modify_events( } /* Modify in poll context */ - if (php_poll_modify(watcher->poll_ctx, (int) fd, events, watcher) != SUCCESS) { - php_poll_error err = php_poll_get_error(watcher->poll_ctx); + if (php_poll_modify(watcher->context->ctx, (int) fd, events, watcher) != SUCCESS) { + php_poll_error err = php_poll_get_error(watcher->context->ctx); php_io_poll_throw_failed_operation(php_io_poll_failed_watcher_mod_class_entry, "Failed to modify watcher in polling system", err); return FAILURE; @@ -644,19 +786,35 @@ PHP_METHOD(Io_Poll_Watcher, remove) php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); - if (!intern->active || !intern->poll_ctx) { + if (!intern->active || !intern->context) { zend_throw_exception( php_io_poll_inactive_watcher_class_entry, "Cannot remove inactive watcher", 0); RETURN_THROWS(); } - php_socket_t fd = php_poll_handle_get_fd(intern->handle); + php_io_poll_context_object *context = intern->context; + php_poll_handle_object *handle = intern->handle; + + php_socket_t fd = php_poll_handle_get_fd(handle); if (fd != SOCK_ERR) { - php_poll_remove(intern->poll_ctx, (int) fd); + php_poll_remove(intern->context->ctx, (int) fd); + } + + /* Remove from context->watchers: drops context's ref to this watcher; + getThis() still holds a ref so it won't be freed here. */ + if (context && context->watchers) { + zend_ulong handle_key = php_io_poll_compute_ptr_key(handle); + zend_hash_index_del(context->watchers, handle_key); + } + + /* Remove from handle->watching */ + if (handle && handle->watching && context) { + zend_ulong ctx_key = php_io_poll_compute_ptr_key(context); + zend_hash_index_del(handle->watching, ctx_key); } intern->active = false; - intern->poll_ctx = NULL; + intern->context = NULL; } PHP_METHOD(Io_Poll_Context, __construct) @@ -740,7 +898,7 @@ PHP_METHOD(Io_Poll_Context, add) watcher->watched_events = events; watcher->triggered_events = 0; watcher->active = true; - watcher->poll_ctx = intern->ctx; + watcher->context = intern; GC_ADDREF(&handle->std); @@ -763,13 +921,23 @@ PHP_METHOD(Io_Poll_Context, add) RETURN_THROWS(); } - /* Store in our watchers map using shifted pointer as key */ + /* Store in our watchers map using shifted handle pointer as key */ zval watcher_zv; ZVAL_OBJ(&watcher_zv, &watcher->std); GC_ADDREF(&watcher->std); zend_ulong hash_key = php_io_poll_compute_ptr_key(handle); zend_hash_index_add(intern->watchers, hash_key, &watcher_zv); + + /* Register in handle->watching for reverse lookup */ + if (!handle->watching) { + handle->watching = emalloc(sizeof(HashTable)); + zend_hash_init(handle->watching, 4, NULL, NULL, 0); + } + zval ctx_ptr_zv; + ZVAL_PTR(&ctx_ptr_zv, watcher); + zend_ulong ctx_key = php_io_poll_compute_ptr_key(intern); + zend_hash_index_update(handle->watching, ctx_key, &ctx_ptr_zv); } PHP_METHOD(Io_Poll_Context, wait) @@ -858,6 +1026,80 @@ PHP_METHOD(Io_Poll_Context, getBackend) RETURN_OBJ_COPY(zend_enum_get_case_cstr(php_io_poll_backend_class_entry, backend_name)); } +PHP_METHOD(Io_Poll_Context, onWatcherRemoved) +{ + zend_fcall_info fci = empty_fcall_info; + zend_fcall_info_cache fcc = empty_fcall_info_cache; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_FUNC_OR_NULL(fci, fcc) + ZEND_PARSE_PARAMETERS_END(); + + php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis()); + + if (ZEND_FCC_INITIALIZED(intern->on_watcher_removed_fcc)) { + zend_fcc_dtor(&intern->on_watcher_removed_fcc); + } + + if (ZEND_FCI_INITIALIZED(fci)) { + intern->on_watcher_removed_fcc = fcc; + zend_fcc_addref(&intern->on_watcher_removed_fcc); + } +} + +PHPAPI void php_io_poll_handle_remove_from_all_contexts(zend_object *handle_obj) +{ + php_poll_handle_object *handle = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(handle_obj); + if (!handle->watching) { + return; + } + + HashTable *watching = handle->watching; + handle->watching = NULL; + + ZEND_HASH_FOREACH_VAL(watching, zval *zv) { + php_io_poll_watcher_object *watcher = (php_io_poll_watcher_object *) Z_PTR_P(zv); + if (!watcher->active) { + continue; + } + + /* Hold a temp ref to survive context->watchers removal */ + GC_ADDREF(&watcher->std); + + php_io_poll_context_object *context = watcher->context; + + /* Remove from backend */ + if (watcher->context) { + php_socket_t fd = php_poll_handle_get_fd(handle); + if (fd != SOCK_ERR) { + php_poll_remove(watcher->context->ctx, (int) fd); + } + } + + /* Remove from context->watchers (drops context's ref) */ + if (context && context->watchers) { + zend_ulong handle_key = php_io_poll_compute_ptr_key(handle); + zend_hash_index_del(context->watchers, handle_key); + } + + watcher->active = false; + watcher->context = NULL; + + /* Notify context */ + if (context && ZEND_FCC_INITIALIZED(context->on_watcher_removed_fcc)) { + zval watcher_zv; + ZVAL_OBJ(&watcher_zv, &watcher->std); + zend_call_known_fcc(&context->on_watcher_removed_fcc, NULL, 1, &watcher_zv, NULL); + } + + OBJ_RELEASE(&watcher->std); + } ZEND_HASH_FOREACH_END(); + + zend_hash_destroy(watching); + efree(watching); +} + /* Initialize the stream poll classes - add to PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(poll) { @@ -882,6 +1124,12 @@ PHP_MINIT_FUNCTION(poll) php_io_poll_handle_object_handlers.clone_obj = NULL; php_stream_poll_handle_class_entry->default_object_handlers = &php_io_poll_handle_object_handlers; + /* Register StreamPollWeakHandle class */ + php_stream_poll_weak_handle_class_entry + = register_class_StreamPollWeakHandle(php_io_poll_handle_class_entry); + php_stream_poll_weak_handle_class_entry->create_object = php_stream_poll_weak_handle_create_object; + php_stream_poll_weak_handle_class_entry->default_object_handlers = &php_io_poll_handle_object_handlers; + /* Register Watcher class */ php_io_poll_watcher_class_entry = register_class_Io_Poll_Watcher(); php_io_poll_watcher_class_entry->create_object = php_io_poll_watcher_create_object; diff --git a/ext/standard/io_poll.h b/ext/standard/io_poll.h index 1d9e19d918ec..d9e2c0c0371f 100644 --- a/ext/standard/io_poll.h +++ b/ext/standard/io_poll.h @@ -25,5 +25,8 @@ PHPAPI extern zend_class_entry *php_stream_poll_handle_class_entry; PHPAPI zend_result php_io_poll_events_to_event_enums(uint32_t events, zval *event_enums); PHPAPI void php_stream_poll_handle_from_stream(zval *dest, php_stream *stream); +PHPAPI void php_stream_poll_weak_handle_from_stream(zval *dest, php_stream *stream); +PHPAPI void php_stream_poll_weak_handle_notify(zend_object *handle_obj); +PHPAPI void php_io_poll_handle_remove_from_all_contexts(zend_object *handle_obj); #endif /* PHP_IO_POLL_H */ diff --git a/ext/standard/io_poll.stub.php b/ext/standard/io_poll.stub.php index 83c1ba5cbe2e..d9c4217bc1bc 100644 --- a/ext/standard/io_poll.stub.php +++ b/ext/standard/io_poll.stub.php @@ -89,6 +89,8 @@ public function add(Handle $handle, array $events, mixed $data = null): Watcher public function wait(?int $timeoutSeconds = null, int $timeoutMicroseconds = 0, ?int $maxEvents = null): array {} public function getBackend(): Backend {} + + public function onWatcherRemoved(?callable $callback = null): void {} } class PollException extends \Io\IoException {} @@ -150,6 +152,21 @@ class InvalidHandleException extends PollException {} } namespace { + /** + * @strict-properties + * @not-serializable + */ + final class StreamPollWeakHandle implements Io\Poll\Handle + { + private function __construct() {} + + /** @param resource $stream */ + public static function create($stream): static {} + + /** @return resource|null */ + public function getStream(): mixed {} + } + /** * @strict-properties * @not-serializable diff --git a/ext/standard/io_poll_arginfo.h b/ext/standard/io_poll_arginfo.h index 5fc62f629564..e2a8e3d08f0d 100644 --- a/ext/standard/io_poll_arginfo.h +++ b/ext/standard/io_poll_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit io_poll.stub.php instead. - * Stub hash: a7450146c5b3b3f3486611c83a55cf0cc932b27a + * Stub hash: fff911001761ee523b904e729c44b7b0ed96c3ff * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Poll_Backend_getAvailableBackends, 0, 0, IS_ARRAY, 0) @@ -64,6 +64,18 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Poll_Context_getBackend, 0, 0, Io\\Poll\\Backend, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Poll_Context_onWatcherRemoved, 0, 0, IS_VOID, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, callback, IS_CALLABLE, 1, "null") +ZEND_END_ARG_INFO() + +#define arginfo_class_StreamPollWeakHandle___construct arginfo_class_Io_Poll_Watcher___construct + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_StreamPollWeakHandle_create, 0, 1, IS_STATIC, 0) + ZEND_ARG_INFO(0, stream) +ZEND_END_ARG_INFO() + +#define arginfo_class_StreamPollWeakHandle_getStream arginfo_class_Io_Poll_Watcher_getData + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_StreamPollHandle___construct, 0, 0, 1) ZEND_ARG_INFO(0, stream) ZEND_END_ARG_INFO() @@ -90,6 +102,10 @@ ZEND_METHOD(Io_Poll_Context, __construct); ZEND_METHOD(Io_Poll_Context, add); ZEND_METHOD(Io_Poll_Context, wait); ZEND_METHOD(Io_Poll_Context, getBackend); +ZEND_METHOD(Io_Poll_Context, onWatcherRemoved); +ZEND_METHOD(StreamPollWeakHandle, __construct); +ZEND_METHOD(StreamPollWeakHandle, create); +ZEND_METHOD(StreamPollWeakHandle, getStream); ZEND_METHOD(StreamPollHandle, __construct); ZEND_METHOD(StreamPollHandle, getStream); ZEND_METHOD(StreamPollHandle, isValid); @@ -121,6 +137,14 @@ static const zend_function_entry class_Io_Poll_Context_methods[] = { ZEND_ME(Io_Poll_Context, add, arginfo_class_Io_Poll_Context_add, ZEND_ACC_PUBLIC) ZEND_ME(Io_Poll_Context, wait, arginfo_class_Io_Poll_Context_wait, ZEND_ACC_PUBLIC) ZEND_ME(Io_Poll_Context, getBackend, arginfo_class_Io_Poll_Context_getBackend, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_Context, onWatcherRemoved, arginfo_class_Io_Poll_Context_onWatcherRemoved, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_StreamPollWeakHandle_methods[] = { + ZEND_ME(StreamPollWeakHandle, __construct, arginfo_class_StreamPollWeakHandle___construct, ZEND_ACC_PRIVATE) + ZEND_ME(StreamPollWeakHandle, create, arginfo_class_StreamPollWeakHandle_create, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(StreamPollWeakHandle, getStream, arginfo_class_StreamPollWeakHandle_getStream, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -383,6 +407,17 @@ static zend_class_entry *register_class_Io_Poll_InvalidHandleException(zend_clas return class_entry; } +static zend_class_entry *register_class_StreamPollWeakHandle(zend_class_entry *class_entry_Io_Poll_Handle) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "StreamPollWeakHandle", class_StreamPollWeakHandle_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Io_Poll_Handle); + + return class_entry; +} + static zend_class_entry *register_class_StreamPollHandle(zend_class_entry *class_entry_Io_Poll_Handle) { zend_class_entry ce, *class_entry; diff --git a/ext/standard/io_poll_decl.h b/ext/standard/io_poll_decl.h index 2b09e3665f58..c59fadf4ebed 100644 --- a/ext/standard/io_poll_decl.h +++ b/ext/standard/io_poll_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit io_poll.stub.php instead. - * Stub hash: a7450146c5b3b3f3486611c83a55cf0cc932b27a */ + * Stub hash: fff911001761ee523b904e729c44b7b0ed96c3ff */ -#ifndef ZEND_IO_POLL_DECL_a7450146c5b3b3f3486611c83a55cf0cc932b27a_H -#define ZEND_IO_POLL_DECL_a7450146c5b3b3f3486611c83a55cf0cc932b27a_H +#ifndef ZEND_IO_POLL_DECL_fff911001761ee523b904e729c44b7b0ed96c3ff_H +#define ZEND_IO_POLL_DECL_fff911001761ee523b904e729c44b7b0ed96c3ff_H typedef enum zend_enum_Io_Poll_Backend { ZEND_ENUM_Io_Poll_Backend_Auto = 1, @@ -23,4 +23,4 @@ typedef enum zend_enum_Io_Poll_Event { ZEND_ENUM_Io_Poll_Event_EdgeTriggered = 7, } zend_enum_Io_Poll_Event; -#endif /* ZEND_IO_POLL_DECL_a7450146c5b3b3f3486611c83a55cf0cc932b27a_H */ +#endif /* ZEND_IO_POLL_DECL_fff911001761ee523b904e729c44b7b0ed96c3ff_H */ diff --git a/ext/standard/tests/poll/poll_weakhandle_context_destroyed_first.phpt b/ext/standard/tests/poll/poll_weakhandle_context_destroyed_first.phpt new file mode 100644 index 000000000000..11477236170c --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_context_destroyed_first.phpt @@ -0,0 +1,24 @@ +--TEST-- +Io\Poll: Context destroyed before stream: watcher deactivated +--FILE-- +add($handle, [Io\Poll\Event::Read]); + +unset($ctx); +gc_collect_cycles(); + +var_dump($watcher->isActive()); + +fclose($r); +fclose($w); + +echo "done\n"; +?> +--EXPECT-- +bool(false) +done diff --git a/ext/standard/tests/poll/poll_weakhandle_getstream_null.phpt b/ext/standard/tests/poll/poll_weakhandle_getstream_null.phpt new file mode 100644 index 000000000000..137ac1d623e9 --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_getstream_null.phpt @@ -0,0 +1,21 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle::getStream() returns null after stream is freed +--FILE-- +getStream())); + +fclose($r); +fclose($w); + +var_dump($handle->getStream()); +echo "done\n"; +?> +--EXPECT-- +bool(true) +NULL +done diff --git a/ext/standard/tests/poll/poll_weakhandle_manual_remove.phpt b/ext/standard/tests/poll/poll_weakhandle_manual_remove.phpt new file mode 100644 index 000000000000..3000ab59b91b --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_manual_remove.phpt @@ -0,0 +1,34 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle watcher manual remove from single Context +--FILE-- +add($handle, [Io\Poll\Event::Read], 'my-data'); + +var_dump($watcher->isActive()); +var_dump($watcher->getData()); + +$watcher->remove(); + +var_dump($watcher->isActive()); + +try { + $watcher->remove(); +} catch (Io\Poll\InactiveWatcherException $e) { + echo $e->getMessage(), "\n"; +} + +fclose($r); +fclose($w); +echo "done\n"; +?> +--EXPECT-- +bool(true) +string(7) "my-data" +bool(false) +Cannot remove inactive watcher +done diff --git a/ext/standard/tests/poll/poll_weakhandle_multiple_contexts.phpt b/ext/standard/tests/poll/poll_weakhandle_multiple_contexts.phpt new file mode 100644 index 000000000000..59f1207165ee --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_multiple_contexts.phpt @@ -0,0 +1,36 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle auto-removed from multiple Contexts when stream is collected +--FILE-- +add($handle, [Io\Poll\Event::Read], 'ctx1'); +$watcher2 = $ctx2->add($handle, [Io\Poll\Event::Read], 'ctx2'); + +var_dump($watcher1->isActive()); +var_dump($watcher2->isActive()); + +$weakWatcher1 = WeakReference::create($watcher1); +$weakWatcher2 = WeakReference::create($watcher2); +unset($handle, $watcher1, $watcher2); + +fclose($r); +fclose($w); +gc_collect_cycles(); + +// Both watchers removed from their contexts and freed. +var_dump($weakWatcher1->get() === null); +var_dump($weakWatcher2->get() === null); +echo "done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +done diff --git a/ext/standard/tests/poll/poll_weakhandle_on_watcher_removed.phpt b/ext/standard/tests/poll/poll_weakhandle_on_watcher_removed.phpt new file mode 100644 index 000000000000..b146ee240414 --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_on_watcher_removed.phpt @@ -0,0 +1,34 @@ +--TEST-- +Io\Poll: onWatcherRemoved fires on stream GC but not on manual Watcher::remove() +--FILE-- +onWatcherRemoved(function (Io\Poll\Watcher $w) { + echo "removed: " . $w->getData() . "\n"; +}); + +$handle = StreamPollWeakHandle::create($r); +$watcher = $ctx->add($handle, [Io\Poll\Event::Read], 'manual'); +$watcher->remove(); // no callback expected +echo "after manual remove\n"; +unset($watcher, $handle); + +// --- Part 2: stream GC fires the callback --- +$handle = StreamPollWeakHandle::create($r); +$ctx->add($handle, [Io\Poll\Event::Read], 'stream-gc'); +unset($handle); + +fclose($r); // notifies context -> onWatcherRemoved +fclose($w); + +echo "done\n"; +?> +--EXPECT-- +after manual remove +removed: stream-gc +done diff --git a/ext/standard/tests/poll/poll_weakhandle_readd_after_remove.phpt b/ext/standard/tests/poll/poll_weakhandle_readd_after_remove.phpt new file mode 100644 index 000000000000..289e80efdc88 --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_readd_after_remove.phpt @@ -0,0 +1,28 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle can be re-added to a Context after manual Watcher::remove() +--FILE-- +add($handle, [Io\Poll\Event::Read], 'first'); +$watcher1->remove(); +var_dump($watcher1->isActive()); + +$watcher2 = $ctx->add($handle, [Io\Poll\Event::Read], 'second'); +var_dump($watcher2->isActive()); +var_dump($watcher2->getData()); + +$watcher2->remove(); +fclose($r); +fclose($w); +echo "done\n"; +?> +--EXPECT-- +bool(false) +bool(true) +string(6) "second" +done diff --git a/ext/standard/tests/poll/poll_weakhandle_stream_gc.phpt b/ext/standard/tests/poll/poll_weakhandle_stream_gc.phpt new file mode 100644 index 000000000000..fbe72234485a --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_stream_gc.phpt @@ -0,0 +1,39 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle watcher auto-removed and GC'd when stream is collected +--FILE-- +add($handle, [Io\Poll\Event::Read]); + +$weakHandle = WeakReference::create($handle); +$weakWatcher = WeakReference::create($watcher); + +// Refs to handle and watcher; context still holds a ref to +// watcher (and watcher holds a ref to handle), but only until the stream dies. +unset($handle, $watcher); + +// Stream is still alive: watcher should still be in the context. +var_dump($weakHandle->get() !== null); // handle alive (held by watcher in context) +var_dump($weakWatcher->get() !== null); // watcher alive (held by context) + +// Freeing the stream notifies handle and context +fclose($r); +fclose($w); + +// Context no longer holds the watcher ref; watcher held no external refs either. +gc_collect_cycles(); + +var_dump($weakHandle->get() === null); // handle freed +var_dump($weakWatcher->get() === null); // watcher freed +echo "done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +done diff --git a/ext/standard/tests/streams/hooks/use-case.phpt b/ext/standard/tests/streams/hooks/use-case.phpt index fabcc1a330b6..28591815f7e1 100644 --- a/ext/standard/tests/streams/hooks/use-case.phpt +++ b/ext/standard/tests/streams/hooks/use-case.phpt @@ -42,7 +42,6 @@ $scheduler->run(function () { fwrite($client, "HTTP/1.0 200 OK\r\n"); fwrite($client, "\r\n"); fwrite($client, "Hello world!\n"); - fclose($client); }); }); diff --git a/main/php_poll.h b/main/php_poll.h index 9b878d44bd20..88dcc88d23ed 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -132,21 +132,18 @@ typedef struct php_poll_handle_object php_poll_handle_object; struct php_poll_handle_ops { /** * Get file descriptor for this handle - * @param handle The handle object * @return File descriptor or SOCK_ERR if invalid/not applicable */ php_socket_t (*get_fd)(php_poll_handle_object *handle); /** * Check if handle is still valid - * @param handle The handle object * @return true if valid, false if invalid */ int (*is_valid)(php_poll_handle_object *handle); /** * Cleanup handle-specific data - * @param handle The handle object */ void (*cleanup)(php_poll_handle_object *handle); }; @@ -155,6 +152,7 @@ struct php_poll_handle_ops { struct php_poll_handle_object { php_poll_handle_ops *ops; void *handle_data; + HashTable *watching; /* context_ptr_key -> php_io_poll_watcher_object* (IS_PTR, no refcount) */ zend_object std; }; diff --git a/main/php_streams.h b/main/php_streams.h index 4eac49e27399..9cf3a2a6ddf0 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -252,6 +252,11 @@ struct _php_stream { struct _php_stream *enclosing_stream; /* this is a private stream owned by enclosing_stream */ + /* StreamPollWeakHandle singleton for this stream. Not refcounted; zeroed when the + * WeakHandle is freed. streams.c calls weak_ops->notify() through this pointer + * when the stream is freed (see php_poll_weak_handle_ops in php_poll.h). */ + zend_object *weak_poll_handle; + zend_llist *error_list; }; /* php_stream */ diff --git a/main/poll/poll_handle.c b/main/poll/poll_handle.c index 0c0628ac49dc..cd6070ef4df0 100644 --- a/main/poll/poll_handle.c +++ b/main/poll/poll_handle.c @@ -70,6 +70,7 @@ PHPAPI php_poll_handle_object *php_poll_handle_object_create( intern->ops = ops ? ops : &php_poll_handle_default_ops; intern->handle_data = NULL; + intern->watching = NULL; return intern; } @@ -83,6 +84,11 @@ PHPAPI void php_poll_handle_object_free(zend_object *obj) intern->ops->cleanup(intern); } + if (intern->watching) { + zend_hash_destroy(intern->watching); + efree(intern->watching); + } + zend_object_std_dtor(&intern->std); } diff --git a/main/streams/streams.c b/main/streams/streams.c index 9e223048f343..449f3ebc134a 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -27,6 +27,7 @@ #include "ext/standard/basic_functions.h" /* for BG(CurrentStatFile) */ #include "ext/standard/php_string.h" /* for php_memnstr, used by php_stream_get_record() */ #include "ext/uri/php_uri.h" +#include "ext/standard/io_poll.h" #include #include #include "php_streams_int.h" @@ -341,6 +342,12 @@ fprintf(stderr, "stream_free: %s:%p[%s] preserve_handle=%d release_cast=%d remov return fclose(stream->stdiocast); } + if (stream->weak_poll_handle) { + zend_object *handle_obj = stream->weak_poll_handle; + stream->weak_poll_handle = NULL; + php_stream_poll_weak_handle_notify(handle_obj); + } + ret = stream->ops->close(stream, preserve_handle ? 0 : 1); stream->abstract = NULL; From c3500600e4f3687707dc94804973c09805807e08 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 30 Jun 2026 10:11:51 +0200 Subject: [PATCH 12/18] Use zend_enum_get_case_by_id --- ext/curl/interface.c | 26 ++++++++++++++++++++------ ext/standard/io_hooks.c | 9 +++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/ext/curl/interface.c b/ext/curl/interface.c index c1f6f99bb12b..f82b6de03cfb 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -43,6 +43,7 @@ #include "ext/standard/file.h" #include "ext/standard/url.h" #include "ext/standard/io_poll.h" +#include "ext/standard/io_poll_decl.h" #include "main/php_poll.h" #include "curl_private.h" #include "curl_socket_handle_arginfo.h" @@ -2465,12 +2466,12 @@ static void php_curl_poll_events_to_zval(int what, zval *dest) array_init(dest); if (what & CURL_POLL_IN) { zval zv; - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Read")); + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Read)); zend_hash_next_index_insert(Z_ARRVAL_P(dest), &zv); } if (what & CURL_POLL_OUT) { zval zv; - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Write")); + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Write)); zend_hash_next_index_insert(Z_ARRVAL_P(dest), &zv); } } @@ -2482,10 +2483,23 @@ static int php_curl_poll_result_to_curl_events(zval *events_arr) zval *event; ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(events_arr), event) { if (Z_TYPE_P(event) != IS_OBJECT) continue; - zend_string *name = Z_STR_P(zend_enum_fetch_case_name(Z_OBJ_P(event))); - if (zend_string_equals_literal(name, "Read")) curl_events |= CURL_CSELECT_IN; - if (zend_string_equals_literal(name, "Write")) curl_events |= CURL_CSELECT_OUT; - if (zend_string_equals_literal(name, "Error")) curl_events |= CURL_CSELECT_ERR; + zend_enum_Io_Poll_Event ev = (zend_enum_Io_Poll_Event) zend_enum_fetch_case_id(Z_OBJ_P(event)); + switch (ev) { + case ZEND_ENUM_Io_Poll_Event_Read: + curl_events |= CURL_CSELECT_IN; + break; + case ZEND_ENUM_Io_Poll_Event_Write: + curl_events |= CURL_CSELECT_OUT; + break; + case ZEND_ENUM_Io_Poll_Event_Error: + curl_events |= CURL_CSELECT_ERR; + break; + case ZEND_ENUM_Io_Poll_Event_HangUp: + case ZEND_ENUM_Io_Poll_Event_ReadHangUp: + case ZEND_ENUM_Io_Poll_Event_OneShot: + case ZEND_ENUM_Io_Poll_Event_EdgeTriggered: + break; + } } ZEND_HASH_FOREACH_END(); return curl_events; } diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c index d85ffb1b08a5..13a7277ac61d 100644 --- a/ext/standard/io_hooks.c +++ b/ext/standard/io_hooks.c @@ -16,6 +16,7 @@ #include "zend_enum.h" #include "ext/standard/file.h" #include "ext/standard/io_poll.h" +#include "io_poll_decl.h" #include "ext/standard/io_hooks.h" #include "io_hooks_arginfo.h" @@ -34,19 +35,19 @@ static void php_pollfd_events_to_io_poll_events(zend_array *dest, int events) zval zv; if (events & POLLIN) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Read")); + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Read)); zend_hash_next_index_insert(dest, &zv); } if (events & POLLOUT) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Write")); + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Write)); zend_hash_next_index_insert(dest, &zv); } if (events & POLLERR) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "Error")); + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Error)); zend_hash_next_index_insert(dest, &zv); } if (events & POLLHUP) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_cstr(php_io_poll_event_class_entry, "HangUp")); + ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_HangUp)); zend_hash_next_index_insert(dest, &zv); } } From 731b9b35c59ade341517ae147dfbc750cb24647b Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 30 Jun 2026 11:31:31 +0200 Subject: [PATCH 13/18] Switch to non-blocking operations interally * Mark OS sockets as non-blocking when creating a stream (without affecting the stream's blocking status) * Perform I/O optimistically before polling * Poll on EAGAIN only This should be faster (less polling), and makes operations compatible with edge-triggering. --- .../tests/streams/hooks/hook-close-fgets.phpt | 5 - .../streams/hooks/hook-concurrent-access.phpt | 7 +- main/network.c | 66 ++++++---- main/streams/xp_socket.c | 121 +++++++----------- 4 files changed, 89 insertions(+), 110 deletions(-) diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt index d70ee40d9360..47b24ca908c4 100644 --- a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -7,12 +7,7 @@ use Io\Hooks\{Hooks, PollInfo, PollResult}; $server = stream_socket_server('tcp://127.0.0.1:0'); $addr = stream_socket_get_name($server, false); - $client = stream_socket_client("tcp://$addr"); -$conn = stream_socket_accept($server); -fwrite($conn, " --EXPECT-- Concurrent access to a stream -string(6) "hello -" +bool(false) diff --git a/main/network.c b/main/network.c index c6f647032ccf..cb4678133223 100644 --- a/main/network.c +++ b/main/network.c @@ -820,42 +820,53 @@ PHPAPI php_socket_t php_network_accept_incoming_ex(php_stream *stream, { php_socket_t clisock = -1; int error = 0; - php_pollstream_result result; php_sockaddr_storage sa; socklen_t sl; - result = php_pollstream_for(stream, srvsock, PHP_POLLREADABLE, timeout); + sl = sizeof(sa); + clisock = accept(srvsock, (struct sockaddr*)&sa, &sl); - if (result == PHP_POLLSTREAM_TIMEOUT) { - error = PHP_TIMEOUT_ERROR_VALUE; - } else if (result == PHP_POLLSTREAM_ERROR) { - error = php_socket_errno(); // TODO - } else { - sl = sizeof(sa); - - clisock = accept(srvsock, (struct sockaddr*)&sa, &sl); + if (clisock == SOCK_ERR) { + error = php_socket_errno(); + if (PHP_IS_TRANSIENT_ERROR(error)) { + php_pollstream_result result; + do { + result = php_pollstream_for(stream, srvsock, PHP_POLLREADABLE, timeout); + if (result == PHP_POLLSTREAM_TIMEOUT) { + error = PHP_TIMEOUT_ERROR_VALUE; + break; + } + if (result == PHP_POLLSTREAM_READY) { + sl = sizeof(sa); + clisock = accept(srvsock, (struct sockaddr*)&sa, &sl); + if (clisock == SOCK_ERR) { + error = php_socket_errno(); + } + break; + } + error = php_socket_errno(); + } while (error == EINTR); + } + } - if (clisock != SOCK_ERR) { - php_network_populate_name_from_sockaddr((struct sockaddr*)&sa, sl, - textaddr, - addr, addrlen - ); + if (clisock != SOCK_ERR) { + php_network_populate_name_from_sockaddr((struct sockaddr*)&sa, sl, + textaddr, + addr, addrlen + ); #ifdef TCP_NODELAY - if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_NODELAY)) { - int tcp_nodelay = 1; - setsockopt(clisock, IPPROTO_TCP, TCP_NODELAY, (char*)&tcp_nodelay, sizeof(tcp_nodelay)); - } + if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_NODELAY)) { + int tcp_nodelay = 1; + setsockopt(clisock, IPPROTO_TCP, TCP_NODELAY, (char*)&tcp_nodelay, sizeof(tcp_nodelay)); + } #endif #ifdef TCP_KEEPALIVE - /* MacOS does not inherit TCP_KEEPALIVE so it needs to be set */ - if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_KEEPIDLE)) { - setsockopt(clisock, IPPROTO_TCP, TCP_KEEPALIVE, - (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); - } -#endif - } else { - error = php_socket_errno(); + /* MacOS does not inherit TCP_KEEPALIVE so it needs to be set */ + if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_KEEPIDLE)) { + setsockopt(clisock, IPPROTO_TCP, TCP_KEEPALIVE, + (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); } +#endif } if (error_code) { @@ -1290,6 +1301,7 @@ PHPAPI php_stream *_php_stream_sock_open_from_socket(php_socket_t socket, const sock->timeout.tv_sec = FG(default_socket_timeout); sock->timeout.tv_usec = 0; sock->socket = socket; + php_set_sock_blocking(socket, false); stream = php_stream_alloc_rel(&php_stream_generic_socket_ops, sock, persistent_id, "r+"); diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index ec8ecdd1ab71..73f207bb3fb7 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -78,7 +78,7 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun ptimeout = &sock->timeout; retry: - didwrite = send(sock->socket, buf, XP_SOCK_BUF_SIZE(count), (sock->is_blocked && ptimeout) ? MSG_DONTWAIT : 0); + didwrite = send(sock->socket, buf, XP_SOCK_BUF_SIZE(count), 0); if (didwrite <= 0) { char *estr; @@ -86,23 +86,20 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun if (PHP_IS_TRANSIENT_ERROR(err)) { if (sock->is_blocked) { - int retval; - sock->timeout_event = false; do { - retval = php_pollstream_for(stream, sock->socket, POLLOUT, ptimeout); + int retval = php_pollstream_for(stream, sock->socket, POLLOUT, ptimeout); + + if (retval == PHP_POLLSTREAM_READY) { + goto retry; + } if (retval == PHP_POLLSTREAM_TIMEOUT) { sock->timeout_event = true; break; } - if (retval == PHP_POLLSTREAM_READY) { - /* writable now; retry */ - goto retry; - } - err = php_socket_errno(); } while (err == EINTR); } else { @@ -127,77 +124,56 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun return didwrite; } -static void php_sock_stream_wait_for_data(php_stream *stream, php_netstream_data_t *sock, bool has_buffered_data) +static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) { - int retval; - struct timeval *ptimeout, zero_timeout; + php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; if (!sock || sock->socket == -1) { - return; + return -1; } - sock->timeout_event = false; - - if (has_buffered_data) { - /* If there is already buffered data, use no timeout. */ - zero_timeout.tv_sec = 0; - zero_timeout.tv_usec = 0; - ptimeout = &zero_timeout; - } else if (sock->timeout.tv_sec == -1) { - ptimeout = NULL; - } else { - ptimeout = &sock->timeout; - } + ssize_t nr_bytes = recv(sock->socket, buf, XP_SOCK_BUF_SIZE(count), 0); + int err = php_socket_errno(); - while(1) { - retval = php_pollstream_for(stream, sock->socket, PHP_POLLREADABLE, ptimeout); + sock->timeout_event = false; - if (retval == PHP_POLLSTREAM_TIMEOUT) - sock->timeout_event = true; + if (nr_bytes < 0 && PHP_IS_TRANSIENT_ERROR(err) && sock->is_blocked) { + bool has_buffered_data = stream->has_buffered_data; - if (retval == PHP_POLLSTREAM_READY) - break; + struct timeval zero_timeout = {0, 0}; + struct timeval *ptimeout; + if (has_buffered_data) { + ptimeout = &zero_timeout; + } else if (sock->timeout.tv_sec == -1) { + ptimeout = NULL; + } else { + ptimeout = &sock->timeout; + } - if (php_socket_errno() != EINTR) - break; - } -} + int retval; + do { + retval = php_pollstream_for(stream, sock->socket, PHP_POLLREADABLE, ptimeout); -static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) -{ - php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; + if (retval == PHP_POLLSTREAM_TIMEOUT) { + sock->timeout_event = true; + break; + } - if (!sock || sock->socket == -1) { - return -1; - } + if (retval == PHP_POLLSTREAM_READY) { + break; + } + } while (php_socket_errno() == EINTR); - int recv_flags = 0; - /* Special handling for blocking read. */ - if (sock->is_blocked) { - /* Find out if there is any data buffered from the previous read. */ - bool has_buffered_data = stream->has_buffered_data; - /* No need to wait if there is any data buffered or no timeout. */ - bool dont_wait = has_buffered_data || - (sock->timeout.tv_sec == 0 && sock->timeout.tv_usec == 0); - /* Set MSG_DONTWAIT if no wait is needed or there is unlimited timeout which was - * added by fix for #41984 committed in 9343c5404. */ - if (dont_wait || sock->timeout.tv_sec != -1) { - recv_flags = MSG_DONTWAIT; + if (sock->timeout_event) { + return has_buffered_data ? 0 : -1; } - /* If the wait is needed or it is a platform without MSG_DONTWAIT support (e.g. Windows), - * then poll for data. */ - if (!dont_wait || MSG_DONTWAIT == 0) { - php_sock_stream_wait_for_data(stream, sock, has_buffered_data); - if (sock->timeout_event) { - /* It is ok to timeout if there is any data buffered so return 0, otherwise -1. */ - return has_buffered_data ? 0 : -1; - } + + if (retval == PHP_POLLSTREAM_READY) { + nr_bytes = recv(sock->socket, buf, XP_SOCK_BUF_SIZE(count), 0); + err = php_socket_errno(); } } - ssize_t nr_bytes = recv(sock->socket, buf, XP_SOCK_BUF_SIZE(count), recv_flags); - int err = php_socket_errno(); - if (nr_bytes < 0) { if (PHP_IS_TRANSIENT_ERROR(err)) { nr_bytes = 0; @@ -398,11 +374,8 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void case PHP_STREAM_OPTION_BLOCKING: oldmode = sock->is_blocked; - if (SUCCESS == php_set_sock_blocking(sock->socket, value)) { - sock->is_blocked = value; - return oldmode; - } - return PHP_STREAM_OPTION_RETURN_ERR; + sock->is_blocked = value; + return oldmode; case PHP_STREAM_OPTION_READ_TIMEOUT: sock->timeout = *(struct timeval*)ptrparam; @@ -992,10 +965,8 @@ static inline int php_tcp_sockop_accept(php_stream *stream, php_netstream_data_t memcpy(clisockdata, sock, sizeof(*clisockdata)); clisockdata->socket = clisock; -#ifdef __linux__ - /* O_NONBLOCK is not inherited on Linux */ clisockdata->is_blocked = true; -#endif + php_set_sock_blocking(clisock, false); xparam->outputs.client = php_stream_alloc_rel(stream->ops, clisockdata, NULL, "r+"); if (xparam->outputs.client) { @@ -1022,10 +993,16 @@ static int php_tcp_sockop_set_option(php_stream *stream, int option, int value, case STREAM_XPORT_OP_CONNECT: case STREAM_XPORT_OP_CONNECT_ASYNC: xparam->outputs.returncode = php_tcp_sockop_connect(stream, sock, xparam); + if (xparam->outputs.returncode == 0 && sock->socket != SOCK_ERR) { + php_set_sock_blocking(sock->socket, false); + } return PHP_STREAM_OPTION_RETURN_OK; case STREAM_XPORT_OP_BIND: xparam->outputs.returncode = php_tcp_sockop_bind(stream, sock, xparam); + if (xparam->outputs.returncode == 0 && sock->socket != SOCK_ERR) { + php_set_sock_blocking(sock->socket, false); + } return PHP_STREAM_OPTION_RETURN_OK; From eb2929aab725224ae3d674826e6d5d27241fca36 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 30 Jun 2026 12:17:55 +0200 Subject: [PATCH 14/18] Update test scheduler to use EdgeTriggered mode --- .../tests/streams/hooks/scheduler.inc | 198 ++++++++++++++---- 1 file changed, 152 insertions(+), 46 deletions(-) diff --git a/ext/standard/tests/streams/hooks/scheduler.inc b/ext/standard/tests/streams/hooks/scheduler.inc index b4037a13022f..4f06eaeac78d 100644 --- a/ext/standard/tests/streams/hooks/scheduler.inc +++ b/ext/standard/tests/streams/hooks/scheduler.inc @@ -1,27 +1,43 @@ spl_object_id(Handle) (for poll() handles only) - private array $fiberWatchers = []; // fiberId -> [[Watcher, fdKey|null], ...] - private array $fiberDeadlines = []; // fiberId -> [deadline_ns, fiber, PollInfo|null] - private array $fiberIsMulti = []; // fiberId -> true (for poll_multi fibers) + private array $watcherByHandle = []; // handleId -> Watcher (persistent ET) + private array $fiberByHandleEvent = []; // handleId -> [eventName -> fiber] + private array $pendingByHandle = []; // handleId -> [Event, ...] (fired, not yet consumed) + private array $registrationsByFiber = []; // fiberId -> [handleId -> [eventName, ...]] + private array $fiberDeadlines = []; // fiberId -> [deadline_ns, fiber, PollInfo|null] + private array $fiberIsMulti = []; // fiberId -> true public function __construct() { $this->pollContext = new Context(); + $this->pollContext->onWatcherRemoved($this->onWatcherRemoved(...)); + } + + private function onWatcherRemoved(Watcher $watcher): void + { + $handle = $watcher->getHandle(); + $handleId = spl_object_id($handle); + + // Handles can not be closed during polling, therefore no fiber is waiting for it + assert(($this->fiberByHandleEvent[$handleId] ?? []) === []); + + unset($this->watcherByHandle[$handleId]); + unset($this->fiberByHandleEvent[$handleId]); + unset($this->pendingByHandle[$handleId]); } public function run(callable $main): void { $this->ready[] = [new Fiber($main), null]; - while ($this->ready !== [] || $this->fiberWatchers !== [] || $this->fiberDeadlines !== []) { + while ($this->ready !== [] || $this->registrationsByFiber !== [] || $this->fiberDeadlines !== []) { $this->runReadyFibers(); $this->pollFds(); } @@ -37,7 +53,7 @@ class Scheduler implements Hooks public function pollFds(): void { - if ($this->fiberWatchers === [] && $this->fiberDeadlines === []) { + if ($this->registrationsByFiber === [] && $this->fiberDeadlines === []) { return; } @@ -61,34 +77,52 @@ class Scheduler implements Hooks $watchers = $this->pollContext->wait($timeoutSec, $timeoutUsec); - // Group ready PollResults by fiber before resuming anyone - $fiberResults = []; + // Group triggered events by fiber; a fiber gets at most one result handle + // (the first one that fires). Events from a second handle for the same fiber + // go to pending for later consumption. + $fiberResults = []; // fiberId -> [$fiber, PollResult] foreach ($watchers as $watcher) { - [$fiber] = $watcher->getData(); - $fiberId = spl_object_id($fiber); - if (!isset($this->fiberWatchers[$fiberId])) { - continue; + $handle = $watcher->getHandle(); + $handleId = spl_object_id($handle); + + foreach ($watcher->getTriggeredEvents() as $event) { + $fiber = $this->fiberByHandleEvent[$handleId][$event->name] ?? null; + if ($fiber !== null) { + $fiberId = spl_object_id($fiber); + $existing = $fiberResults[$fiberId][1] ?? null; + + if ($existing !== null && spl_object_id($existing->handle) !== $handleId) { + // Fiber already has a result from a different handle: defer to pending + $this->addPendingEvent($handleId, $event); + } else { + if ($existing === null) { + $result = new PollResult(); + $result->handle = $handle; + $result->events = []; + $result->timeout = false; + $fiberResults[$fiberId] = [$fiber, $result]; + } + $fiberResults[$fiberId][1]->events[] = $event; + // Terminal events persist in pending even when delivered to a fiber + if (self::isTerminalEvent($event)) { + $this->addPendingEvent($handleId, $event); + } + } + } else { + $this->addPendingEvent($handleId, $event); + } } - $result = new PollResult(); - $result->handle = $watcher->getHandle(); - $result->events = $watcher->getTriggeredEvents(); - $result->timeout = false; - $fiberResults[$fiberId] ??= [$fiber, []]; - $fiberResults[$fiberId][1][] = $result; } - foreach ($fiberResults as $fiberId => [$fiber, $results]) { - if (!isset($this->fiberWatchers[$fiberId])) { - continue; - } - $this->removeAllFiberWatchers($fiberId); + foreach ($fiberResults as $fiberId => [$fiber, $result]) { + $this->clearFiberRegistrations($fiberId); unset($this->fiberDeadlines[$fiberId]); if ($this->fiberIsMulti[$fiberId] ?? false) { unset($this->fiberIsMulti[$fiberId]); - $this->ready[] = [$fiber, $results]; // array of PollResult + $this->ready[] = [$fiber, [$result]]; } else { - $this->ready[] = [$fiber, $results[0]]; // single PollResult + $this->ready[] = [$fiber, $result]; } } @@ -98,12 +132,12 @@ class Scheduler implements Hooks if ($deadline > $now) { continue; } - $this->removeAllFiberWatchers($fiberId); + $this->clearFiberRegistrations($fiberId); unset($this->fiberDeadlines[$fiberId]); if ($this->fiberIsMulti[$fiberId] ?? false) { unset($this->fiberIsMulti[$fiberId]); - $this->ready[] = [$fiber, []]; // empty array = timeout for poll_multi + $this->ready[] = [$fiber, []]; } else { $result = new PollResult(); $result->handle = $info->handle; @@ -114,35 +148,96 @@ class Scheduler implements Hooks } } - private function removeAllFiberWatchers(int $fiberId): void + private function clearFiberRegistrations(int $fiberId): void + { + foreach ($this->registrationsByFiber[$fiberId] ?? [] as $handleId => $eventNames) { + foreach ($eventNames as $eventName) { + unset($this->fiberByHandleEvent[$handleId][$eventName]); + } + if (empty($this->fiberByHandleEvent[$handleId])) { + unset($this->fiberByHandleEvent[$handleId]); + } + } + unset($this->registrationsByFiber[$fiberId]); + } + + private function addOrModifyWatcher(int $handleId, object $handle, array $events): void + { + $eventsWithET = [...$events, Event::EdgeTriggered]; + + if (isset($this->watcherByHandle[$handleId]) && $this->watcherByHandle[$handleId]->isActive()) { + $watcher = $this->watcherByHandle[$handleId]; + $merged = array_values(array_unique([...$watcher->getWatchedEvents(), ...$eventsWithET], SORT_REGULAR)); + $watcher->modifyEvents($merged); + } else { + $this->watcherByHandle[$handleId] = $this->pollContext->add($handle, $eventsWithET); + } + } + + private static function isTerminalEvent(Event $event): bool { - foreach ($this->fiberWatchers[$fiberId] as [$w, $fdKey]) { - if ($fdKey !== null) { - unset($this->handles[$fdKey]); + return $event === Event::Error || $event === Event::HangUp || $event === Event::ReadHangUp; + } + + private function addPendingEvent(int $handleId, Event $event): void + { + if (!in_array($event, $this->pendingByHandle[$handleId] ?? [], true)) { + $this->pendingByHandle[$handleId][] = $event; + } + } + + private function consumePending(int $handleId, object $handle, array $requestedEvents): ?PollResult + { + if (empty($this->pendingByHandle[$handleId])) { + return null; + } + $matching = []; + $remaining = []; + foreach ($this->pendingByHandle[$handleId] as $pending) { + if (in_array($pending, $requestedEvents, true)) { + $matching[] = $pending; + // Terminal events persist - every future poll sees them too + if (self::isTerminalEvent($pending)) { + $remaining[] = $pending; + } + } else { + $remaining[] = $pending; } - $w->remove(); } - unset($this->fiberWatchers[$fiberId]); + if (!$matching) { + return null; + } + $this->pendingByHandle[$handleId] = $remaining; + $result = new PollResult(); + $result->handle = $handle; + $result->events = $matching; + $result->timeout = false; + return $result; } public function poll(PollInfo $info): PollResult { $fiber = Fiber::getCurrent(); $fiberId = spl_object_id($fiber); + $id = spl_object_id($info->handle); + + $result = $this->consumePending($id, $info->handle, $info->events); + if ($result !== null) { + return $result; + } if ($info->timeout_ms >= 0) { $deadline = hrtime(true) + $info->timeout_ms * 1_000_000; $this->fiberDeadlines[$fiberId] = [$deadline, $fiber, $info]; } - $id = spl_object_id($info->handle); - if (isset($this->handles[$id])) { - throw new \Exception("Handle already registered"); + $this->registrationsByFiber[$fiberId] = []; + foreach ($info->events as $event) { + $this->fiberByHandleEvent[$id][$event->name] = $fiber; + $this->registrationsByFiber[$fiberId][$id][] = $event->name; } - $this->handles[$id] = $id; - $this->fiberWatchers[$fiberId] = [ - [$this->pollContext->add($info->handle, $info->events, [$fiber]), $id], - ]; + + $this->addOrModifyWatcher($id, $info->handle, $info->events); /** @var PollResult $result */ $result = Fiber::suspend(); @@ -153,19 +248,30 @@ class Scheduler implements Hooks { $fiber = Fiber::getCurrent(); $fiberId = spl_object_id($fiber); - $this->fiberWatchers[$fiberId] = []; $this->fiberIsMulti[$fiberId] = true; + foreach ($infos as $info) { + $id = spl_object_id($info->handle); + $result = $this->consumePending($id, $info->handle, $info->events); + if ($result !== null) { + unset($this->fiberIsMulti[$fiberId]); + return [$result]; + } + } + if ($timeout_ms !== null && $timeout_ms >= 0) { $deadline = hrtime(true) + $timeout_ms * 1_000_000; $this->fiberDeadlines[$fiberId] = [$deadline, $fiber, null]; } + $this->registrationsByFiber[$fiberId] = []; foreach ($infos as $info) { - $this->fiberWatchers[$fiberId][] = [ - $this->pollContext->add($info->handle, $info->events, [$fiber]), - null, - ]; + $id = spl_object_id($info->handle); + foreach ($info->events as $event) { + $this->fiberByHandleEvent[$id][$event->name] = $fiber; + $this->registrationsByFiber[$fiberId][$id][] = $event->name; + } + $this->addOrModifyWatcher($id, $info->handle, $info->events); } /** @var PollResult[] $results */ From 69fd485867e426bde32bef57115caa200c26e983 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 30 Jun 2026 17:40:22 +0200 Subject: [PATCH 15/18] Rename poll_multi() -> pollMulti() --- ext/curl/interface.c | 2 +- ext/standard/basic_functions.c | 4 ++-- ext/standard/file.h | 2 +- ext/standard/io_hooks.c | 4 ++-- ext/standard/io_hooks.stub.php | 2 +- ext/standard/io_hooks_arginfo.h | 4 ++-- ext/standard/tests/streams/hooks/hook-close-fgets.phpt | 2 +- ext/standard/tests/streams/hooks/hook-concurrent-access.phpt | 4 ++-- ext/standard/tests/streams/hooks/scheduler.inc | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ext/curl/interface.c b/ext/curl/interface.c index f82b6de03cfb..53fca464a6c4 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -2569,7 +2569,7 @@ static CURLcode php_curl_exec_multi(php_curl *ch) zval retval; ZVAL_UNDEF(&retval); - zend_call_known_fcc(&FG(io_hooks_poll_multi_fcc), &retval, 1 + n, params, NULL); + zend_call_known_fcc(&FG(io_hooks_pollMulti_fcc), &retval, 1 + n, params, NULL); for (uint32_t j = 0; j < 1 + n; j++) { zval_ptr_dtor(¶ms[j]); diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index ab7b7c1cf45e..4a2b4ad0ca07 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -427,7 +427,7 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */ ZVAL_UNDEF(&FG(io_hooks)); FG(io_hooks_poll_fcc) = empty_fcall_info_cache; - FG(io_hooks_poll_multi_fcc) = empty_fcall_info_cache; + FG(io_hooks_pollMulti_fcc) = empty_fcall_info_cache; return SUCCESS; } @@ -494,7 +494,7 @@ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */ zval_ptr_dtor(&FG(io_hooks)); ZVAL_UNDEF(&FG(io_hooks)); zend_fcc_dtor(&FG(io_hooks_poll_fcc)); - zend_fcc_dtor(&FG(io_hooks_poll_multi_fcc)); + zend_fcc_dtor(&FG(io_hooks_pollMulti_fcc)); } return SUCCESS; diff --git a/ext/standard/file.h b/ext/standard/file.h index afd728996f8b..e8e79b983ada 100644 --- a/ext/standard/file.h +++ b/ext/standard/file.h @@ -106,7 +106,7 @@ typedef struct { int pclose_wait; zval io_hooks; zend_fcall_info_cache io_hooks_poll_fcc; - zend_fcall_info_cache io_hooks_poll_multi_fcc; + zend_fcall_info_cache io_hooks_pollMulti_fcc; #ifdef HAVE_GETHOSTBYNAME_R struct hostent tmp_host_info; char *tmp_host_buf; diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c index 13a7277ac61d..6ba7fa42c855 100644 --- a/ext/standard/io_hooks.c +++ b/ext/standard/io_hooks.c @@ -144,12 +144,12 @@ PHP_FUNCTION(Io_Hooks_set_hooks) }; GC_ADDREF(obj); - zend_string *poll_multi_name = zend_string_init("poll_multi", sizeof("poll_multi") - 1, false); + zend_string *poll_multi_name = zend_string_init("pollMulti", sizeof("pollMulti") - 1, false); zend_function *poll_multi_fn = obj->handlers->get_method(&obj, poll_multi_name, NULL); zend_string_release(poll_multi_name); ZEND_ASSERT(poll_multi_fn != NULL); - FG(io_hooks_poll_multi_fcc) = (zend_fcall_info_cache){ + FG(io_hooks_pollMulti_fcc) = (zend_fcall_info_cache){ .function_handler = poll_multi_fn, .object = obj, .called_scope = obj->ce, diff --git a/ext/standard/io_hooks.stub.php b/ext/standard/io_hooks.stub.php index ae48d64f2f48..8415d1ddaf07 100644 --- a/ext/standard/io_hooks.stub.php +++ b/ext/standard/io_hooks.stub.php @@ -21,7 +21,7 @@ final class PollResult { interface Hooks { public function poll(PollInfo $info): PollResult; /* @return PollResult[] Empty when $timeout_ms is exceeded */ - public function poll_multi(?int $timeout_ms, PollInfo ...$info): array; + public function pollMulti(?int $timeout_ms, PollInfo ...$info): array; } function set_hooks(?Hooks $hooks): ?Hooks {} diff --git a/ext/standard/io_hooks_arginfo.h b/ext/standard/io_hooks_arginfo.h index 7da3897b4f93..c0faaded0c18 100644 --- a/ext/standard/io_hooks_arginfo.h +++ b/ext/standard/io_hooks_arginfo.h @@ -9,7 +9,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Hooks_Hooks_poll, 0, 1, ZEND_ARG_OBJ_INFO(0, info, Io\\Hooks\\PollInfo, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Hooks_Hooks_poll_multi, 0, 1, IS_ARRAY, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Hooks_Hooks_pollMulti, 0, 1, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, timeout_ms, IS_LONG, 1) ZEND_ARG_VARIADIC_OBJ_INFO(0, info, Io\\Hooks\\PollInfo, 0) ZEND_END_ARG_INFO() @@ -24,7 +24,7 @@ static const zend_function_entry ext_functions[] = { static const zend_function_entry class_Io_Hooks_Hooks_methods[] = { ZEND_RAW_FENTRY("poll", NULL, arginfo_class_Io_Hooks_Hooks_poll, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) - ZEND_RAW_FENTRY("poll_multi", NULL, arginfo_class_Io_Hooks_Hooks_poll_multi, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("pollMulti", NULL, arginfo_class_Io_Hooks_Hooks_pollMulti, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) ZEND_FE_END }; diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt index 47b24ca908c4..c7baf1656727 100644 --- a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -18,7 +18,7 @@ class CloseOnceHooks implements Hooks { $result->timeout = false; return $result; } - public function poll_multi(?int $timeout_ms, PollInfo ...$info): array { throw new \Exception("poll_multi not implemented"); } + public function pollMulti(?int $timeout_ms, PollInfo ...$info): array { throw new \Exception("pollMulti not implemented"); } } Io\Hooks\set_hooks(new CloseOnceHooks()); diff --git a/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt b/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt index ee5483a23828..961fa0b45eed 100644 --- a/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt +++ b/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt @@ -22,8 +22,8 @@ class ConcurrentHook implements Hooks { $result->timeout = false; return $result; } - public function poll_multi(?int $timeout_ms, PollInfo ...$info): array { - throw new \Exception("poll_multi not implemented"); + public function pollMulti(?int $timeout_ms, PollInfo ...$info): array { + throw new \Exception("pollMulti not implemented"); } } diff --git a/ext/standard/tests/streams/hooks/scheduler.inc b/ext/standard/tests/streams/hooks/scheduler.inc index 4f06eaeac78d..965b3a818924 100644 --- a/ext/standard/tests/streams/hooks/scheduler.inc +++ b/ext/standard/tests/streams/hooks/scheduler.inc @@ -244,7 +244,7 @@ class Scheduler implements Hooks return $result; } - public function poll_multi(?int $timeout_ms, PollInfo ...$infos): array + public function pollMulti(?int $timeout_ms, PollInfo ...$infos): array { $fiber = Fiber::getCurrent(); $fiberId = spl_object_id($fiber); From 401fdbb80be26193462b96de6800b6dae6a572e8 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Tue, 30 Jun 2026 18:01:02 +0200 Subject: [PATCH 16/18] Hooks::sleep() --- ext/standard/basic_functions.c | 23 +++++++++++++++++++ ext/standard/file.h | 1 + ext/standard/io_hooks.c | 21 +++++++++++++++++ ext/standard/io_hooks.h | 2 ++ ext/standard/io_hooks.stub.php | 3 +++ ext/standard/io_hooks_arginfo.h | 7 +++++- .../tests/streams/hooks/hook-close-fgets.phpt | 1 + .../streams/hooks/hook-concurrent-access.phpt | 1 + .../tests/streams/hooks/scheduler.inc | 12 ++++++++++ 9 files changed, 70 insertions(+), 1 deletion(-) diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 4a2b4ad0ca07..50dcaf1be1c2 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -113,6 +113,7 @@ PHPAPI php_basic_globals basic_globals; #include "streamsfuncs.h" #include "zend_frameless_function.h" #include "basic_functions_arginfo.h" +#include "io_hooks.h" #if __has_feature(memory_sanitizer) # include @@ -428,6 +429,7 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */ ZVAL_UNDEF(&FG(io_hooks)); FG(io_hooks_poll_fcc) = empty_fcall_info_cache; FG(io_hooks_pollMulti_fcc) = empty_fcall_info_cache; + FG(io_hooks_sleep_fcc) = empty_fcall_info_cache; return SUCCESS; } @@ -495,6 +497,7 @@ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */ ZVAL_UNDEF(&FG(io_hooks)); zend_fcc_dtor(&FG(io_hooks_poll_fcc)); zend_fcc_dtor(&FG(io_hooks_pollMulti_fcc)); + zend_fcc_dtor(&FG(io_hooks_sleep_fcc)); } return SUCCESS; @@ -1146,6 +1149,11 @@ PHP_FUNCTION(sleep) RETURN_THROWS(); } + if (PHP_HAS_IO_SLEEP_HOOK()) { + php_io_hooks_sleep(num, 0); + RETURN_LONG(0); + } + RETURN_LONG(php_sleep((unsigned int)num)); } /* }}} */ @@ -1165,6 +1173,10 @@ PHP_FUNCTION(usleep) } #ifdef HAVE_USLEEP + if (PHP_HAS_IO_SLEEP_HOOK()) { + php_io_hooks_sleep(num / 1000000LL, (num % 1000000LL) * 1000LL); + return; + } usleep((unsigned int)num); #endif } @@ -1191,6 +1203,11 @@ PHP_FUNCTION(time_nanosleep) RETURN_THROWS(); } + if (PHP_HAS_IO_SLEEP_HOOK()) { + php_io_hooks_sleep(tv_sec, tv_nsec); + RETURN_TRUE; + } + php_req.tv_sec = (time_t) tv_sec; php_req.tv_nsec = (long)tv_nsec; if (!nanosleep(&php_req, &php_rem)) { @@ -1241,6 +1258,12 @@ PHP_FUNCTION(time_sleep_until) } diff_ns = target_ns - current_ns; + + if (PHP_HAS_IO_SLEEP_HOOK()) { + php_io_hooks_sleep((zend_long)(diff_ns / ns_per_sec), (zend_long)(diff_ns % ns_per_sec)); + RETURN_TRUE; + } + php_req.tv_sec = (time_t) (diff_ns / ns_per_sec); php_req.tv_nsec = (long) (diff_ns % ns_per_sec); diff --git a/ext/standard/file.h b/ext/standard/file.h index e8e79b983ada..894fe332aff4 100644 --- a/ext/standard/file.h +++ b/ext/standard/file.h @@ -107,6 +107,7 @@ typedef struct { zval io_hooks; zend_fcall_info_cache io_hooks_poll_fcc; zend_fcall_info_cache io_hooks_pollMulti_fcc; + zend_fcall_info_cache io_hooks_sleep_fcc; #ifdef HAVE_GETHOSTBYNAME_R struct hostent tmp_host_info; char *tmp_host_buf; diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c index 6ba7fa42c855..e372603c4a3d 100644 --- a/ext/standard/io_hooks.c +++ b/ext/standard/io_hooks.c @@ -121,6 +121,8 @@ PHP_FUNCTION(Io_Hooks_set_hooks) ZVAL_COPY(return_value, &FG(io_hooks)); zval_ptr_dtor(&FG(io_hooks)); zend_fcc_dtor(&FG(io_hooks_poll_fcc)); + zend_fcc_dtor(&FG(io_hooks_pollMulti_fcc)); + zend_fcc_dtor(&FG(io_hooks_sleep_fcc)); } if (hooks_obj == NULL || Z_TYPE_P(hooks_obj) == IS_NULL) { @@ -156,6 +158,25 @@ PHP_FUNCTION(Io_Hooks_set_hooks) }; GC_ADDREF(obj); + zend_string *sleep_name = zend_string_init("sleep", sizeof("sleep") - 1, false); + zend_function *sleep_fn = obj->handlers->get_method(&obj, sleep_name, NULL); + zend_string_release(sleep_name); + ZEND_ASSERT(sleep_fn != NULL); + + FG(io_hooks_sleep_fcc) = (zend_fcall_info_cache){ + .function_handler = sleep_fn, + .object = obj, + .called_scope = obj->ce, + }; + GC_ADDREF(obj); +} + +PHPAPI void php_io_hooks_sleep(zend_long seconds, zend_long nanoseconds) +{ + zval params[2]; + ZVAL_LONG(¶ms[0], seconds); + ZVAL_LONG(¶ms[1], nanoseconds); + zend_call_known_fcc(&FG(io_hooks_sleep_fcc), NULL, 2, params, NULL); } PHP_MINIT_FUNCTION(io_hooks) diff --git a/ext/standard/io_hooks.h b/ext/standard/io_hooks.h index 04f2f23396b3..dbe330a18408 100644 --- a/ext/standard/io_hooks.h +++ b/ext/standard/io_hooks.h @@ -23,8 +23,10 @@ PHPAPI extern zend_class_entry *php_io_hooks_poll_info_ce; PHPAPI extern zend_class_entry *php_io_hooks_poll_result_ce; #define PHP_HAS_IO_POLL_HOOK() ZEND_FCC_INITIALIZED(FG(io_hooks_poll_fcc)) +#define PHP_HAS_IO_SLEEP_HOOK() ZEND_FCC_INITIALIZED(FG(io_hooks_sleep_fcc)) PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, const struct timeval *timeout); +PHPAPI void php_io_hooks_sleep(zend_long seconds, zend_long nanoseconds); PHP_MINIT_FUNCTION(io_hooks); diff --git a/ext/standard/io_hooks.stub.php b/ext/standard/io_hooks.stub.php index 8415d1ddaf07..e2632d603a4a 100644 --- a/ext/standard/io_hooks.stub.php +++ b/ext/standard/io_hooks.stub.php @@ -4,6 +4,8 @@ namespace Io\Hooks { + use Io\Poll\Handle; + final class PollInfo { public Handle $handle; /* @var Io\Poll\Event[] */ @@ -22,6 +24,7 @@ interface Hooks { public function poll(PollInfo $info): PollResult; /* @return PollResult[] Empty when $timeout_ms is exceeded */ public function pollMulti(?int $timeout_ms, PollInfo ...$info): array; + public function sleep(int $seconds, int $nanoseconds): void; } function set_hooks(?Hooks $hooks): ?Hooks {} diff --git a/ext/standard/io_hooks_arginfo.h b/ext/standard/io_hooks_arginfo.h index c0faaded0c18..5abee175ac69 100644 --- a/ext/standard/io_hooks_arginfo.h +++ b/ext/standard/io_hooks_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit io_hooks.stub.php instead. - * Stub hash: b741447fd9d275264617736e8130562298da3d45 */ + * Stub hash: 364d7e209716e763806acd085421c11a404683d3 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_Io_Hooks_set_hooks, 0, 1, Io\\Hooks\\Hooks, 1) ZEND_ARG_OBJ_INFO(0, hooks, Io\\Hooks\\Hooks, 1) @@ -14,6 +14,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Hooks_Hooks_pollMulti, ZEND_ARG_VARIADIC_OBJ_INFO(0, info, Io\\Hooks\\PollInfo, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Hooks_Hooks_sleep, 0, 2, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) + ZEND_ARG_TYPE_INFO(0, nanoseconds, IS_LONG, 0) +ZEND_END_ARG_INFO() ZEND_FUNCTION(Io_Hooks_set_hooks); @@ -25,6 +29,7 @@ static const zend_function_entry ext_functions[] = { static const zend_function_entry class_Io_Hooks_Hooks_methods[] = { ZEND_RAW_FENTRY("poll", NULL, arginfo_class_Io_Hooks_Hooks_poll, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) ZEND_RAW_FENTRY("pollMulti", NULL, arginfo_class_Io_Hooks_Hooks_pollMulti, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("sleep", NULL, arginfo_class_Io_Hooks_Hooks_sleep, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) ZEND_FE_END }; diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt index c7baf1656727..6ab4919d4b51 100644 --- a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -19,6 +19,7 @@ class CloseOnceHooks implements Hooks { return $result; } public function pollMulti(?int $timeout_ms, PollInfo ...$info): array { throw new \Exception("pollMulti not implemented"); } + public function sleep(int $seconds, int $nanoseconds): void { throw new \Exception("sleep not implemented"); } } Io\Hooks\set_hooks(new CloseOnceHooks()); diff --git a/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt b/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt index 961fa0b45eed..5efd061e9baf 100644 --- a/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt +++ b/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt @@ -25,6 +25,7 @@ class ConcurrentHook implements Hooks { public function pollMulti(?int $timeout_ms, PollInfo ...$info): array { throw new \Exception("pollMulti not implemented"); } + public function sleep(int $seconds, int $nanoseconds): void { throw new \Exception("sleep not implemented"); } } Io\Hooks\set_hooks(new ConcurrentHook()); diff --git a/ext/standard/tests/streams/hooks/scheduler.inc b/ext/standard/tests/streams/hooks/scheduler.inc index 965b3a818924..38a9e8200595 100644 --- a/ext/standard/tests/streams/hooks/scheduler.inc +++ b/ext/standard/tests/streams/hooks/scheduler.inc @@ -138,6 +138,9 @@ class Scheduler implements Hooks if ($this->fiberIsMulti[$fiberId] ?? false) { unset($this->fiberIsMulti[$fiberId]); $this->ready[] = [$fiber, []]; + } elseif ($info === null) { + // sleep deadline: resume with no value + $this->ready[] = [$fiber, null]; } else { $result = new PollResult(); $result->handle = $info->handle; @@ -279,6 +282,15 @@ class Scheduler implements Hooks return $results ?? []; } + public function sleep(int $seconds, int $nanoseconds): void + { + $fiber = Fiber::getCurrent(); + $fiberId = spl_object_id($fiber); + $deadline = hrtime(true) + $seconds * 1_000_000_000 + $nanoseconds; + $this->fiberDeadlines[$fiberId] = [$deadline, $fiber, null]; + Fiber::suspend(); + } + public function go(callable $fn): void { $this->ready[] = [new Fiber($fn), null]; From afc9b43471624c853f457548253cdb262bc9b5a1 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 3 Jul 2026 11:17:37 +0200 Subject: [PATCH 17/18] Internal API --- configure.ac | 3 + ext/curl/interface.c | 134 ++++------------- ext/standard/basic_functions.c | 22 +-- ext/standard/file.h | 8 +- ext/standard/io_hooks.c | 261 +++++++++++++++++++++------------ ext/standard/io_hooks.h | 10 +- ext/standard/io_poll.c | 2 +- ext/standard/io_poll.h | 1 + main/hooks/io_hooks.c | 90 ++++++++++++ main/hooks/io_hooks.h | 66 +++++++++ main/php_network.h | 17 ++- 11 files changed, 382 insertions(+), 232 deletions(-) create mode 100644 main/hooks/io_hooks.c create mode 100644 main/hooks/io_hooks.h diff --git a/configure.ac b/configure.ac index 9014869fb94e..abd938c54143 100644 --- a/configure.ac +++ b/configure.ac @@ -1696,6 +1696,9 @@ PHP_ADD_SOURCES([main/poll], m4_normalize([ ]), [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) +PHP_ADD_SOURCES([main/hooks], [io_hooks.c], + [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) + PHP_ADD_SOURCES([main/streams], m4_normalize([ cast.c filter.c diff --git a/ext/curl/interface.c b/ext/curl/interface.c index 53fca464a6c4..b32f8d588ce3 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -2460,49 +2460,7 @@ static int php_curl_timer_callback(CURLM *multi, long timeout_ms, void *userp) return 0; } -/* Translate CURL_POLL_* to Io\Poll\Event[] array */ -static void php_curl_poll_events_to_zval(int what, zval *dest) -{ - array_init(dest); - if (what & CURL_POLL_IN) { - zval zv; - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Read)); - zend_hash_next_index_insert(Z_ARRVAL_P(dest), &zv); - } - if (what & CURL_POLL_OUT) { - zval zv; - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Write)); - zend_hash_next_index_insert(Z_ARRVAL_P(dest), &zv); - } -} -/* Translate PollResult::$events back to CURL_CSELECT_* flags */ -static int php_curl_poll_result_to_curl_events(zval *events_arr) -{ - int curl_events = 0; - zval *event; - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(events_arr), event) { - if (Z_TYPE_P(event) != IS_OBJECT) continue; - zend_enum_Io_Poll_Event ev = (zend_enum_Io_Poll_Event) zend_enum_fetch_case_id(Z_OBJ_P(event)); - switch (ev) { - case ZEND_ENUM_Io_Poll_Event_Read: - curl_events |= CURL_CSELECT_IN; - break; - case ZEND_ENUM_Io_Poll_Event_Write: - curl_events |= CURL_CSELECT_OUT; - break; - case ZEND_ENUM_Io_Poll_Event_Error: - curl_events |= CURL_CSELECT_ERR; - break; - case ZEND_ENUM_Io_Poll_Event_HangUp: - case ZEND_ENUM_Io_Poll_Event_ReadHangUp: - case ZEND_ENUM_Io_Poll_Event_OneShot: - case ZEND_ENUM_Io_Poll_Event_EdgeTriggered: - break; - } - } ZEND_HASH_FOREACH_END(); - return curl_events; -} /* Main multi-based exec loop */ static CURLcode php_curl_exec_multi(php_curl *ch) @@ -2528,87 +2486,59 @@ static CURLcode php_curl_exec_multi(php_curl *ch) continue; } - if (PHP_HAS_IO_POLL_HOOK()) { - /* Count active sockets */ + if (FG(io_hooks).poll_multi) { + /* Build php_io_hooks_poll_info[] for all active sockets */ uint32_t n = ch->io_sockets ? zend_hash_num_elements(ch->io_sockets) : 0; + php_io_hooks_poll_info *infos = n > 0 ? safe_emalloc(n, sizeof(php_io_hooks_poll_info), 0) : NULL; - /* Build argv: [timeout_ms_or_null, PollInfo...] */ - zval *params = safe_emalloc(n, sizeof(zval), sizeof(zval)); - - if (ch->io_timer_ms < 0) { - ZVAL_NULL(¶ms[0]); - } else { - ZVAL_LONG(¶ms[0], ch->io_timer_ms); - } - - uint32_t i = 1; if (n > 0) { + uint32_t i = 0; zend_ulong sock_ulong; zval *events_zv; ZEND_HASH_FOREACH_NUM_KEY_VAL(ch->io_sockets, sock_ulong, events_zv) { - zval *poll_info = ¶ms[i++]; - object_init_ex(poll_info, php_io_hooks_poll_info_ce); - - zval handle; - php_curl_socket_handle_from_fd(ch, &handle, (curl_socket_t)sock_ulong); - zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ_P(poll_info), - "handle", sizeof("handle") - 1, &handle); - zval_ptr_dtor(&handle); - - zval evts; - php_curl_poll_events_to_zval((int)Z_LVAL_P(events_zv), &evts); - zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ_P(poll_info), - "events", sizeof("events") - 1, &evts); - zval_ptr_dtor(&evts); - - /* timeout_ms per PollInfo: -1 (we use global timeout via params[0]) */ - zend_update_property_long(php_io_hooks_poll_info_ce, Z_OBJ_P(poll_info), - "timeout_ms", sizeof("timeout_ms") - 1, -1); + zval handle_zv; + php_curl_socket_handle_from_fd(ch, &handle_zv, (curl_socket_t)sock_ulong); + infos[i].handle = Z_OBJ(handle_zv); + int curl_what = (int)Z_LVAL_P(events_zv); + infos[i].events = ((curl_what & CURL_POLL_IN) ? PHP_POLL_READ : 0) + | ((curl_what & CURL_POLL_OUT) ? PHP_POLL_WRITE : 0); + infos[i].timeout_ms = -1; + i++; } ZEND_HASH_FOREACH_END(); } - zval retval; - ZVAL_UNDEF(&retval); - zend_call_known_fcc(&FG(io_hooks_pollMulti_fcc), &retval, 1 + n, params, NULL); + php_io_hooks_poll_result *poll_result = FG(io_hooks).poll_multi( + FG(io_hooks_data), ch->io_timer_ms, n, infos); - for (uint32_t j = 0; j < 1 + n; j++) { - zval_ptr_dtor(¶ms[j]); + for (uint32_t i = 0; i < n; i++) { + OBJ_RELEASE(infos[i].handle); } - efree(params); + if (infos) efree(infos); if (EG(exception)) { - zval_ptr_dtor(&retval); + if (poll_result) { + if (poll_result->handle) OBJ_RELEASE(poll_result->handle); + efree(poll_result); + } break; } - if (zend_hash_num_elements(Z_ARRVAL(retval)) > 0) { - /* Drive ready sockets */ - zval *result; - ZEND_HASH_FOREACH_VAL(Z_ARRVAL(retval), result) { - if (Z_TYPE_P(result) != IS_OBJECT || - Z_OBJCE_P(result) != php_io_hooks_poll_result_ce) continue; - - zval rv; - zval *handle_prop = zend_read_property(php_io_hooks_poll_result_ce, - Z_OBJ_P(result), "handle", sizeof("handle") - 1, 1, &rv); - if (Z_TYPE_P(handle_prop) != IS_OBJECT) continue; - - php_poll_handle_object *hobj = - PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(Z_OBJ_P(handle_prop)); + if (poll_result) { + /* Drive the ready socket */ + if (poll_result->handle) { + php_poll_handle_object *hobj = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(poll_result->handle); php_socket_t fd = php_poll_handle_get_fd(hobj); - - zval *events_prop = zend_read_property(php_io_hooks_poll_result_ce, - Z_OBJ_P(result), "events", sizeof("events") - 1, 1, &rv); - int curl_events = (Z_TYPE_P(events_prop) == IS_ARRAY) - ? php_curl_poll_result_to_curl_events(events_prop) : 0; - + int curl_events = ((poll_result->events & PHP_POLL_READ) ? CURL_CSELECT_IN : 0) + | ((poll_result->events & PHP_POLL_WRITE) ? CURL_CSELECT_OUT : 0) + | ((poll_result->events & PHP_POLL_ERROR) ? CURL_CSELECT_ERR : 0); curl_multi_socket_action(multi, (curl_socket_t)fd, curl_events, &still_running); - } ZEND_HASH_FOREACH_END(); + OBJ_RELEASE(poll_result->handle); + } + efree(poll_result); } else { - /* Empty array = timeout */ + /* NULL = timeout */ curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &still_running); } - zval_ptr_dtor(&retval); } else { /* No hook: select() on the sockets logged by the socket callback */ fd_set rfds, wfds, efds; diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 50dcaf1be1c2..f4d6251db00e 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -426,10 +426,8 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */ /* Default to global filters only */ FG(stream_filters) = NULL; - ZVAL_UNDEF(&FG(io_hooks)); - FG(io_hooks_poll_fcc) = empty_fcall_info_cache; - FG(io_hooks_pollMulti_fcc) = empty_fcall_info_cache; - FG(io_hooks_sleep_fcc) = empty_fcall_info_cache; + memset(&FG(io_hooks), 0, sizeof(FG(io_hooks))); + FG(io_hooks_data) = NULL; return SUCCESS; } @@ -492,13 +490,7 @@ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */ BG(page_uid) = -1; BG(page_gid) = -1; - if (!Z_ISUNDEF(FG(io_hooks))) { - zval_ptr_dtor(&FG(io_hooks)); - ZVAL_UNDEF(&FG(io_hooks)); - zend_fcc_dtor(&FG(io_hooks_poll_fcc)); - zend_fcc_dtor(&FG(io_hooks_pollMulti_fcc)); - zend_fcc_dtor(&FG(io_hooks_sleep_fcc)); - } + php_set_io_hooks(NULL, 0, NULL); return SUCCESS; } @@ -1149,7 +1141,7 @@ PHP_FUNCTION(sleep) RETURN_THROWS(); } - if (PHP_HAS_IO_SLEEP_HOOK()) { + if (FG(io_hooks).sleep) { php_io_hooks_sleep(num, 0); RETURN_LONG(0); } @@ -1173,7 +1165,7 @@ PHP_FUNCTION(usleep) } #ifdef HAVE_USLEEP - if (PHP_HAS_IO_SLEEP_HOOK()) { + if (FG(io_hooks).sleep) { php_io_hooks_sleep(num / 1000000LL, (num % 1000000LL) * 1000LL); return; } @@ -1203,7 +1195,7 @@ PHP_FUNCTION(time_nanosleep) RETURN_THROWS(); } - if (PHP_HAS_IO_SLEEP_HOOK()) { + if (FG(io_hooks).sleep) { php_io_hooks_sleep(tv_sec, tv_nsec); RETURN_TRUE; } @@ -1259,7 +1251,7 @@ PHP_FUNCTION(time_sleep_until) diff_ns = target_ns - current_ns; - if (PHP_HAS_IO_SLEEP_HOOK()) { + if (FG(io_hooks).sleep) { php_io_hooks_sleep((zend_long)(diff_ns / ns_per_sec), (zend_long)(diff_ns % ns_per_sec)); RETURN_TRUE; } diff --git a/ext/standard/file.h b/ext/standard/file.h index 894fe332aff4..aa7d36f29c1a 100644 --- a/ext/standard/file.h +++ b/ext/standard/file.h @@ -19,6 +19,8 @@ # include #endif +#include "main/hooks/io_hooks.h" + PHP_MINIT_FUNCTION(file); PHP_MSHUTDOWN_FUNCTION(file); @@ -104,10 +106,8 @@ typedef struct { HashTable *wrapper_logged_errors; /* key: wrapper address; value: linked list of error entries */ php_stream_error_state stream_error_state; int pclose_wait; - zval io_hooks; - zend_fcall_info_cache io_hooks_poll_fcc; - zend_fcall_info_cache io_hooks_pollMulti_fcc; - zend_fcall_info_cache io_hooks_sleep_fcc; + php_io_hooks io_hooks; + void *io_hooks_data; #ifdef HAVE_GETHOSTBYNAME_R struct hostent tmp_host_info; char *tmp_host_buf; diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c index e372603c4a3d..0adafcb1870a 100644 --- a/ext/standard/io_hooks.c +++ b/ext/standard/io_hooks.c @@ -13,102 +13,184 @@ */ #include "php.h" -#include "zend_enum.h" #include "ext/standard/file.h" #include "ext/standard/io_poll.h" -#include "io_poll_decl.h" #include "ext/standard/io_hooks.h" #include "io_hooks_arginfo.h" -#ifdef HAVE_POLL_H -#include -#elif HAVE_SYS_POLL_H -#include -#endif - PHPAPI zend_class_entry *php_io_hooks_poll_info_ce; PHPAPI zend_class_entry *php_io_hooks_poll_result_ce; static zend_class_entry *php_io_hooks_hooks_ce; -static void php_pollfd_events_to_io_poll_events(zend_array *dest, int events) +/* Data held by the PHP-object adapter registered via php_set_io_hooks() */ +typedef struct _php_io_hooks_php_data { + zend_fcall_info_cache poll_fcc; + zend_fcall_info_cache pollMulti_fcc; + zend_fcall_info_cache sleep_fcc; +} php_io_hooks_php_data; + +/* ----------------------------------------------------------------------- + * Helpers + * ---------------------------------------------------------------------- */ + +/* Build a PollInfo PHP object from a php_io_hooks_poll_info. */ +static void php_poll_info_to_zval(php_io_hooks_poll_info *info, zval *dest) { - zval zv; + object_init_ex(dest, php_io_hooks_poll_info_ce); - if (events & POLLIN) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Read)); - zend_hash_next_index_insert(dest, &zv); - } - if (events & POLLOUT) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Write)); - zend_hash_next_index_insert(dest, &zv); + zval handle_zv; + ZVAL_OBJ_COPY(&handle_zv, info->handle); + zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ_P(dest), + "handle", sizeof("handle") - 1, &handle_zv); + zval_ptr_dtor(&handle_zv); + + zval events_zv; + php_io_poll_events_to_event_enums(info->events, &events_zv); + zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ_P(dest), + "events", sizeof("events") - 1, &events_zv); + zval_ptr_dtor(&events_zv); + + zend_update_property_long(php_io_hooks_poll_info_ce, Z_OBJ_P(dest), + "timeout_ms", sizeof("timeout_ms") - 1, info->timeout_ms); +} + +/* Convert a PHP PollResult object to a php_io_hooks_poll_result. */ +static php_io_hooks_poll_result *php_poll_result_from_zval(zval *result_zv) +{ + zend_object *result_obj = Z_OBJ_P(result_zv); + zval rv; + + zval *handle_prop = zend_read_property(php_io_hooks_poll_result_ce, result_obj, + "handle", sizeof("handle") - 1, /* silent */ 1, &rv); + zval *events_prop = zend_read_property(php_io_hooks_poll_result_ce, result_obj, + "events", sizeof("events") - 1, /* silent */ 1, &rv); + zval *timeout_prop = zend_read_property(php_io_hooks_poll_result_ce, result_obj, + "timeout", sizeof("timeout") - 1, /* silent */ 1, &rv); + + php_io_hooks_poll_result *result = emalloc(sizeof(php_io_hooks_poll_result)); + if (Z_TYPE_P(handle_prop) == IS_OBJECT) { + GC_ADDREF(Z_OBJ_P(handle_prop)); + result->handle = Z_OBJ_P(handle_prop); + } else { + result->handle = NULL; } - if (events & POLLERR) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Error)); - zend_hash_next_index_insert(dest, &zv); + result->events = (Z_TYPE_P(events_prop) == IS_ARRAY) + ? php_io_poll_event_enums_to_events(events_prop) : 0; + result->timeout = zend_is_true(timeout_prop); + return result; +} + +/* ----------------------------------------------------------------------- + * PHP adapter: hook implementations that forward to a PHP Hooks object + * ---------------------------------------------------------------------- */ + +static php_io_hooks_poll_result *php_io_hooks_php_poll(void *data, php_io_hooks_poll_info *info) +{ + php_io_hooks_php_data *php_data = (php_io_hooks_php_data *)data; + + zval poll_info_zv; + php_poll_info_to_zval(info, &poll_info_zv); + + zval retval; + ZVAL_UNDEF(&retval); + zend_call_known_fcc(&php_data->poll_fcc, &retval, 1, &poll_info_zv, NULL); + zval_ptr_dtor(&poll_info_zv); + + if (EG(exception)) { + zval_ptr_dtor(&retval); + return NULL; } - if (events & POLLHUP) { - ZVAL_OBJ_COPY(&zv, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_HangUp)); - zend_hash_next_index_insert(dest, &zv); + + if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT || Z_OBJCE(retval) != php_io_hooks_poll_result_ce)) { + zend_type_error("%s::poll() must return %s, %s returned", + ZSTR_VAL(php_io_hooks_hooks_ce->name), + ZSTR_VAL(php_io_hooks_poll_result_ce->name), + zend_zval_type_name(&retval)); + zval_ptr_dtor(&retval); + return NULL; } + + php_io_hooks_poll_result *result = php_poll_result_from_zval(&retval); + zval_ptr_dtor(&retval); + return result; } -PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, const struct timeval *timeout) +static php_io_hooks_poll_result *php_io_hooks_php_poll_multi(void *data, zend_long timeout_ms, + uint32_t num_infos, php_io_hooks_poll_info *infos) { - // TODO: optimize poll_info object creation+init. Maybe reuse it too (e.g. if RC=1) - zval poll_info; - object_init_ex(&poll_info, php_io_hooks_poll_info_ce); - - zval handle; - php_stream_poll_weak_handle_from_stream(&handle, stream); - zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ(poll_info), - "handle", sizeof("handle") - 1, &handle); - zval_ptr_dtor(&handle); - - zval events_arr; - array_init(&events_arr); - php_pollfd_events_to_io_poll_events(Z_ARRVAL(events_arr), events); - zend_update_property(php_io_hooks_poll_info_ce, Z_OBJ(poll_info), - "events", sizeof("events") - 1, &events_arr); - zval_ptr_dtor(&events_arr); - - zend_long timeout_ms; - if (timeout == NULL) { - timeout_ms = -1; + php_io_hooks_php_data *php_data = (php_io_hooks_php_data *)data; + + zval *params = safe_emalloc(num_infos, sizeof(zval), sizeof(zval)); + + if (timeout_ms < 0) { + ZVAL_NULL(¶ms[0]); } else { - timeout_ms = (zend_long)timeout->tv_sec * 1000 + (zend_long)timeout->tv_usec / 1000; + ZVAL_LONG(¶ms[0], timeout_ms); + } + + for (uint32_t i = 0; i < num_infos; i++) { + php_poll_info_to_zval(&infos[i], ¶ms[1 + i]); } - zend_update_property_long(php_io_hooks_poll_info_ce, Z_OBJ(poll_info), - "timeout_ms", sizeof("timeout_ms") - 1, timeout_ms); zval retval; ZVAL_UNDEF(&retval); - ZEND_ASSERT(!(stream->flags & PHP_STREAM_FLAG_BEING_POLLED)); - stream->flags |= PHP_STREAM_FLAG_BEING_POLLED; - zend_call_known_fcc(&FG(io_hooks_poll_fcc), &retval, 1, &poll_info, NULL); - stream->flags &= ~PHP_STREAM_FLAG_BEING_POLLED; - zval_ptr_dtor(&poll_info); + zend_call_known_fcc(&php_data->pollMulti_fcc, &retval, 1 + num_infos, params, NULL); + + for (uint32_t i = 0; i < 1 + num_infos; i++) { + zval_ptr_dtor(¶ms[i]); + } + efree(params); if (EG(exception)) { - goto return_error; + zval_ptr_dtor(&retval); + return NULL; } - if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT || Z_OBJCE(retval) != php_io_hooks_poll_result_ce)) { - zend_type_error("%s::poll() must return %s, %s returned", - ZSTR_VAL(php_io_hooks_hooks_ce->name), - ZSTR_VAL(php_io_hooks_poll_result_ce->name), - zend_zval_type_name(&retval)); - goto return_error; + if (Z_TYPE(retval) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL(retval)) == 0) { + zval_ptr_dtor(&retval); + return NULL; } - return Z_OBJ(retval); + zval *first = zend_hash_index_find(Z_ARRVAL(retval), 0); + php_poll_result *result = NULL; + if (first && Z_TYPE_P(first) == IS_OBJECT && Z_OBJCE_P(first) == php_io_hooks_poll_result_ce) { + result = php_poll_result_from_zval(first); + } -return_error: - ZEND_ASSERT(EG(exception)); + php_io_hooks_poll_result *result = php_poll_result_from_zval(&retval); zval_ptr_dtor(&retval); + return result; +} - return NULL; +static void php_io_hooks_php_sleep(void *data, zend_long seconds, zend_long nanoseconds) +{ + php_io_hooks_php_data *php_data = (php_io_hooks_php_data *)data; + zval params[2]; + ZVAL_LONG(¶ms[0], seconds); + ZVAL_LONG(¶ms[1], nanoseconds); + zend_call_known_fcc(&php_data->sleep_fcc, NULL, 2, params, NULL); +} + +static void php_io_hooks_php_dtor(void *data) +{ + php_io_hooks_php_data *php_data = (php_io_hooks_php_data *)data; + zend_fcc_dtor(&php_data->poll_fcc); + zend_fcc_dtor(&php_data->pollMulti_fcc); + zend_fcc_dtor(&php_data->sleep_fcc); + efree(php_data); } +static const php_io_hooks php_io_hooks_php_adapter = { + .poll = php_io_hooks_php_poll, + .poll_multi = php_io_hooks_php_poll_multi, + .sleep = php_io_hooks_php_sleep, + .dtor = php_io_hooks_php_dtor, +}; + +/* ----------------------------------------------------------------------- + * PHP function: Io\Hooks\set_hooks() + * ---------------------------------------------------------------------- */ + PHP_FUNCTION(Io_Hooks_set_hooks) { zval *hooks_obj = NULL; @@ -117,32 +199,33 @@ PHP_FUNCTION(Io_Hooks_set_hooks) Z_PARAM_OBJECT_OF_CLASS_OR_NULL(hooks_obj, php_io_hooks_hooks_ce) ZEND_PARSE_PARAMETERS_END(); - if (!Z_ISUNDEF(FG(io_hooks))) { - ZVAL_COPY(return_value, &FG(io_hooks)); - zval_ptr_dtor(&FG(io_hooks)); - zend_fcc_dtor(&FG(io_hooks_poll_fcc)); - zend_fcc_dtor(&FG(io_hooks_pollMulti_fcc)); - zend_fcc_dtor(&FG(io_hooks_sleep_fcc)); - } + bool has_hooks = FG(io_hooks).poll || FG(io_hooks).poll_multi + || FG(io_hooks).sleep || FG(io_hooks).dtor; - if (hooks_obj == NULL || Z_TYPE_P(hooks_obj) == IS_NULL) { - ZVAL_UNDEF(&FG(io_hooks)); + if (hooks_obj != NULL && Z_TYPE_P(hooks_obj) != IS_NULL) { + if (has_hooks) { + zend_throw_error(NULL, + "I/O hooks are already set; call %s(null) to unregister them first", + ZSTR_VAL(execute_data->func->common.function_name)); + RETURN_THROWS(); + } + } else { + php_set_io_hooks(NULL, 0, NULL); return; } - ZVAL_COPY(&FG(io_hooks), hooks_obj); - zend_object *obj = Z_OBJ_P(hooks_obj); + php_io_hooks_php_data *php_data = emalloc(sizeof(php_io_hooks_php_data)); + zend_string *poll_name = zend_string_init("poll", sizeof("poll") - 1, false); zend_function *poll_fn = obj->handlers->get_method(&obj, poll_name, NULL); zend_string_release(poll_name); ZEND_ASSERT(poll_fn != NULL); - - FG(io_hooks_poll_fcc) = (zend_fcall_info_cache){ + php_data->poll_fcc = (zend_fcall_info_cache){ .function_handler = poll_fn, - .object = obj, - .called_scope = obj->ce, + .object = obj, + .called_scope = obj->ce, }; GC_ADDREF(obj); @@ -150,11 +233,10 @@ PHP_FUNCTION(Io_Hooks_set_hooks) zend_function *poll_multi_fn = obj->handlers->get_method(&obj, poll_multi_name, NULL); zend_string_release(poll_multi_name); ZEND_ASSERT(poll_multi_fn != NULL); - - FG(io_hooks_pollMulti_fcc) = (zend_fcall_info_cache){ + php_data->pollMulti_fcc = (zend_fcall_info_cache){ .function_handler = poll_multi_fn, - .object = obj, - .called_scope = obj->ce, + .object = obj, + .called_scope = obj->ce, }; GC_ADDREF(obj); @@ -162,21 +244,14 @@ PHP_FUNCTION(Io_Hooks_set_hooks) zend_function *sleep_fn = obj->handlers->get_method(&obj, sleep_name, NULL); zend_string_release(sleep_name); ZEND_ASSERT(sleep_fn != NULL); - - FG(io_hooks_sleep_fcc) = (zend_fcall_info_cache){ + php_data->sleep_fcc = (zend_fcall_info_cache){ .function_handler = sleep_fn, - .object = obj, - .called_scope = obj->ce, + .object = obj, + .called_scope = obj->ce, }; GC_ADDREF(obj); -} -PHPAPI void php_io_hooks_sleep(zend_long seconds, zend_long nanoseconds) -{ - zval params[2]; - ZVAL_LONG(¶ms[0], seconds); - ZVAL_LONG(¶ms[1], nanoseconds); - zend_call_known_fcc(&FG(io_hooks_sleep_fcc), NULL, 2, params, NULL); + php_set_io_hooks(&php_io_hooks_php_adapter, sizeof(php_io_hooks), php_data); } PHP_MINIT_FUNCTION(io_hooks) diff --git a/ext/standard/io_hooks.h b/ext/standard/io_hooks.h index dbe330a18408..e4fc2a0cd200 100644 --- a/ext/standard/io_hooks.h +++ b/ext/standard/io_hooks.h @@ -15,19 +15,11 @@ #ifndef PHP_IO_HOOKS_H #define PHP_IO_HOOKS_H -#include "main/php.h" -#include "Zend/zend_types.h" -#include "ext/standard/file.h" +#include "main/hooks/io_hooks.h" PHPAPI extern zend_class_entry *php_io_hooks_poll_info_ce; PHPAPI extern zend_class_entry *php_io_hooks_poll_result_ce; -#define PHP_HAS_IO_POLL_HOOK() ZEND_FCC_INITIALIZED(FG(io_hooks_poll_fcc)) -#define PHP_HAS_IO_SLEEP_HOOK() ZEND_FCC_INITIALIZED(FG(io_hooks_sleep_fcc)) - -PHPAPI zend_object *php_io_hooks_poll_stream(php_stream *stream, int events, const struct timeval *timeout); -PHPAPI void php_io_hooks_sleep(zend_long seconds, zend_long nanoseconds); - PHP_MINIT_FUNCTION(io_hooks); #endif /* PHP_IO_HOOKS_H */ diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c index 9e750a2b34b2..cd5e6eeac6fc 100644 --- a/ext/standard/io_poll.c +++ b/ext/standard/io_poll.c @@ -94,7 +94,7 @@ static uint32_t php_io_poll_event_enum_to_bit(zend_object *event_enum) return 1 << (zend_enum_fetch_case_id(event_enum) - 1); } -static uint32_t php_io_poll_event_enums_to_events(zval *event_enums) +PHPAPI uint32_t php_io_poll_event_enums_to_events(zval *event_enums) { HashTable *ht; uint32_t events = 0; diff --git a/ext/standard/io_poll.h b/ext/standard/io_poll.h index d9e2c0c0371f..90cdd255bae6 100644 --- a/ext/standard/io_poll.h +++ b/ext/standard/io_poll.h @@ -23,6 +23,7 @@ PHPAPI extern zend_class_entry *php_io_poll_handle_class_entry; PHPAPI extern zend_class_entry *php_stream_poll_handle_class_entry; PHPAPI zend_result php_io_poll_events_to_event_enums(uint32_t events, zval *event_enums); +PHPAPI uint32_t php_io_poll_event_enums_to_events(zval *event_enums); PHPAPI void php_stream_poll_handle_from_stream(zval *dest, php_stream *stream); PHPAPI void php_stream_poll_weak_handle_from_stream(zval *dest, php_stream *stream); diff --git a/main/hooks/io_hooks.c b/main/hooks/io_hooks.c new file mode 100644 index 000000000000..b92e1eae332a --- /dev/null +++ b/main/hooks/io_hooks.c @@ -0,0 +1,90 @@ +/* + +----------------------------------------------------------------------+ + | 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 | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "main/php_poll.h" +#include "ext/standard/file.h" +#include "ext/standard/io_poll.h" +#include "main/hooks/io_hooks.h" + +#ifdef HAVE_POLL_H +#include +#elif HAVE_SYS_POLL_H +#include +#endif + +PHPAPI uint32_t php_posix_poll_to_php_poll(int posix_events) +{ + uint32_t result = 0; + if (posix_events & POLLIN) result |= PHP_POLL_READ; + if (posix_events & POLLOUT) result |= PHP_POLL_WRITE; + if (posix_events & POLLERR) result |= PHP_POLL_ERROR; + if (posix_events & POLLHUP) result |= PHP_POLL_HUP; +#ifdef POLLRDHUP + if (posix_events & POLLRDHUP) result |= PHP_POLL_RDHUP; +#endif + /* TODO: poll API doesn't support POLLPRI */ + return result; +} + +PHPAPI void php_set_io_hooks(const php_io_hooks *hooks, size_t size, void *data) +{ + ZEND_ASSERT((!FG(io_hooks).poll && !FG(io_hooks).poll_multi + && !FG(io_hooks).sleep && !FG(io_hooks).dtor) || !hooks); + + if (FG(io_hooks).dtor) { + FG(io_hooks).dtor(FG(io_hooks_data)); + } + + if (hooks == NULL) { + memset(&FG(io_hooks), 0, sizeof(FG(io_hooks))); + FG(io_hooks_data) = NULL; + } else { + ZEND_ASSERT(size <= sizeof(php_io_hooks)); + memcpy(&FG(io_hooks), hooks, size); + memset((char *)&FG(io_hooks) + size, 0, sizeof(php_io_hooks) - size); + FG(io_hooks_data) = data; + } +} + +PHPAPI php_io_hooks_poll_result *php_io_hooks_poll_stream(php_stream *stream, uint32_t events, + const struct timeval *timeout) +{ + zval handle_zv; + php_stream_poll_weak_handle_from_stream(&handle_zv, stream); + + zend_long timeout_ms; + if (timeout == NULL) { + timeout_ms = -1; + } else { + timeout_ms = (zend_long)timeout->tv_sec * 1000 + (zend_long)timeout->tv_usec / 1000; + } + + php_io_hooks_poll_info info = { + .handle = Z_OBJ(handle_zv), + .events = events, + .timeout_ms = timeout_ms, + }; + + ZEND_ASSERT(!(stream->flags & PHP_STREAM_FLAG_BEING_POLLED)); + stream->flags |= PHP_STREAM_FLAG_BEING_POLLED; + php_io_hooks_poll_result *result = FG(io_hooks).poll(FG(io_hooks_data), &info); + stream->flags &= ~PHP_STREAM_FLAG_BEING_POLLED; + + zval_ptr_dtor(&handle_zv); + return result; +} + +PHPAPI void php_io_hooks_sleep(zend_long seconds, zend_long nanoseconds) +{ + FG(io_hooks).sleep(FG(io_hooks_data), seconds, nanoseconds); +} diff --git a/main/hooks/io_hooks.h b/main/hooks/io_hooks.h new file mode 100644 index 000000000000..bef87e6a3484 --- /dev/null +++ b/main/hooks/io_hooks.h @@ -0,0 +1,66 @@ +/* + +----------------------------------------------------------------------+ + | 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 | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_HOOKS_IO_HOOKS_H +#define PHP_HOOKS_IO_HOOKS_H + +#include "php.h" + +/* The handle is always a weak singleton: + * - References the inner stream/socket weakly. The Poll API will automatically + * stop watching this Handle when the inner stream/socket is collected. + * - The instance is always the same for a given stream/socket. + */ +typedef struct _php_io_hooks_poll_info { + zend_object *handle; /* Io\Poll\Handle */ + uint32_t events; /* PHP_POLL_* bitmask */ + zend_long timeout_ms; /* -1 = no timeout */ +} php_io_hooks_poll_info; + +typedef struct _php_io_hooks_poll_result { + zend_object *handle; /* handle that triggered */ + uint32_t events; /* PHP_POLL_* bitmask of triggered events */ + bool timeout; +} php_io_hooks_poll_result; + +typedef struct _php_io_hooks { + /* Called when a single stream/handle needs to be polled. + * Returns NULL on error. */ + php_io_hooks_poll_result *(*poll)(void *data, php_io_hooks_poll_info *info); + + /* Called to poll multiple handles simultaneously. + * Returns NULL on error (EG(exception) is set) or on timeout. */ + php_io_hooks_poll_result *(*poll_multi)(void *data, zend_long timeout_ms, + uint32_t num_infos, php_io_hooks_poll_info *infos); + + /* Called by nanosleep()/usleep()/sleep(). */ + void (*sleep)(void *data, zend_long seconds, zend_long nanoseconds); + + /* Called when hooks are unregistered or replaced. */ + void (*dtor)(void *data); +} php_io_hooks; + +/* Register a set of I/O hooks. Pass NULL to unregister. + * size is sizeof(*hooks) as seen by the caller; trailing unknown fields are zeroed. */ +PHPAPI void php_set_io_hooks(const php_io_hooks *hooks, size_t size, void *data); + +/* Convert POSIX poll(2) events to a PHP_POLL_* bitmask. */ +PHPAPI uint32_t php_posix_poll_to_php_poll(int posix_events); + +/* Poll a single stream; events is a PHP_POLL_* bitmask. */ +PHPAPI php_io_hooks_poll_result *php_io_hooks_poll_stream(struct _php_stream *stream, uint32_t events, + const struct timeval *timeout); + +/* Call the sleep hook. */ +PHPAPI void php_io_hooks_sleep(zend_long seconds, zend_long nanoseconds); + +#endif /* PHP_HOOKS_IO_HOOKS_H */ diff --git a/main/php_network.h b/main/php_network.h index ce5c47330096..d39187ebd891 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -17,7 +17,8 @@ #include #include "zend_enum.h" -#include "ext/standard/io_hooks.h" +#include "main/hooks/io_hooks.h" +#include "ext/standard/file.h" #ifndef PHP_WIN32 # undef closesocket @@ -214,7 +215,6 @@ static inline int php_pollfd_for_ms(php_socket_t fd, int events, int timeout) return n; } -// TODO: C23_ENUM() typedef enum php_pollstream_result { PHP_POLLSTREAM_ERROR = -1, PHP_POLLSTREAM_TIMEOUT = 0, @@ -223,7 +223,7 @@ typedef enum php_pollstream_result { static inline php_pollstream_result php_pollstream_for(php_stream *stream, php_socket_t fd, int events, struct timeval *timeouttv) { - if (!PHP_HAS_IO_POLL_HOOK()) { + if (!FG(io_hooks).poll) { int n = php_pollfd_for(fd, events, timeouttv); if (n > 0) { return PHP_POLLSTREAM_READY; @@ -234,15 +234,16 @@ static inline php_pollstream_result php_pollstream_for(php_stream *stream, php_s return PHP_POLLSTREAM_ERROR; } - zend_object *result = php_io_hooks_poll_stream(stream, events, timeouttv); + php_io_hooks_poll_result *result = php_io_hooks_poll_stream(stream, php_posix_poll_to_php_poll(events), timeouttv); if (result == NULL) { return PHP_POLLSTREAM_ERROR; } - zval rv; - zval *timeout_prop = zend_read_property(php_io_hooks_poll_result_ce, result, "timeout", sizeof("timeout") - 1, /* silent */ 1, &rv); - bool timed_out = zend_is_true(timeout_prop); - OBJ_RELEASE(result); + bool timed_out = result->timeout; + if (result->handle) { + OBJ_RELEASE(result->handle); + } + efree(result); return timed_out ? PHP_POLLSTREAM_TIMEOUT : PHP_POLLSTREAM_READY; } From f57470b6170f44d80bd5eded5dd6ae096249dd0a Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 3 Jul 2026 12:41:27 +0200 Subject: [PATCH 18/18] Update pollMulti() signature --- ext/standard/io_hooks.c | 18 +------------ ext/standard/io_hooks.stub.php | 4 +-- ext/standard/io_hooks_arginfo.h | 4 +-- .../tests/streams/hooks/hook-close-fgets.phpt | 2 +- .../streams/hooks/hook-concurrent-access.phpt | 2 +- .../tests/streams/hooks/scheduler.inc | 25 +++++-------------- 6 files changed, 13 insertions(+), 42 deletions(-) diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c index 0adafcb1870a..5ca4f96be107 100644 --- a/ext/standard/io_hooks.c +++ b/ext/standard/io_hooks.c @@ -101,15 +101,6 @@ static php_io_hooks_poll_result *php_io_hooks_php_poll(void *data, php_io_hooks_ return NULL; } - if (UNEXPECTED(Z_TYPE(retval) != IS_OBJECT || Z_OBJCE(retval) != php_io_hooks_poll_result_ce)) { - zend_type_error("%s::poll() must return %s, %s returned", - ZSTR_VAL(php_io_hooks_hooks_ce->name), - ZSTR_VAL(php_io_hooks_poll_result_ce->name), - zend_zval_type_name(&retval)); - zval_ptr_dtor(&retval); - return NULL; - } - php_io_hooks_poll_result *result = php_poll_result_from_zval(&retval); zval_ptr_dtor(&retval); return result; @@ -146,17 +137,10 @@ static php_io_hooks_poll_result *php_io_hooks_php_poll_multi(void *data, zend_lo return NULL; } - if (Z_TYPE(retval) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL(retval)) == 0) { - zval_ptr_dtor(&retval); + if (Z_TYPE(retval) == IS_NULL) { return NULL; } - zval *first = zend_hash_index_find(Z_ARRVAL(retval), 0); - php_poll_result *result = NULL; - if (first && Z_TYPE_P(first) == IS_OBJECT && Z_OBJCE_P(first) == php_io_hooks_poll_result_ce) { - result = php_poll_result_from_zval(first); - } - php_io_hooks_poll_result *result = php_poll_result_from_zval(&retval); zval_ptr_dtor(&retval); return result; diff --git a/ext/standard/io_hooks.stub.php b/ext/standard/io_hooks.stub.php index e2632d603a4a..9f21431bef8e 100644 --- a/ext/standard/io_hooks.stub.php +++ b/ext/standard/io_hooks.stub.php @@ -22,8 +22,8 @@ final class PollResult { interface Hooks { public function poll(PollInfo $info): PollResult; - /* @return PollResult[] Empty when $timeout_ms is exceeded */ - public function pollMulti(?int $timeout_ms, PollInfo ...$info): array; + /* @return ?PollResult[] NULL when $timeout_ms is exceeded */ + public function pollMulti(?int $timeout_ms, PollInfo ...$info): ?PollResult; public function sleep(int $seconds, int $nanoseconds): void; } diff --git a/ext/standard/io_hooks_arginfo.h b/ext/standard/io_hooks_arginfo.h index 5abee175ac69..b549163fe477 100644 --- a/ext/standard/io_hooks_arginfo.h +++ b/ext/standard/io_hooks_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit io_hooks.stub.php instead. - * Stub hash: 364d7e209716e763806acd085421c11a404683d3 */ + * Stub hash: a6d4bd83077f52bcb197cd2934450a8490a74c80 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_Io_Hooks_set_hooks, 0, 1, Io\\Hooks\\Hooks, 1) ZEND_ARG_OBJ_INFO(0, hooks, Io\\Hooks\\Hooks, 1) @@ -9,7 +9,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Hooks_Hooks_poll, 0, 1, ZEND_ARG_OBJ_INFO(0, info, Io\\Hooks\\PollInfo, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Hooks_Hooks_pollMulti, 0, 1, IS_ARRAY, 0) +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Hooks_Hooks_pollMulti, 0, 1, Io\\Hooks\\PollResult, 1) ZEND_ARG_TYPE_INFO(0, timeout_ms, IS_LONG, 1) ZEND_ARG_VARIADIC_OBJ_INFO(0, info, Io\\Hooks\\PollInfo, 0) ZEND_END_ARG_INFO() diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt index 6ab4919d4b51..825a13044bac 100644 --- a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -18,7 +18,7 @@ class CloseOnceHooks implements Hooks { $result->timeout = false; return $result; } - public function pollMulti(?int $timeout_ms, PollInfo ...$info): array { throw new \Exception("pollMulti not implemented"); } + public function pollMulti(?int $timeout_ms, PollInfo ...$info): ?PollResult { throw new \Exception("pollMulti not implemented"); } public function sleep(int $seconds, int $nanoseconds): void { throw new \Exception("sleep not implemented"); } } diff --git a/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt b/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt index 5efd061e9baf..8459f852486f 100644 --- a/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt +++ b/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt @@ -22,7 +22,7 @@ class ConcurrentHook implements Hooks { $result->timeout = false; return $result; } - public function pollMulti(?int $timeout_ms, PollInfo ...$info): array { + public function pollMulti(?int $timeout_ms, PollInfo ...$info): ?PollResult { throw new \Exception("pollMulti not implemented"); } public function sleep(int $seconds, int $nanoseconds): void { throw new \Exception("sleep not implemented"); } diff --git a/ext/standard/tests/streams/hooks/scheduler.inc b/ext/standard/tests/streams/hooks/scheduler.inc index 38a9e8200595..75e08480c05f 100644 --- a/ext/standard/tests/streams/hooks/scheduler.inc +++ b/ext/standard/tests/streams/hooks/scheduler.inc @@ -12,7 +12,6 @@ class Scheduler implements Hooks private array $pendingByHandle = []; // handleId -> [Event, ...] (fired, not yet consumed) private array $registrationsByFiber = []; // fiberId -> [handleId -> [eventName, ...]] private array $fiberDeadlines = []; // fiberId -> [deadline_ns, fiber, PollInfo|null] - private array $fiberIsMulti = []; // fiberId -> true public function __construct() { @@ -118,12 +117,7 @@ class Scheduler implements Hooks $this->clearFiberRegistrations($fiberId); unset($this->fiberDeadlines[$fiberId]); - if ($this->fiberIsMulti[$fiberId] ?? false) { - unset($this->fiberIsMulti[$fiberId]); - $this->ready[] = [$fiber, [$result]]; - } else { - $this->ready[] = [$fiber, $result]; - } + $this->ready[] = [$fiber, $result]; } // Handle expired deadlines @@ -135,11 +129,8 @@ class Scheduler implements Hooks $this->clearFiberRegistrations($fiberId); unset($this->fiberDeadlines[$fiberId]); - if ($this->fiberIsMulti[$fiberId] ?? false) { - unset($this->fiberIsMulti[$fiberId]); - $this->ready[] = [$fiber, []]; - } elseif ($info === null) { - // sleep deadline: resume with no value + if ($info === null) { + // sleep or pollMulti deadline: resume with no value $this->ready[] = [$fiber, null]; } else { $result = new PollResult(); @@ -247,18 +238,16 @@ class Scheduler implements Hooks return $result; } - public function pollMulti(?int $timeout_ms, PollInfo ...$infos): array + public function pollMulti(?int $timeout_ms, PollInfo ...$infos): ?PollResult { $fiber = Fiber::getCurrent(); $fiberId = spl_object_id($fiber); - $this->fiberIsMulti[$fiberId] = true; foreach ($infos as $info) { $id = spl_object_id($info->handle); $result = $this->consumePending($id, $info->handle, $info->events); if ($result !== null) { - unset($this->fiberIsMulti[$fiberId]); - return [$result]; + return $result; } } @@ -277,9 +266,7 @@ class Scheduler implements Hooks $this->addOrModifyWatcher($id, $info->handle, $info->events); } - /** @var PollResult[] $results */ - $results = Fiber::suspend(); - return $results ?? []; + return Fiber::suspend(); } public function sleep(int $seconds, int $nanoseconds): void