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
9 changes: 9 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ See docs/process.md for more on how version tagging works.
- The `GROWABLE_ARRAYBUFFERS` setting now defaults to 1, which means it will be
used when available. Note that this only affects programs that are built with
`ALLOW_MEMORY_GROWTH`, which is not enabled by default. (#27212)
backends with a `poll` handler must update. (#27207)
- 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)
- New `-sNODERAWSOCKETS` setting that backs the POSIX sockets API with real TCP
(`node:net`) and UDP (`node:dgram`) sockets on Node.js, with no `ws`, proxy
process, or pthreads required. Supports incoming and outgoing TCP, UDP, IPv6,
Expand Down
403 changes: 403 additions & 0 deletions src/lib/libepoll.js

Large diffs are not rendered by default.

2 changes: 2 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_fallocate__sig: 'iiijj',
__syscall_fchdir__sig: 'ii',
Expand Down Expand Up @@ -637,6 +638,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 @@ -594,9 +594,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 @@ -702,13 +700,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?.epoll) return -{{{ cDefs.EBADF }}};
return epollCtl(ep, 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?.epoll) return -{{{ cDefs.EBADF }}};
if (maxevents <= 0) return -{{{ cDefs.EINVAL }}};
return epollPwait(ep, 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?.epoll) return -{{{ cDefs.EBADF }}};
if (maxevents <= 0) return -{{{ cDefs.EINVAL }}};
return doEpollWait(ep, 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 @@ -88,6 +88,7 @@ function calculateLibraries() {

if (!WASMFS) {
libraries.push('libsyscall.js');
libraries.push('libepoll.js');
}

if (MAIN_MODULE) {
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 @@ -1018,6 +1031,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 @@ -1018,6 +1031,11 @@
"stack_ptr": 16,
"user_data": 32
},
"epoll_event": {
"__size__": 16,
"data": 8,
"events": 0
},
"flock": {
"__size__": 32,
"l_type": 0
Expand Down
57 changes: 57 additions & 0 deletions system/include/emscripten/epoll.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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 main thread's event loop; under
// PROXY_TO_PTHREAD use a blocking epoll_wait from the pthread instead.
//
// 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 -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
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
6 changes: 6 additions & 0 deletions system/lib/wasmfs/syscalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1862,4 +1862,10 @@ int __syscall_epoll_pwait(int epfd,
return -ENOSYS;
}

int __syscall_epoll_pwait_nonblocking(int epfd,
struct epoll_event* ev,
int maxevents) {
return -ENOSYS;
}

} // extern "C"
9 changes: 6 additions & 3 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"a.out.js": 268311,
"a.out.nodebug.wasm": 587978,
"total": 856289,
"a.out.js": 271358,
"a.out.nodebug.wasm": 588038,
"total": 859396,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down Expand Up @@ -225,6 +225,7 @@
"__syscall_epoll_create1",
"__syscall_epoll_ctl",
"__syscall_epoll_pwait",
"__syscall_epoll_pwait_nonblocking",
"__syscall_faccessat",
"__syscall_fallocate",
"__syscall_fchdir",
Expand Down Expand Up @@ -455,6 +456,7 @@
"emscripten_debugger",
"emscripten_destroy_worker",
"emscripten_enter_soft_fullscreen",
"emscripten_epoll_set_callback",
"emscripten_err",
"emscripten_errn",
"emscripten_exit_fullscreen",
Expand Down Expand Up @@ -1750,6 +1752,7 @@
"env.__syscall_epoll_create1",
"env.__syscall_epoll_ctl",
"env.__syscall_epoll_pwait",
"env.__syscall_epoll_pwait_nonblocking",
"env.__syscall_faccessat",
"env.__syscall_fallocate",
"env.__syscall_fchdir",
Expand Down
Loading