Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ See docs/process.md for more on how version tagging works.
(`JS_BIGINT_INTEGRATION`) are universally available across all supported
engines, and removes legacy JS polyfills and Binaryen lowering passes.
(#27542)
- Added support for `epoll` (`epoll_create1`/`epoll_ctl`/`epoll_wait`/
`epoll_pwait`) on the legacy (non-WASMFS) JS filesystem, including
level- and edge-triggered modes, `EPOLLONESHOT`, `EPOLLEXCLUSIVE`,
`EPOLLRDHUP`, nesting, and blocking waits under `PROXY_TO_PTHREAD`,
`ASYNCIFY`, and `JSPI`. (#27207)
- Added `emscripten_epoll_add_listener`/`emscripten_epoll_remove_listener` (in
the new `<emscripten/epoll.h>`, experimental), a non-blocking variant of
`epoll_wait` that signals an epoll set's readiness to listener callbacks
(which collect the events themselves via a zero-timeout `epoll_wait`) with no
`ASYNCIFY`/`JSPI` requirement.

6.0.6 - 08/05/26
----------------
Expand Down
571 changes: 571 additions & 0 deletions src/lib/libepoll.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ sigs = {
__syscall_epoll_create1__sig: 'ii',
__syscall_epoll_ctl__sig: 'iiiip',
__syscall_epoll_pwait__sig: 'iipiipp',
__syscall_epoll_pwait_nonblocking__sig: 'iipi',
__syscall_faccessat__sig: 'iipii',
__syscall_fadvise64__sig: 'iijji',
__syscall_fallocate__sig: 'iiijj',
Expand Down Expand Up @@ -329,6 +330,7 @@ sigs = {
_emscripten_create_wasm_worker__sig: 'iipip',
_emscripten_dlopen_js__sig: 'vpppp',
_emscripten_dlsync_threads__sig: 'v',
_emscripten_epoll_delivery_done__sig: 'vi',
_emscripten_fetch_get_response_headers__sig: 'pipp',
_emscripten_fetch_get_response_headers_length__sig: 'pi',
_emscripten_fs_load_embedded_files__sig: 'vp',
Expand Down Expand Up @@ -642,6 +644,8 @@ sigs = {
emscripten_destroy_web_audio_node__sig: 'vi',
emscripten_destroy_worker__sig: 'vi',
emscripten_enter_soft_fullscreen__sig: 'ipp',
emscripten_epoll_add_listener__sig: 'iipp',
emscripten_epoll_remove_listener__sig: 'iip',
emscripten_err__sig: 'vp',
emscripten_errn__sig: 'vpp',
emscripten_exit_fullscreen__sig: 'i',
Expand Down
49 changes: 39 additions & 10 deletions src/lib/libsyscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,7 @@ var SyscallsLibrary = {
if (!stream) return {{{ cDefs.POLLNVAL }}};
// Streams without a poll handler (regular files, incl. NODERAWFS/NODEFS
// which leave stream_ops unset) are treated as always readable+writable.
var flags = stream.stream_ops?.poll
? stream.stream_ops.poll(stream)
: {{{ cDefs.POLLIN | cDefs.POLLOUT }}};
var flags = stream.stream_ops?.poll?.(stream) ?? {{{ cDefs.POLLIN | cDefs.POLLOUT }}};
return flags & (events | {{{ cDefs.POLLERR }}} | {{{ cDefs.POLLHUP }}} | {{{ cDefs.POLLNVAL }}});
},
__syscall_poll__proxy: 'sync',
Expand Down Expand Up @@ -718,13 +716,44 @@ var SyscallsLibrary = {
__syscall_poll_nonblocking: (fds, nfds) => {
return doPollSync(fds, nfds);
},
// epoll is not yet implemented in the legacy (non-WASMFS) JS syscall layer.
__syscall_epoll_create1__nothrow: true,
__syscall_epoll_create1: (flags) => -{{{ cDefs.ENOSYS }}},
__syscall_epoll_ctl__nothrow: true,
__syscall_epoll_ctl: (epfd, op, fd, ev) => -{{{ cDefs.ENOSYS }}},
__syscall_epoll_pwait__nothrow: true,
__syscall_epoll_pwait: (epfd, ev, maxevents, timeout, sigmask, sigsetsize) => -{{{ cDefs.ENOSYS }}},
// epoll: the entry points live here (like every other syscall); the heavy
// lifting is in libepoll.js, which they call after resolving the epoll stream.
__syscall_epoll_create1__deps: ['$newEpollInstance'],
__syscall_epoll_create1__proxy: 'sync',
__syscall_epoll_create1: (flags) => {
// EPOLL_CLOEXEC is accepted but a no-op (there is no exec).
if (flags & ~{{{ cDefs.EPOLL_CLOEXEC }}}) return -{{{ cDefs.EINVAL }}};
return newEpollInstance().fd;
},
__syscall_epoll_ctl__deps: ['$FS', '$epollCtl'],
__syscall_epoll_ctl__proxy: 'sync',
__syscall_epoll_ctl: (epfd, op, fd, ev) => {
var ep = FS.getStream(epfd);
if (!ep?.shared.epoll) return -{{{ cDefs.EBADF }}};
return epollCtl(ep.shared, op, fd, ev);
},
__syscall_epoll_pwait__proxy: 'sync',
__syscall_epoll_pwait__async: 'auto',
__syscall_epoll_pwait__deps: ['$FS', '$epollPwait'],
__syscall_epoll_pwait: (epfd, ev, maxevents, timeout, sigmask, sigsetsize) => {
var ep = FS.getStream(epfd);
if (!ep?.shared.epoll) return -{{{ cDefs.EBADF }}};
if (maxevents <= 0) return -{{{ cDefs.EINVAL }}};
return epollPwait(ep.shared, ev, maxevents, timeout);
},
// libc routes zero-timeout epoll_wait()/epoll_pwait() calls here: a plain
// import that never suspends, so probes stay callable from any context (under
// JSPI, __syscall_epoll_pwait is a suspending import and traps when called
// from a stack that wasn't entered through a promising export). Mirrors
// __syscall_poll_nonblocking.
__syscall_epoll_pwait_nonblocking__proxy: 'sync',
__syscall_epoll_pwait_nonblocking__deps: ['$FS', '$doEpollWait'],
__syscall_epoll_pwait_nonblocking: (epfd, ev, maxevents) => {
var ep = FS.getStream(epfd);
if (!ep?.shared.epoll) return -{{{ cDefs.EBADF }}};
if (maxevents <= 0) return -{{{ cDefs.EINVAL }}};
return doEpollWait(ep.shared, ev, maxevents);
},
__syscall_getcwd__deps: ['$lengthBytesUTF8', '$stringToUTF8'],
__syscall_getcwd: (buf, size) => {
if (!size) return -{{{ cDefs.EINVAL }}};
Expand Down
1 change: 1 addition & 0 deletions src/modules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ function calculateLibraries() {
'libtty.js',
'libpipefs.js', // ok to include it by default since it's only used if the syscall is used
'libsockfs.js', // ok to include it by default since it's only used if the syscall is used
'libepoll.js', // ok to include it by default since it's only used if the syscall is used
);

if (NODERAWSOCKETS) {
Expand Down
24 changes: 24 additions & 0 deletions src/struct_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,30 @@
]
}
},
{
"file": "sys/epoll.h",
"defines": [
"EPOLLIN",
"EPOLLOUT",
"EPOLLERR",
"EPOLLHUP",
"EPOLLRDNORM",
"EPOLLWRNORM",
"EPOLLET",
"EPOLLONESHOT",
"EPOLLEXCLUSIVE",
"EPOLL_CTL_ADD",
"EPOLL_CTL_DEL",
"EPOLL_CTL_MOD",
"EPOLL_CLOEXEC"
],
"structs": {
"epoll_event": [
"events",
"data"
]
}
},
{
"file": "time.h",
"defines": [
Expand Down
18 changes: 18 additions & 0 deletions src/struct_info_generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,19 @@
"EPERM": 63,
"EPFNOSUPPORT": 139,
"EPIPE": 64,
"EPOLLERR": 8,
"EPOLLET": -2147483648,
"EPOLLEXCLUSIVE": 268435456,
"EPOLLHUP": 16,
"EPOLLIN": 1,
"EPOLLONESHOT": 1073741824,
"EPOLLOUT": 4,
"EPOLLRDNORM": 64,
"EPOLLWRNORM": 256,
"EPOLL_CLOEXEC": 524288,
"EPOLL_CTL_ADD": 1,
"EPOLL_CTL_DEL": 2,
"EPOLL_CTL_MOD": 3,
"EPROTO": 65,
"EPROTONOSUPPORT": 66,
"EPROTOTYPE": 67,
Expand Down Expand Up @@ -1020,6 +1033,11 @@
"stack_ptr": 8,
"user_data": 16
},
"epoll_event": {
"__size__": 16,
"data": 8,
"events": 0
},
"flock": {
"__size__": 32,
"l_type": 0
Expand Down
18 changes: 18 additions & 0 deletions src/struct_info_generated_wasm64.json
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,19 @@
"EPERM": 63,
"EPFNOSUPPORT": 139,
"EPIPE": 64,
"EPOLLERR": 8,
"EPOLLET": -2147483648,
"EPOLLEXCLUSIVE": 268435456,
"EPOLLHUP": 16,
"EPOLLIN": 1,
"EPOLLONESHOT": 1073741824,
"EPOLLOUT": 4,
"EPOLLRDNORM": 64,
"EPOLLWRNORM": 256,
"EPOLL_CLOEXEC": 524288,
"EPOLL_CTL_ADD": 1,
"EPOLL_CTL_DEL": 2,
"EPOLL_CTL_MOD": 3,
"EPROTO": 65,
"EPROTONOSUPPORT": 66,
"EPROTOTYPE": 67,
Expand Down Expand Up @@ -1020,6 +1033,11 @@
"stack_ptr": 16,
"user_data": 32
},
"epoll_event": {
"__size__": 16,
"data": 8,
"events": 0
},
"flock": {
"__size__": 32,
"l_type": 0
Expand Down
75 changes: 75 additions & 0 deletions system/include/emscripten/epoll.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2026 The Emscripten Authors. All rights reserved.
* Emscripten is available under two separate licenses, the MIT license and the
* University of Illinois/NCSA Open Source License. Both these licenses can be
* found in the LICENSE file.
*/

#pragma once

#include <sys/epoll.h>

#ifdef __cplusplus
extern "C" {
#endif

// EXPERIMENTAL. This API is new and may change (signature or semantics) over the
// next few releases.
//
// Register a persistent readiness listener on an existing epoll fd (built with
// epoll_create1/epoll_ctl): instead of blocking in epoll_wait, the runtime
// invokes `callback` on the event loop whenever the epoll set has ready events
// waiting to be collected. The callback receives only `userdata`; it does not
// receive the events. To collect them it calls epoll_wait(epfd, ..., 0) itself
// - a non-blocking, zero-timeout wait - from within the callback (or later).
// Unlike epoll_wait it never blocks the calling stack, so it works without
// ASYNCIFY/JSPI. The callback is delivered on the registering thread's event
// loop: with pthreads the epoll readiness is tracked on the main thread (the
// syscalls are proxied there), but each delivery is dispatched back to the
// thread that added the listener.
//
// Any number of listeners may be added, from any threads, identified by the
// (callback, registering thread) pair; re-adding the same identity just updates
// `userdata`. Every listener is signalled while uncollected ready events remain
// (broadcast), and listeners race to collect: per-fd trigger modes distribute
// events across collectors exactly as between multiple blocking epoll_wait
// callers on one epoll, so an EPOLLET edge or an EPOLLONESHOT firing is
// collected by exactly one listener (load balancing), while a level fd keeps
// signalling every listener until drained.
//
// A listener fires on the next event-loop tick while the set has ready events
// that have not yet been collected, and keeps firing while any remain - it only
// signals that events are pending, so a callback that does not drain them (via
// epoll_wait) leaves them pending and re-fires. Whether a given fd is
// re-reported follows its per-fd trigger mode (set via epoll_ctl) exactly as
// epoll_wait does, so one epoll can mix modes:
// - Level-triggered (the default): the fd is reported on the next tick whenever
// it is ready, and keeps re-firing while it stays ready. The runtime - not
// the application - drives the loop, so an fd that is structurally always
// ready (notably EPOLLOUT on a writable socket) will spin the event loop.
// Use one of the modes below for such fds.
// - EPOLLET (edge-triggered): reported once per readiness edge and not again
// until a fresh edge; usually preferable in this model.
// - EPOLLONESHOT: reported once, then the registration is disabled until you
// re-arm it with epoll_ctl(EPOLL_CTL_MOD).
//
// Listeners keep the runtime alive as long as the set can still fire - i.e.
// while the epoll has at least one open watched fd. This follows the Node.js
// model, where registered I/O interest holds the event loop open. Once every
// watched fd is closed the set is terminal (it can never become ready again)
// and its listeners stop holding the runtime, so no explicit disposal is
// required in that case.
//
// Listeners are shared instance state: they see registrations made through any
// dup'd fd, and closing the last fd to the instance removes them all. Returns
// 0, or a positive errno (EBADF if `epfd` is not an epoll fd).
typedef void (*em_epoll_callback)(void *userdata);
int emscripten_epoll_add_listener(int epfd, em_epoll_callback callback, void *userdata);

// Remove the calling thread's listener for `callback`. Returns 0, EBADF if
// `epfd` is not an epoll fd, or ENOENT if no such listener is registered.
int emscripten_epoll_remove_listener(int epfd, em_epoll_callback callback);

#ifdef __cplusplus
}
#endif
1 change: 1 addition & 0 deletions system/include/emscripten/syscalls.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ int __syscall_shutdown(int sockfd, int how, int unused1, int unused2, int unused
int __syscall_epoll_create1(int flags);
int __syscall_epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
int __syscall_epoll_pwait(int epfd, struct epoll_event *ev, int maxevents, int timeout, const sigset_t *sigmask, size_t sigsetsize);
int __syscall_epoll_pwait_nonblocking(int epfd, struct epoll_event *ev, int maxevents);

#ifdef __cplusplus
}
Expand Down
4 changes: 4 additions & 0 deletions system/lib/libc/emscripten_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ emscripten_stack_unwind_buffer(uintptr_t pc, uintptr_t* buffer, uint32_t depth);

bool _emscripten_get_now_is_monotonic(void);

// Defined in library.js; called by emscripten_epoll_callback.c to report a
// completed cross-thread epoll callback delivery back to the main thread.
void _emscripten_epoll_delivery_done(int token);

void _emscripten_get_progname(char*, int);

// Not defined in musl, but defined in library.js. Included here for
Expand Down
10 changes: 10 additions & 0 deletions system/lib/libc/musl/src/linux/epoll.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ int epoll_ctl(int fd, int op, int fd2, struct epoll_event *ev)

int epoll_pwait(int fd, struct epoll_event *ev, int cnt, int to, const sigset_t *sigs)
{
#ifdef __EMSCRIPTEN__
// A zero timeout is an instantaneous probe: route it through a plain
// import that never suspends. Under JSPI, __syscall_epoll_pwait is a
// suspending import and so may only be called from a stack entered through
// a promising export — a requirement a readiness probe must not carry
// (e.g. probes from event-loop callbacks). Mirrors poll() above.
if (to == 0) {
return __syscall_ret(__syscall_epoll_pwait_nonblocking(fd, ev, cnt));
}
#endif
int r = __syscall_cp(SYS_epoll_pwait, fd, ev, cnt, to, sigs, _NSIG/8);
#ifdef SYS_epoll_wait
if (r==-ENOSYS && !sigs) r = __syscall_cp(SYS_epoll_wait, fd, ev, cnt, to);
Expand Down
Loading
Loading