2020#include <sys/event.h>
2121#include <sys/time.h>
2222
23+ /* Flags for tracking FD state in single hash table */
24+ #define KQUEUE_FD_PRESENT (1 << 0) /* FD is registered */
25+ #define KQUEUE_FD_ONESHOT_COMPLETE (1 << 1) /* Has both read+write oneshot */
26+ #define KQUEUE_FD_GARBAGE_READ (1 << 2) /* Read filter fired, needs write cleanup */
27+ #define KQUEUE_FD_GARBAGE_WRITE (1 << 3) /* Write filter fired, needs read cleanup */
28+ #define KQUEUE_FD_HAS_GARBAGE (KQUEUE_FD_GARBAGE_READ | KQUEUE_FD_GARBAGE_WRITE)
29+
2330typedef struct {
2431 int kqueue_fd ;
2532 struct kevent * events ;
2633 int events_capacity ;
2734 int fd_count ; /* Track number of unique FDs (not individual filters) */
2835 int filter_count ; /* Track total number of filters for raw events */
29- HashTable * complete_oneshot_fds ; /* Track FDs with both read+write oneshot */
30- HashTable * garbage_oneshot_fds ; /* Pre-cached hash table for FDs to delete */
36+ HashTable * fd_tracking ; /* Single hash table for all FD state tracking */
3137} kqueue_backend_data_t ;
3238
3339static zend_result kqueue_backend_init (php_poll_ctx * ctx )
@@ -56,18 +62,15 @@ static zend_result kqueue_backend_init(php_poll_ctx *ctx)
5662 return FAILURE ;
5763 }
5864 data -> events_capacity = initial_capacity ;
59- data -> fd_count = 0 ; /* Initialize FD counter */
60- data -> filter_count = 0 ; /* Initialize filter counter */
65+ data -> fd_count = 0 ;
66+ data -> filter_count = 0 ;
6167
62- /* Only initialize oneshot related hash tables if not using raw events */
68+ /* Initialize single tracking hash table (only if not using raw events) */
6369 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 );
70+ data -> fd_tracking = php_poll_malloc (sizeof (HashTable ), ctx -> persistent );
71+ zend_hash_init (data -> fd_tracking , 8 , NULL , NULL , ctx -> persistent );
6872 } else {
69- data -> complete_oneshot_fds = NULL ;
70- data -> garbage_oneshot_fds = NULL ;
73+ data -> fd_tracking = NULL ;
7174 }
7275
7376 ctx -> backend_data = data ;
@@ -83,14 +86,10 @@ static void kqueue_backend_cleanup(php_poll_ctx *ctx)
8386 }
8487 pefree (data -> events , ctx -> persistent );
8588
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 );
89+ /* Cleanup tracking hash table if initialized */
90+ if (data -> fd_tracking ) {
91+ zend_hash_destroy (data -> fd_tracking );
92+ pefree (data -> fd_tracking , ctx -> persistent );
9493 }
9594
9695 pefree (data , ctx -> persistent );
@@ -102,6 +101,15 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events
102101{
103102 kqueue_backend_data_t * backend_data = (kqueue_backend_data_t * ) ctx -> backend_data ;
104103
104+ /* Check for duplicate in non-raw mode */
105+ if (!ctx -> raw_events ) {
106+ zval * existing = zend_hash_index_find (backend_data -> fd_tracking , fd );
107+ if (existing && (Z_LVAL_P (existing ) & KQUEUE_FD_PRESENT )) {
108+ php_poll_set_error (ctx , PHP_POLL_ERR_EXISTS );
109+ return FAILURE ;
110+ }
111+ }
112+
105113 struct kevent changes [2 ]; /* Max 2 changes: read + write */
106114 int change_count = 0 ;
107115
@@ -130,16 +138,23 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events
130138 return FAILURE ;
131139 }
132140
133- /* Increment FD count only once per unique FD */
141+ /* Increment counters */
134142 backend_data -> fd_count ++ ;
135- /* Increment filter count by number of filters added */
136143 backend_data -> filter_count += change_count ;
137144
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 )) {
142- zend_hash_index_add_empty_element (backend_data -> complete_oneshot_fds , fd );
145+ /* Track FD state in non-raw mode */
146+ if (!ctx -> raw_events ) {
147+ zend_long tracking_flags = KQUEUE_FD_PRESENT ;
148+
149+ /* Mark as complete oneshot if both read+write with oneshot */
150+ if ((events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT ))
151+ == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT )) {
152+ tracking_flags |= KQUEUE_FD_ONESHOT_COMPLETE ;
153+ }
154+
155+ zval tracking_zval ;
156+ ZVAL_LONG (& tracking_zval , tracking_flags );
157+ zend_hash_index_update (backend_data -> fd_tracking , fd , & tracking_zval );
143158 }
144159 }
145160
@@ -205,29 +220,33 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve
205220 }
206221 }
207222
208- /* Update counters and oneshot tracking */
223+ /* Update counters and tracking */
209224 if (successful_deletes > 0 && add_count == 0 ) {
210225 /* Removed all filters - FD is gone */
211226 backend_data -> fd_count -- ;
212227 backend_data -> filter_count -= successful_deletes ;
213228 if (!ctx -> raw_events ) {
214- zend_hash_index_del (backend_data -> complete_oneshot_fds , fd );
229+ zend_hash_index_del (backend_data -> fd_tracking , fd );
215230 }
216- } else if (successful_deletes == 0 && add_count > 0 ) {
217- /* Added filters to previously empty FD */
218- backend_data -> fd_count ++ ;
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 )) {
223- zend_hash_index_add_empty_element ( backend_data -> complete_oneshot_fds , fd ) ;
231+ } else if (add_count > 0 ) {
232+ if ( successful_deletes == 0 ) {
233+ /* Added filters to previously empty FD */
234+ backend_data -> fd_count ++ ;
235+ backend_data -> filter_count += add_count ;
236+ } else {
237+ /* Mixed operation when successful_deletes > 0 - update filter count */
238+ backend_data -> filter_count = backend_data -> filter_count - successful_deletes + add_count ;
224239 }
225- } else if (successful_deletes > 0 || add_count > 0 ) {
226- /* Mixed operation - update filter count */
227- backend_data -> filter_count = backend_data -> filter_count - successful_deletes + add_count ;
240+
228241 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 );
242+ zend_long tracking_flags = KQUEUE_FD_PRESENT ;
243+ if ((events & (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT ))
244+ == (PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ONESHOT )) {
245+ tracking_flags |= KQUEUE_FD_ONESHOT_COMPLETE ;
246+ }
247+ zval tracking_zval ;
248+ ZVAL_LONG (& tracking_zval , tracking_flags );
249+ zend_hash_index_update (backend_data -> fd_tracking , fd , & tracking_zval );
231250 }
232251 }
233252
@@ -266,13 +285,13 @@ static zend_result kqueue_backend_remove(php_poll_ctx *ctx, int fd)
266285 return FAILURE ;
267286 }
268287
269- /* Update counters - we removed all filters for this FD */
288+ /* Update counters */
270289 backend_data -> fd_count -- ;
271290 backend_data -> filter_count -= successful_deletes ;
272291
273- /* Remove from complete oneshot tracking if not using raw events */
292+ /* Remove from tracking */
274293 if (!ctx -> raw_events ) {
275- zend_hash_index_del (backend_data -> complete_oneshot_fds , fd );
294+ zend_hash_index_del (backend_data -> fd_tracking , fd );
276295 }
277296
278297 return SUCCESS ;
@@ -312,7 +331,7 @@ static int kqueue_backend_wait(
312331 /* Raw events mode - direct 1:1 mapping, no grouping */
313332 for (int i = 0 ; i < nfds && i < max_events ; i ++ ) {
314333 events [i ].fd = (int ) backend_data -> events [i ].ident ;
315- events [i ].events = 0 ; /* Not used in raw mode */
334+ events [i ].events = 0 ;
316335 events [i ].revents = 0 ;
317336 events [i ].data = backend_data -> events [i ].udata ;
318337
@@ -331,12 +350,10 @@ static int kqueue_backend_wait(
331350 events [i ].revents |= PHP_POLL_ERROR ;
332351 }
333352 }
334- /* In raw mode, we might return fewer events than nfds if max_events < nfds */
335353 return nfds > max_events ? max_events : nfds ;
336354 } else {
337- /* Grouped events mode - existing complex logic */
338- int unique_events = 0 , fd ;
339- zend_hash_clean (backend_data -> garbage_oneshot_fds );
355+ /* Grouped events mode with improved oneshot tracking */
356+ int unique_events = 0 , garbage_events = 0 , fd ;
340357
341358 for (int i = 0 ; i < nfds ; i ++ ) {
342359 fd = (int ) backend_data -> events [i ].ident ;
@@ -378,29 +395,54 @@ static int kqueue_backend_wait(
378395 events [unique_events ].data = data ;
379396 unique_events ++ ;
380397
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 -- ;
398+ /* Handle oneshot tracking */
399+ if (is_oneshot ) {
400+ zval * tracking = zend_hash_index_find (backend_data -> fd_tracking , fd );
401+ if (tracking && (Z_LVAL_P (tracking ) & KQUEUE_FD_ONESHOT_COMPLETE )) {
402+ /* Mark which filter fired for garbage collection */
403+ zend_long flags = Z_LVAL_P (tracking );
404+ flags &= ~KQUEUE_FD_ONESHOT_COMPLETE ; /* Clear complete flag */
405+ if (revents & PHP_POLL_READ ) {
406+ flags |= KQUEUE_FD_GARBAGE_READ ; /* Need to clean write */
407+ }
408+ if (revents & PHP_POLL_WRITE ) {
409+ flags |= KQUEUE_FD_GARBAGE_WRITE ; /* Need to clean read */
410+ }
411+ ZVAL_LONG (tracking , flags );
412+ backend_data -> fd_count -- ;
413+ garbage_events ++ ;
414+ }
388415 }
389416 } else if (is_oneshot ) {
390- zend_hash_index_del (backend_data -> garbage_oneshot_fds , fd );
417+ /* Second filter for same FD fired - clear garbage flags */
418+ zval * tracking = zend_hash_index_find (backend_data -> fd_tracking , fd );
419+ if (tracking ) {
420+ /* Remove FD from tracking as it gets deleted from kqueue as well */
421+ zend_hash_index_del (backend_data -> fd_tracking , fd );
422+ garbage_events -- ;
423+ }
391424 }
392425 }
393426
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 );
427+ if (garbage_events > 0 ) {
428+ /* Clean up orphaned filters from complete oneshot FDs */
429+ zend_ulong fd_key ;
430+ zval * tracking ;
431+ struct kevent cleanup_change ;
432+ ZEND_HASH_FOREACH_NUM_KEY_VAL (backend_data -> fd_tracking , fd_key , tracking )
433+ {
434+ zend_long flags = Z_LVAL_P (tracking );
435+ if (flags & KQUEUE_FD_HAS_GARBAGE ) {
436+ int filter = (flags & KQUEUE_FD_GARBAGE_READ ) ? EVFILT_WRITE : EVFILT_READ ;
437+ EV_SET (& cleanup_change , fd_key , filter , EV_DELETE , 0 , 0 , NULL );
438+ kevent (backend_data -> kqueue_fd , & cleanup_change , 1 , NULL , 0 , NULL );
439+
440+ /* Remove FD from tracking after cleanup */
441+ zend_hash_index_del (backend_data -> fd_tracking , fd_key );
442+ }
443+ }
444+ ZEND_HASH_FOREACH_END ();
402445 }
403- ZEND_HASH_FOREACH_END ();
404446
405447 return unique_events ;
406448 }
@@ -449,7 +491,8 @@ const php_poll_backend_ops php_poll_backend_kqueue_ops = {
449491 .wait = kqueue_backend_wait ,
450492 .is_available = kqueue_backend_is_available ,
451493 .get_suitable_max_events = kqueue_backend_get_suitable_max_events ,
452- .supports_et = true /* kqueue supports EV_CLEAR for edge triggering */
494+ .supports_et = true
453495};
454496
497+
455498#endif /* HAVE_KQUEUE */
0 commit comments