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
8 changes: 8 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ See docs/process.md for more on how version tagging works.
FS-backend handler signature changed from `poll(stream, timeout)` to
`poll(stream)` returning the current readiness mask; out-of-tree custom FS
backends with a `poll` handler must update. (#27226)
- 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`. Also added `emscripten_epoll_set_callback`
(in the new `<emscripten/epoll.h>`, experimental), a non-blocking variant
that delivers an epoll set's readiness to a JS callback with no
`ASYNCIFY`/`JSPI`. (#27207)
- compiler-rt and libunwind were updated to LLVM 22.1.8. (#27245, #27246)
- `-fcoverage-mapping` is currently broken due to a mismatch between the version
of LLVM used and the imported version of compiler-rt. We hope to fix this
Expand Down
547 changes: 547 additions & 0 deletions src/lib/libepoll.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,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 @@ -326,6 +327,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 @@ -639,6 +641,7 @@ sigs = {
emscripten_destroy_web_audio_node__sig: 'vi',
emscripten_destroy_worker__sig: 'vi',
emscripten_enter_soft_fullscreen__sig: 'ipp',
emscripten_epoll_set_callback__sig: 'iiipp',
emscripten_err__sig: 'vp',
emscripten_errn__sig: 'vpp',
emscripten_exit_fullscreen__sig: 'i',
Expand Down
47 changes: 37 additions & 10 deletions src/lib/libsyscall.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,9 +589,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 @@ -697,13 +695,42 @@ 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) => {
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 === 0) 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 @@ -113,6 +113,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 @@ -260,6 +260,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 @@ -1019,6 +1032,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 @@ -260,6 +260,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 @@ -1019,6 +1032,11 @@
"stack_ptr": 16,
"user_data": 32
},
"epoll_event": {
"__size__": 16,
"data": 8,
"events": 0
},
"flock": {
"__size__": 32,
"l_type": 0
Expand Down
59 changes: 59 additions & 0 deletions system/include/emscripten/epoll.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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; it is not yet covered by Emscripten's stability guarantees.
//
// Register a persistent readiness callback on an existing epoll fd (built with
// epoll_create1/epoll_ctl): instead of blocking in epoll_wait, the runtime calls
// `callback` every time the set makes progress, delivering up to `maxevents`
// ready events. An epoll is a long-lived readiness aggregator, so the interest is
// armed once and reused across every delivery - no per-spin re-arming. Unlike
// epoll_wait it never blocks the calling stack, so it works without ASYNCIFY/JSPI.
// The callback is delivered on the calling thread's event loop: with pthreads the
// epoll readiness is tracked on the thread that owns the filesystem (the syscalls
// are proxied there), but each delivery is dispatched back to the thread that
// registered the callback.
//
// While armed it keeps the runtime alive only as long as it can still fire - i.e.
// while the epoll has at least one open watched fd. Once every watched fd is
// closed the set is terminal (it can never become ready again) and the callback
// stops holding the runtime, so no explicit disposal is required in that case.
// To dispose while open fds remain, either pass a NULL `callback` (any
// `maxevents`) to unregister, or close the epoll fd. There is at most one
// callback per epoll: calling again replaces it (it does not stack). `events` is
// a runtime-owned buffer valid only for the duration of each callback. Returns 0,
// or a positive errno (EBADF if `epfd` is not an epoll fd, EINVAL).
//
// Each registration's trigger mode (set per-fd via epoll_ctl) controls how often
// the callback fires for it - identically to epoll_wait, so one callback can mix
// modes:
// - Level-triggered (the default): the callback fires on the next tick whenever
// the fd is ready, and keeps firing while it stays ready. The runtime - not
// the application - drives the loop, so an fd that is structurally always
// ready and never drained (notably EPOLLOUT on a writable socket) will spin
// the event loop. Use one of the modes below for such fds.
// - EPOLLET (edge-triggered): the callback fires once per readiness edge and
// not again until a fresh edge. Drain the fd fully on each delivery; this is
// the way to watch an always-writable fd without spinning.
// - EPOLLONESHOT: the callback fires once for that fd, then the registration is
// disabled until you re-arm it with epoll_ctl(EPOLL_CTL_MOD). Use it to
// handle an fd exactly once (e.g. before handing it elsewhere).
typedef void (*em_epoll_callback)(int epfd, struct epoll_event *events, int nready, void *userdata);
int emscripten_epoll_set_callback(int epfd, int maxevents, em_epoll_callback callback, void *userdata);

#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 @@ -124,6 +124,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 FS-owning 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
100 changes: 100 additions & 0 deletions system/lib/pthread/emscripten_epoll_callback.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.
*/

// Backs emscripten_epoll_set_callback under PTHREADS: the epoll readiness lives
// on the thread that owns the filesystem (the epoll syscalls are proxied
// there), but the user callback must run on the thread that registered it. This
// mirrors _emscripten_run_callback_on_thread in html5/callback.c, but carries
// the epoll callback's argument shape (epfd, events, nready, userdata) and,
// crucially, reports back to the registering thread when a delivery completes
// so it can pace the next one - a level-triggered fd stays ready until the
// callback drains it, so the FS thread must wait for that drain before
// re-deriving, or it would spin re-delivering the same readiness.

#include <assert.h>
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>

#include <emscripten/eventloop.h>
#include <emscripten/proxying.h>

#include "emscripten_internal.h"

typedef void (*em_epoll_callback)(int epfd,
struct epoll_event* events,
int nready,
void* userdata);

typedef struct epoll_callback_args_t {
em_epoll_callback callback;
int epfd;
int nready;
void* userdata;
int token;
struct epoll_event events[];
} epoll_callback_args_t;

// Runs on the registering thread: deliver the ready set to the user callback.
static void do_epoll_callback(void* arg) {
epoll_callback_args_t* args = (epoll_callback_args_t*)arg;
args->callback(args->epfd, args->events, args->nready, args->userdata);
}

// Runs back on the FS-owning thread once the delivery above has finished (or
// was cancelled because the target thread went away): let the JS layer
// re-derive.
static void do_epoll_done(void* arg) {
epoll_callback_args_t* args = (epoll_callback_args_t*)arg;
_emscripten_epoll_delivery_done(args->token);
free(arg);
}

void _emscripten_epoll_run_callback_on_thread(pthread_t t,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we maybe re-use the existing _emscripten_run_callback_on_thread that we use elsewhere?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under proxy to pthread, we need the completion on the FS thread to be able to handle the epoll completions, while also still proxying the event to to the listening thread.

em_epoll_callback callback,
int epfd,
struct epoll_event* events,
int nready,
void* userdata,
int token) {
em_proxying_queue* q = emscripten_proxy_get_system_queue();
// Copy the events out synchronously so the caller's (reused) buffer is free
// again the moment this returns; freed once the delivery completes.
size_t bytes = nready * sizeof(struct epoll_event);
epoll_callback_args_t* args = malloc(sizeof(epoll_callback_args_t) + bytes);
args->callback = callback;
args->epfd = epfd;
args->nready = nready;
args->userdata = userdata;
args->token = token;
memcpy(args->events, events, bytes);

if (!emscripten_proxy_callback(
q, t, do_epoll_callback, do_epoll_done, do_epoll_done, args)) {
assert(false && "emscripten_proxy_callback failed");
}
}

// Runs on the owning thread: adjust its (thread-local) runtime keepalive so the
// epoll callback holds the thread it was registered on, not the FS thread.
static void do_epoll_keepalive(void* arg) {
if ((intptr_t)arg > 0) {
emscripten_runtime_keepalive_push();
} else {
emscripten_runtime_keepalive_pop();
}
}

void _emscripten_epoll_keepalive_on_thread(pthread_t t, int delta) {
em_proxying_queue* q = emscripten_proxy_get_system_queue();
if (!emscripten_proxy_async(
q, t, do_epoll_keepalive, (void*)(intptr_t)delta)) {
assert(false && "emscripten_proxy_async failed");
}
}
Loading