Skip to content

Commit c6bfc66

Browse files
committed
Support blocking socket ops via a single fd_wait primitive
Blocking accept/recv on sockets, replacing the previous approach of marking the socket data syscalls __async (which, under JSPI, wrapped every one of them in WebAssembly.Suspending - taxing every nonblocking call with a suspend/resume round-trip, since a Suspending import always resolves through a Promise in V8). The data syscalls stay strictly synchronous imports: single attempt, -EAGAIN when they would block. Blocking is factored into one new import, _emscripten_fd_wait(fd, events), and retry loops in the musl wrappers (accept, accept4, recvfrom, recvmsg): on EAGAIN with a blocking fd and no MSG_DONTWAIT, wait for readiness on the inode's listener queue and retry. This is a pthreads-only facility. The retry loops compile only into the -mt libc (gated on __EMSCRIPTEN_PTHREADS__ - the only compile-time boundary libc has; ASYNCIFY is a link-time transform with no libc variant), and _emscripten_fd_wait blocks only on a proxied pthread worker: __proxy sync + __async gives the PROXY_SYNC_ASYNC call path, whose sync-proxy completes - ending the worker's futex wait - when the returned Promise resolves. In every other context, including the event-loop thread which cannot block, it fails with -EAGAIN. Single-threaded ASYNCIFY/JSPI builds use epoll for readiness instead, so a purely-synchronous build keeps the direct doReadv/doWritev path byte-for-byte and hello-world code size is unchanged. accept4 now applies SOCK_NONBLOCK to the accepted fd (on top of the flags it inherits from the listener); without this a SOCK_NONBLOCK accept off a blocking listener wrongly yielded a blocking socket. Send/write paths are untouched: the node backend buffers and never would-blocks, so blocking send degenerates to synchronous buffered success and needs no wait machinery. read()/write() on a socket fd are likewise not covered - only the socket calls themselves. Tested with test_noderawsockets_tcp_blocking (blocking accept + recv that must suspend, under PROXY_TO_PTHREAD) and test_noderawsockets_tcp_accept_nonblock (accept4 SOCK_NONBLOCK off a blocking listener), plus the mio test suite under PROXY_TO_PTHREAD + NODERAWSOCKETS + NODERAWFS: 144 passed, 0 failed, 5 ignored.
1 parent 53f754a commit c6bfc66

12 files changed

Lines changed: 322 additions & 2 deletions

File tree

