@@ -25,6 +25,7 @@ typedef struct {
2525 struct kevent * events ;
2626 int events_capacity ;
2727 int fd_count ; /* Track number of unique FDs (not individual filters) */
28+ int filter_count ; /* Track total number of filters for raw events */
2829 HashTable * complete_oneshot_fds ; /* Track FDs with both read+write oneshot */
2930 HashTable * garbage_oneshot_fds ; /* Pre-cached hash table for FDs to delete */
3031} kqueue_backend_data_t ;
@@ -56,12 +57,18 @@ static zend_result kqueue_backend_init(php_poll_ctx *ctx)
5657 }
5758 data -> events_capacity = initial_capacity ;
5859 data -> fd_count = 0 ; /* Initialize FD counter */
59-
60- /* Initialize oneshot related hash tables */
61- data -> complete_oneshot_fds = php_poll_malloc (sizeof (HashTable ), ctx -> persistent );
62- zend_hash_init (data -> complete_oneshot_fds , 8 , NULL , NULL , ctx -> persistent );
63- data -> garbage_oneshot_fds = php_poll_malloc (sizeof (HashTable ), ctx -> persistent );
64- zend_hash_init (data -> garbage_oneshot_fds , 8 , NULL , NULL , ctx -> persistent );
60+ data -> filter_count = 0 ; /* Initialize filter counter */
61+
62+ /* Only initialize oneshot related hash tables if not using raw events */
63+ if (!ctx -> raw_events ) {
64+ data -> complete_oneshot_fds = php_poll_malloc (sizeof (HashTable ), ctx -> persistent );
65+ zend_hash_init (data -> complete_oneshot_fds , 8 , NULL , NULL , ctx -> persistent );
66+ data -> garbage_oneshot_fds = php_poll_malloc (sizeof (HashTable ), ctx -> persistent );
67+ zend_hash_init (data -> garbage_oneshot_fds , 8 , NULL , NULL , ctx -> persistent );
68+ } else {
69+ data -> complete_oneshot_fds = NULL ;
70+ data -> garbage_oneshot_fds = NULL ;
71+ }
6572
6673 ctx -> backend_data = data ;
6774 return SUCCESS ;
@@ -75,10 +82,17 @@ static void kqueue_backend_cleanup(php_poll_ctx *ctx)
7582 close (data -> kqueue_fd );
7683 }
7784 pefree (data -> events , ctx -> persistent );
78- zend_hash_destroy (data -> complete_oneshot_fds );
79- pefree (data -> complete_oneshot_fds , ctx -> persistent );
80- zend_hash_destroy (data -> garbage_oneshot_fds );
81- pefree (data -> garbage_oneshot_fds , ctx -> persistent );
85+
86+ /* Only cleanup hash tables if they were initialized */
87+ if (data -> complete_oneshot_fds ) {
88+ zend_hash_destroy (data -> complete_oneshot_fds );
89+ pefree (data -> complete_oneshot_fds , ctx -> persistent );
90+ }
91+ if (data -> garbage_oneshot_fds ) {
92+ zend_hash_destroy (data -> garbage_oneshot_fds );
93+ pefree (data -> garbage_oneshot_fds , ctx -> persistent );
94+ }
95+
8296 pefree (data , ctx -> persistent );
8397 ctx -> backend_data = NULL ;
8498 }
@@ -118,10 +132,13 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events
118132
119133 /* Increment FD count only once per unique FD */
120134 backend_data -> fd_count ++ ;
135+ /* Increment filter count by number of filters added */
136+ backend_data -> filter_count += change_count ;
121137
122- /* Track if this FD has both read+write oneshot */
123- if ((events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT ))
124- == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT )) {
138+ /* Track oneshot only if not using raw events */
139+ if (!ctx -> raw_events
140+ && (events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT ))
141+ == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT )) {
125142 zend_hash_index_add_empty_element (backend_data -> complete_oneshot_fds , fd );
126143 }
127144 }
@@ -188,21 +205,30 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve
188205 }
189206 }
190207
191- /* Update FD count and oneshot tracking */
208+ /* Update counters and oneshot tracking */
192209 if (successful_deletes > 0 && add_count == 0 ) {
193210 /* Removed all filters - FD is gone */
194211 backend_data -> fd_count -- ;
195- zend_hash_index_del (backend_data -> complete_oneshot_fds , fd );
212+ backend_data -> filter_count -= successful_deletes ;
213+ if (!ctx -> raw_events ) {
214+ zend_hash_index_del (backend_data -> complete_oneshot_fds , fd );
215+ }
196216 } else if (successful_deletes == 0 && add_count > 0 ) {
197217 /* Added filters to previously empty FD */
198218 backend_data -> fd_count ++ ;
199- if ((events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT ))
200- == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT )) {
219+ backend_data -> filter_count += add_count ;
220+ if (!ctx -> raw_events
221+ && (events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT ))
222+ == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT )) {
201223 zend_hash_index_add_empty_element (backend_data -> complete_oneshot_fds , fd );
202224 }
203225 } else if (successful_deletes > 0 || add_count > 0 ) {
204- /* One of the filter was deleted so remove from oneshot tracking */
205- zend_hash_index_del (backend_data -> complete_oneshot_fds , fd );
226+ /* Mixed operation - update filter count */
227+ backend_data -> filter_count = backend_data -> filter_count - successful_deletes + add_count ;
228+ if (!ctx -> raw_events ) {
229+ /* One of the filters was deleted so remove from oneshot tracking */
230+ zend_hash_index_del (backend_data -> complete_oneshot_fds , fd );
231+ }
206232 }
207233
208234 return SUCCESS ;
@@ -240,11 +266,14 @@ static zend_result kqueue_backend_remove(php_poll_ctx *ctx, int fd)
240266 return FAILURE ;
241267 }
242268
243- /* Update FD count - we removed all filters for this FD */
269+ /* Update counters - we removed all filters for this FD */
244270 backend_data -> fd_count -- ;
271+ backend_data -> filter_count -= successful_deletes ;
245272
246- /* Remove from complete oneshot tracking */
247- zend_hash_index_del (backend_data -> complete_oneshot_fds , fd );
273+ /* Remove from complete oneshot tracking if not using raw events */
274+ if (!ctx -> raw_events ) {
275+ zend_hash_index_del (backend_data -> complete_oneshot_fds , fd );
276+ }
248277
249278 return SUCCESS ;
250279}
@@ -254,9 +283,9 @@ static int kqueue_backend_wait(
254283{
255284 kqueue_backend_data_t * backend_data = (kqueue_backend_data_t * ) ctx -> backend_data ;
256285
257- /* Ensure we have enough space for the requested events as kqueue can return up to 2 raw events
258- * per FD (read + write), we need capacity for potentially 2x max_events . */
259- int required_capacity = max_events * 2 ;
286+ /* For raw events, we need capacity for max_events.
287+ * For grouped events, kqueue can return up to 2 events per FD, so we need 2x capacity . */
288+ int required_capacity = ctx -> raw_events ? max_events : ( max_events * 2 ) ;
260289 if (required_capacity > backend_data -> events_capacity ) {
261290 struct kevent * new_events = php_poll_realloc (
262291 backend_data -> events , required_capacity * sizeof (struct kevent ), ctx -> persistent );
@@ -279,74 +308,102 @@ static int kqueue_backend_wait(
279308 backend_data -> kqueue_fd , NULL , 0 , backend_data -> events , required_capacity , tsp );
280309
281310 if (nfds > 0 ) {
282- /* Group events by FD and combine read/write events */
283- int unique_events = 0 , fd ;
284- zend_hash_clean (backend_data -> garbage_oneshot_fds );
285-
286- for (int i = 0 ; i < nfds ; i ++ ) {
287- fd = (int ) backend_data -> events [i ].ident ;
288- uint32_t revents = 0 ;
289- void * data = backend_data -> events [i ].udata ;
290- bool is_oneshot = (backend_data -> events [i ].flags & EV_ONESHOT ) != 0 ;
291-
292- /* Convert this event */
293- if (backend_data -> events [i ].filter == EVFILT_READ ) {
294- revents |= PHP_POLL_READ ;
295- } else if (backend_data -> events [i ].filter == EVFILT_WRITE ) {
296- revents |= PHP_POLL_WRITE ;
297- }
311+ if (ctx -> raw_events ) {
312+ /* Raw events mode - direct 1:1 mapping, no grouping */
313+ for (int i = 0 ; i < nfds && i < max_events ; i ++ ) {
314+ events [i ].fd = (int ) backend_data -> events [i ].ident ;
315+ events [i ].events = 0 ; /* Not used in raw mode */
316+ events [i ].revents = 0 ;
317+ events [i ].data = backend_data -> events [i ].udata ;
318+
319+ /* Convert kqueue filter to poll event */
320+ if (backend_data -> events [i ].filter == EVFILT_READ ) {
321+ events [i ].revents |= PHP_POLL_READ ;
322+ } else if (backend_data -> events [i ].filter == EVFILT_WRITE ) {
323+ events [i ].revents |= PHP_POLL_WRITE ;
324+ }
298325
299- if (backend_data -> events [i ].flags & EV_EOF ) {
300- revents |= PHP_POLL_HUP ;
301- }
302- if (backend_data -> events [i ].flags & EV_ERROR ) {
303- revents |= PHP_POLL_ERROR ;
326+ /* Convert kqueue flags to poll events */
327+ if (backend_data -> events [i ].flags & EV_EOF ) {
328+ events [i ].revents |= PHP_POLL_HUP ;
329+ }
330+ if (backend_data -> events [i ].flags & EV_ERROR ) {
331+ events [i ].revents |= PHP_POLL_ERROR ;
332+ }
304333 }
334+ /* In raw mode, we might return fewer events than nfds if max_events < nfds */
335+ return nfds > max_events ? max_events : nfds ;
336+ } else {
337+ /* Grouped events mode - existing complex logic */
338+ int unique_events = 0 , fd ;
339+ zend_hash_clean (backend_data -> garbage_oneshot_fds );
340+
341+ for (int i = 0 ; i < nfds ; i ++ ) {
342+ fd = (int ) backend_data -> events [i ].ident ;
343+ uint32_t revents = 0 ;
344+ void * data = backend_data -> events [i ].udata ;
345+ bool is_oneshot = (backend_data -> events [i ].flags & EV_ONESHOT ) != 0 ;
346+
347+ /* Convert this event */
348+ if (backend_data -> events [i ].filter == EVFILT_READ ) {
349+ revents |= PHP_POLL_READ ;
350+ } else if (backend_data -> events [i ].filter == EVFILT_WRITE ) {
351+ revents |= PHP_POLL_WRITE ;
352+ }
305353
306- /* Look for existing event for this FD */
307- bool found = false;
308- for (int j = 0 ; j < unique_events ; j ++ ) {
309- if (events [j ].fd == fd ) {
310- /* Combine with existing event */
311- events [j ].revents |= revents ;
312- found = true;
313- break ;
354+ if (backend_data -> events [i ].flags & EV_EOF ) {
355+ revents |= PHP_POLL_HUP ;
356+ }
357+ if (backend_data -> events [i ].flags & EV_ERROR ) {
358+ revents |= PHP_POLL_ERROR ;
359+ }
360+
361+ /* Look for existing event for this FD */
362+ bool found = false;
363+ for (int j = 0 ; j < unique_events ; j ++ ) {
364+ if (events [j ].fd == fd ) {
365+ /* Combine with existing event */
366+ events [j ].revents |= revents ;
367+ found = true;
368+ break ;
369+ }
314370 }
315- }
316371
317- if (!found ) {
318- /* New FD, create new event */
319- ZEND_ASSERT (unique_events < max_events );
320- events [unique_events ].fd = fd ;
321- events [unique_events ].events = 0 ;
322- events [unique_events ].revents = revents ;
323- events [unique_events ].data = data ;
324- unique_events ++ ;
325-
326- if (is_oneshot && zend_hash_index_exists (backend_data -> complete_oneshot_fds , fd )) {
327- zval dummy ;
328- ZVAL_BOOL (& dummy , revents & PHP_POLL_READ );
329- zend_hash_index_add (backend_data -> garbage_oneshot_fds , fd , & dummy );
330- zend_hash_index_del (backend_data -> complete_oneshot_fds , fd );
331- backend_data -> fd_count -- ;
372+ if (!found ) {
373+ /* New FD, create new event */
374+ ZEND_ASSERT (unique_events < max_events );
375+ events [unique_events ].fd = fd ;
376+ events [unique_events ].events = 0 ;
377+ events [unique_events ].revents = revents ;
378+ events [unique_events ].data = data ;
379+ unique_events ++ ;
380+
381+ if (is_oneshot
382+ && zend_hash_index_exists (backend_data -> complete_oneshot_fds , fd )) {
383+ zval dummy ;
384+ ZVAL_BOOL (& dummy , revents & PHP_POLL_READ );
385+ zend_hash_index_add (backend_data -> garbage_oneshot_fds , fd , & dummy );
386+ zend_hash_index_del (backend_data -> complete_oneshot_fds , fd );
387+ backend_data -> fd_count -- ;
388+ }
389+ } else if (is_oneshot ) {
390+ zend_hash_index_del (backend_data -> garbage_oneshot_fds , fd );
332391 }
333- } else if (is_oneshot ) {
334- zend_hash_index_del (backend_data -> garbage_oneshot_fds , fd );
335392 }
336- }
337393
338- /* Clean up all the same FD filters for other read or write side */
339- zval * item ;
340- struct kevent cleanup_change ;
341- ZEND_HASH_FOREACH_NUM_KEY_VAL (backend_data -> garbage_oneshot_fds , fd , item )
342- {
343- int filter = Z_TYPE_P (item ) == IS_TRUE ? EVFILT_WRITE : EVFILT_READ ;
344- EV_SET (& cleanup_change , fd , filter , EV_DELETE , 0 , 0 , NULL );
345- kevent (backend_data -> kqueue_fd , & cleanup_change , 1 , NULL , 0 , NULL );
346- }
347- ZEND_HASH_FOREACH_END ();
394+ /* Clean up all the same FD filters for other read or write side */
395+ zval * item ;
396+ struct kevent cleanup_change ;
397+ ZEND_HASH_FOREACH_NUM_KEY_VAL (backend_data -> garbage_oneshot_fds , fd , item )
398+ {
399+ int filter = Z_TYPE_P (item ) == IS_TRUE ? EVFILT_WRITE : EVFILT_READ ;
400+ EV_SET (& cleanup_change , fd , filter , EV_DELETE , 0 , 0 , NULL );
401+ kevent (backend_data -> kqueue_fd , & cleanup_change , 1 , NULL , 0 , NULL );
402+ }
403+ ZEND_HASH_FOREACH_END ();
348404
349- return unique_events ;
405+ return unique_events ;
406+ }
350407 }
351408
352409 return nfds ;
@@ -370,16 +427,15 @@ static int kqueue_backend_get_suitable_max_events(php_poll_ctx *ctx)
370427 return -1 ;
371428 }
372429
373- /* For kqueue, we now track exactly how many unique FDs are registered */
374- int active_fds = backend_data -> fd_count ;
375-
376- if (active_fds == 0 ) {
377- return 1 ;
430+ if (ctx -> raw_events ) {
431+ /* For raw events, return the total number of filters */
432+ int active_filters = backend_data -> filter_count ;
433+ return active_filters == 0 ? 1 : active_filters ;
434+ } else {
435+ /* For grouped events, return the number of unique FDs */
436+ int active_fds = backend_data -> fd_count ;
437+ return active_fds == 0 ? 1 : active_fds ;
378438 }
379-
380- /* Kqueue backend will return one grouped event per FD (like epoll),
381- * so the suitable max_events is exactly the number of registered FDs */
382- return active_fds ;
383439}
384440
385441const php_poll_backend_ops php_poll_backend_kqueue_ops = {
0 commit comments