Add emscripten_epoll_add_listener for epoll readiness callbacks - #27547
Open
guybedford wants to merge 7 commits into
Open
Add emscripten_epoll_add_listener for epoll readiness callbacks#27547guybedford wants to merge 7 commits into
guybedford wants to merge 7 commits into
Conversation
Add epoll_create1/epoll_ctl/epoll_wait/epoll_pwait on the legacy (non-WASMFS) JS syscall layer, built on the per-inode readiness wait-queue: level- and edge-triggered modes, EPOLLONESHOT, EPOLLEXCLUSIVE, EPOLLRDHUP, nesting, dup-shared epoll instances, and blocking waits under PROXY_TO_PTHREAD, ASYNCIFY, and JSPI.
A non-blocking readiness delivery mechanism for epoll: instead of blocking in epoll_wait, the runtime invokes registered listener callbacks on the event loop whenever the epoll set has ready events waiting to be collected (new experimental <emscripten/epoll.h>), working without ASYNCIFY/JSPI. A callback takes only its userdata and collects events itself via a zero-timeout epoll_wait(epfd, ..., 0). Firing is gated on the shared readiness derivation ($epollDerive) also used by the epoll fd's own poll handler, so a stale ready-list entry never spuriously fires. Per-fd trigger modes apply exactly as in epoll_wait: a level fd left undrained re-fires every tick, an edge fd once per edge, a fired EPOLLONESHOT not until re-armed. Any number of listeners may be added, keyed by (callback, registering thread); re-adding the same identity updates userdata. Every listener is signalled while uncollected ready events remain (broadcast) and collectors race over the single shared ready list, so EPOLLET/EPOLLONESHOT items are collected by exactly one listener - the same load balancing as between multiple blocking epoll_wait callers on one epoll. Listeners hold a runtime keepalive while the set can still fire, keyed on the armed-registration count (a fired EPOLLONESHOT no longer counts): registered I/O interest holds the event loop open, following the Node.js model, and a terminal set (every watched fd closed) releases the runtime with no explicit disposal needed. Listeners are instance state shared across dup'd fds; the last close removes them all. Under pthreads the registration body runs sync-proxied on the main thread, so the registering thread is captured and each delivery is back-proxied to it via emscripten_proxy_callback (new system/lib/pthread/emscripten_epoll_callback.c), one delivery in flight at a time, paced by its completion (_emscripten_epoll_delivery_done) with a monotonic token dropping stale completions. While armed, each listener also holds its owner thread's keepalive so it survives to receive deliveries.
guybedford
force-pushed
the
epoll-callback
branch
from
August 15, 2026 05:14
3b9ee9a to
1feb245
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-on to #27207 implementing the non-blocking readiness callback model for epoll, split out of that PR per review feedback. (Includes #27207 as its base; the callback model is the final commit.)
emscripten_epoll_add_listener(new experimental<emscripten/epoll.h>) registers a persistent readiness listener on an epoll fd: the runtime invokes the callback on the event loop whenever the set has uncollected ready events, and the callback collects them itself via a zero-timeoutepoll_wait(epfd, ..., 0). It never suspends the stack, so event-loop reactors can drive epoll without ASYNCIFY/JSPI.emscripten_epoll_remove_listenerremoves by identity.EPOLLET/EPOLLONESHOTitems are collected by exactly one listener - the same distribution semantics as between multiple blockingepoll_waitcallers on one epoll.Tests cover delivery and MOD re-arm with no producer event, edge and level modes on the callback path, multi-listener broadcast/load-balancing, registration identity and errors, drain-across-ticks with maxevents=1, nesting, dup sharing, terminal-set runtime release (including nested), a suspended blocking epoll_wait and a listener sharing one ready list (ASYNCIFY and JSPI), and real-socket delivery including PROXY_TO_PTHREAD cross-thread dispatch.
Made with AI assistance under my review