ChangeLog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ See docs/process.md for more on how version tagging works.
4949
that signals an epoll set's readiness to a callback (which collects the
5050
events itself via a zero-timeout `epoll_wait`) with no `ASYNCIFY`/`JSPI`.
5151
(#27207)
52+
- Blocking `accept`, `recv`, `recvfrom` and `recvmsg` on sockets are now
53+
supported under `-pthread` with `PROXY_TO_PTHREAD`: a blocking call whose
54+
socket would-block suspends the proxied worker on the inode readiness queue
55+
and retries when woken, instead of returning `EAGAIN`. (`send`/`write` never
56+
block, as the Node.js backend buffers.) This covers the socket calls only,
57+
not a blocking `read()`/`write()` on a socket fd; single-threaded
58+
`ASYNCIFY`/`JSPI` builds should use `epoll` for readiness instead. (#27277)
5259
- compiler-rt and libunwind were updated to LLVM 22.1.8. (#27245, #27246)
5360
- `-fcoverage-mapping` is currently broken due to a mismatch between the version
5461
of LLVM used and the imported version of compiler-rt. We hope to fix this

src/lib/libsigs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ sigs = {
328328
_emscripten_dlopen_js__sig: 'vpppp',
329329
_emscripten_dlsync_threads__sig: 'v',
330330
_emscripten_epoll_delivery_done__sig: 'vi',
331+
_emscripten_fd_wait__sig: 'iii',
331332
_emscripten_fetch_get_response_headers__sig: 'pipp',
332333
_emscripten_fetch_get_response_headers_length__sig: 'pi',
333334
_emscripten_fs_load_embedded_files__sig: 'vp',

src/lib/libsyscall.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,13 @@ var SyscallsLibrary = {
403403
assert(!errno);
404404
#endif
405405
}
406+
// Honor SOCK_NONBLOCK on the accepted fd (SOCK_CLOEXEC is a no-op for a
407+
// single process, matching F_SETFD). Without this the new fd only inherits
408+
// the listener's flags, so a SOCK_NONBLOCK accept off a blocking listener
409+
// would wrongly yield a blocking socket.
410+
if (flags & {{{ cDefs.SOCK_NONBLOCK }}}) {
411+
newsock.stream.flags |= {{{ cDefs.O_NONBLOCK }}};
412+
}
406413
return newsock.stream.fd;
407414
},
408415
__syscall_bind__deps: ['$getSocketFromFD', '$getSocketAddress'],
@@ -695,6 +702,43 @@ var SyscallsLibrary = {
695702
__syscall_poll_nonblocking: (fds, nfds) => {
696703
return doPollSync(fds, nfds);
697704
},
705+
// The single wait primitive behind blocking socket data ops. The data
706+
// syscalls themselves are strictly synchronous (single attempt, -EAGAIN when
707+
// they would block); libc's blocking wrappers (compiled only into the -mt
708+
// libc) call this on EAGAIN with a blocking fd and then retry. It blocks only
709+
// on a proxied pthread worker: the sync-proxy completes - ending the worker's
710+
// futex wait - when the returned promise resolves. In every other context
711+
// (including the event-loop thread, which cannot block) it fails with
712+
// -EAGAIN. Resolves 0 once `fd` reports one of `events` (POLL* flags;
713+
// error/hangup/close always wake). Single-threaded builds use epoll instead.
714+
#if !PTHREADS
715+
// Without pthreads the body is just `return -EAGAIN`, which cannot throw;
716+
// skip the syscall try/catch wrapper so closure doesn't flag it as dead.
717+
_emscripten_fd_wait__nothrow: true,
718+
#endif
719+
_emscripten_fd_wait__proxy: 'sync',
720+
_emscripten_fd_wait__async: 'auto',
721+
_emscripten_fd_wait__deps: ['$FS', '$pollOne'],
722+
_emscripten_fd_wait: (fd, events) => {
723+
#if PTHREADS
724+
if (PThread.currentProxiedOperationCallerThread) {
725+
// Must resolve through a Promise: the caller's sync-proxy awaits a
726+
// thenable (PROXY_SYNC_ASYNC), even when already ready.
727+
return new Promise((resolve) => {
728+
if (pollOne(fd, events)) return resolve(0);
729+
var stream = FS.getStream(fd);
730+
if (!stream) return resolve(0); // closed: let the retry surface EBADF
731+
var reg = stream.node.addListener(() => {
732+
if (pollOne(fd, events)) {
733+
reg.listeners.delete(reg.entry);
734+
resolve(0);
735+
}
736+
});
737+
});
738+
}
739+
#endif
740+
return -{{{ cDefs.EAGAIN }}};
741+
},
698742
// epoll: the entry points live here (like every other syscall); the heavy
699743
// lifting is in libepoll.js, which they call after resolving the epoll stream.
700744
__syscall_epoll_create1__deps: ['$newEpollInstance'],
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#ifndef EMSCRIPTEN_FD_WAIT_H
2+
#define EMSCRIPTEN_FD_WAIT_H
3+
4+
// Blocking socket data ops on emscripten: the underlying JS syscalls are
5+
// strictly synchronous and return -EAGAIN when they would block. For a
6+
// blocking fd the network wrappers wait for readiness via the single blocking
7+
// primitive _emscripten_fd_wait and retry. This is a pthreads-only facility
8+
// (the retry loops compile only into the -mt libc): _emscripten_fd_wait blocks
9+
// by parking a proxied worker on its sync-proxy. Where no stack can wait (the
10+
// event-loop thread itself), the wait fails and the EAGAIN surfaces unchanged.
11+
// Single-threaded JSPI/ASYNCIFY builds use epoll for readiness instead.
12+
13+
#include <fcntl.h>
14+
#include <errno.h>
15+
#include <poll.h>
16+
#include "syscall.h"
17+
18+
int _emscripten_fd_wait(int fd, int events);
19+
20+
static inline int __emscripten_sock_can_wait(int fd, int dontwait)
21+
{
22+
if (dontwait) return 0;
23+
int fl = __syscall(SYS_fcntl64, fd, F_GETFL);
24+
return fl >= 0 && !(fl & O_NONBLOCK);
25+
}
26+
27+
#endif
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
#include <sys/socket.h>
22
#include "syscall.h"
3+
#ifdef __EMSCRIPTEN_PTHREADS__
4+
#include "emscripten_fd_wait.h"
5+
#endif
36

47
int accept(int fd, struct sockaddr *restrict addr, socklen_t *restrict len)
58
{
9+
#ifdef __EMSCRIPTEN_PTHREADS__
10+
for (;;) {
11+
long r = __socketcall_cp(accept, fd, addr, len, 0, 0, 0);
12+
if (r != -EAGAIN || !__emscripten_sock_can_wait(fd, 0)
13+
|| _emscripten_fd_wait(fd, POLLIN))
14+
return __syscall_ret(r);
15+
}
16+
#else
617
return socketcall_cp(accept, fd, addr, len, 0, 0, 0);
18+
#endif
719
}

system/lib/libc/musl/src/network/accept4.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,26 @@
33
#include <errno.h>
44
#include <fcntl.h>
55
#include "syscall.h"
6+
#ifdef __EMSCRIPTEN_PTHREADS__
7+
#include "emscripten_fd_wait.h"
8+
#endif
69

710
int accept4(int fd, struct sockaddr *restrict addr, socklen_t *restrict len, int flg)
811
{
912
if (!flg) return accept(fd, addr, len);
13+
#ifdef __EMSCRIPTEN_PTHREADS__
14+
int ret;
15+
for (;;) {
16+
long r = __socketcall_cp(accept4, fd, addr, len, flg, 0, 0);
17+
if (r != -EAGAIN || !__emscripten_sock_can_wait(fd, 0)
18+
|| _emscripten_fd_wait(fd, POLLIN)) {
19+
ret = __syscall_ret(r);
20+
break;
21+
}
22+
}
23+
#else
1024
int ret = socketcall_cp(accept4, fd, addr, len, flg, 0, 0);
25+
#endif
1126
if (ret>=0 || (errno != ENOSYS && errno != EINVAL)) return ret;
1227
if (flg & ~(SOCK_CLOEXEC|SOCK_NONBLOCK)) {
1328
errno = EINVAL;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
#include <sys/socket.h>
22
#include "syscall.h"
3+
#ifdef __EMSCRIPTEN_PTHREADS__
4+
#include "emscripten_fd_wait.h"
5+
#endif
36

47
ssize_t recvfrom(int fd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict alen)
58
{
9+
#ifdef __EMSCRIPTEN_PTHREADS__
10+
for (;;) {
11+
long r = __socketcall_cp(recvfrom, fd, buf, len, flags, addr, alen);
12+
if (r != -EAGAIN || !__emscripten_sock_can_wait(fd, flags & MSG_DONTWAIT)
13+
|| _emscripten_fd_wait(fd, POLLIN))
14+
return __syscall_ret(r);
15+
}
16+
#else
617
return socketcall_cp(recvfrom, fd, buf, len, flags, addr, alen);
18+
#endif
719
}

system/lib/libc/musl/src/network/recvmsg.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
#include <sys/time.h>
55
#include <string.h>
66
#include "syscall.h"
7+
#ifdef __EMSCRIPTEN_PTHREADS__
8+
#include "emscripten_fd_wait.h"
9+
#endif
710

811
hidden void __convert_scm_timestamps(struct msghdr *, socklen_t);
912

@@ -59,7 +62,18 @@ ssize_t recvmsg(int fd, struct msghdr *msg, int flags)
5962
msg = &h;
6063
}
6164
#endif
65+
#ifdef __EMSCRIPTEN_PTHREADS__
66+
for (;;) {
67+
long rr = __socketcall_cp(recvmsg, fd, msg, flags, 0, 0, 0);
68+
if (rr != -EAGAIN || !__emscripten_sock_can_wait(fd, flags & MSG_DONTWAIT)
69+
|| _emscripten_fd_wait(fd, POLLIN)) {
70+
r = __syscall_ret(rr);
71+
break;
72+
}
73+
}
74+
#else
6275
r = socketcall_cp(recvmsg, fd, msg, flags, 0, 0, 0);
76+
#endif
6377
if (r >= 0) __convert_scm_timestamps(msg, orig_controllen);
6478
#if LONG_MAX > INT_MAX && !defined(__EMSCRIPTEN__)
6579
if (orig) *orig = h;

test/codesize/test_codesize_hello_dylink_all.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"a.out.js": 272436,
2+
"a.out.js": 272517,
33
"a.out.nodebug.wasm": 588107,
4-
"total": 860543,
4+
"total": 860624,
55
"sent": [
66
"IMG_Init",
77
"IMG_Load",
@@ -277,6 +277,7 @@
277277
"_dlsym_catchup_js",
278278
"_dlsym_js",
279279
"_emscripten_dlopen_js",
280+
"_emscripten_fd_wait",
280281
"_emscripten_fs_load_embedded_files",
281282
"_emscripten_get_last_devicemotion_event",
282283
"_emscripten_get_last_deviceorientation_event",
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright 2026 The Emscripten Authors. All rights reserved.
3+
* Emscripten is available under two separate licenses, the MIT license and the
4+
* University of Illinois/NCSA Open Source License. Both these licenses can be
5+
* found in the LICENSE file.
6+
*
7+
* accept4(SOCK_NONBLOCK) must yield a non-blocking accepted socket even off a
8+
* *blocking* listener: the flag is applied on top of the flags inherited from
9+
* the listener, not dropped. A poll()-driven main loop (single-threaded, zero
10+
* timeout) waits for the incoming connection, accept4()s it with SOCK_NONBLOCK,
11+
* then checks F_GETFL reports O_NONBLOCK and that a data-less recv() would-block
12+
* with EAGAIN rather than hanging. Plain POSIX, so it also runs natively.
13+
*/
14+
15+
#include <arpa/inet.h>
16+
#include <assert.h>
17+
#include <errno.h>
18+
#include <fcntl.h>
19+
#include <netinet/in.h>
20+
#include <poll.h>
21+
#include <stdbool.h>
22+
#include <stdio.h>
23+
#include <string.h>
24+
#include <sys/socket.h>
25+
#include <unistd.h>
26+
27+
#ifdef __EMSCRIPTEN__
28+
#include <emscripten.h>
29+
#endif
30+
31+
static int listen_fd = -1;
32+
static int client_fd = -1;
33+
static int peer_fd = -1;
34+
35+
static void finish(void) {
36+
if (client_fd >= 0) close(client_fd);
37+
if (peer_fd >= 0) close(peer_fd);
38+
if (listen_fd >= 0) close(listen_fd);
39+
printf("done\n");
40+
#ifdef __EMSCRIPTEN__
41+
emscripten_cancel_main_loop();
42+
#endif
43+
}
44+
45+
static void main_loop(void) {
46+
struct pollfd pfd = { .fd = listen_fd, .events = POLLIN };
47+
if (poll(&pfd, 1, 0) <= 0 || !(pfd.revents & POLLIN)) {
48+
return; // no connection queued yet
49+
}
50+
51+
// The listener is blocking (never marked O_NONBLOCK), so inheritance alone
52+
// would give a blocking socket; SOCK_NONBLOCK must override that.
53+
peer_fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK);
54+
assert(peer_fd >= 0);
55+
56+
int fl = fcntl(peer_fd, F_GETFL);
57+
assert(fl >= 0 && (fl & O_NONBLOCK) && "accept4 SOCK_NONBLOCK not honored");
58+
59+
// A non-blocking recv with no data pending returns EAGAIN immediately instead
60+
// of blocking, confirming the fd is really non-blocking.
61+
char buf[4];
62+
ssize_t n = recv(peer_fd, buf, sizeof(buf), 0);
63+
assert(n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK));
64+
65+
finish();
66+
}
67+
68+
int main(void) {
69+
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
70+
assert(listen_fd >= 0);
71+
72+
struct sockaddr_in addr;
73+
memset(&addr, 0, sizeof(addr));
74+
addr.sin_family = AF_INET;
75+
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
76+
assert(bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0);
77+
socklen_t l = sizeof(addr);
78+
assert(getsockname(listen_fd, (struct sockaddr*)&addr, &l) == 0);
79+
assert(listen(listen_fd, 4) == 0);
80+
// Deliberately leave listen_fd blocking to prove SOCK_NONBLOCK is applied on
81+
// top of the inherited (blocking) listener flags.
82+
83+
client_fd = socket(AF_INET, SOCK_STREAM, 0);
84+
assert(client_fd >= 0);
85+
fcntl(client_fd, F_SETFL, O_NONBLOCK);
86+
int r = connect(client_fd, (struct sockaddr*)&addr, sizeof(addr));
87+
assert(r == 0 || errno == EINPROGRESS);
88+
89+
#ifdef __EMSCRIPTEN__
90+
emscripten_set_main_loop(main_loop, 0, 0);
91+
#else
92+
while (peer_fd < 0) {
93+
main_loop();
94+
usleep(1000);
95+
}
96+
#endif
97+
return 0;
98+
}

0 commit comments

Comments
 (0)