Support blocking socket ops via a single fd_wait primitive - #27342
Open
guybedford wants to merge 4 commits into
Open
Support blocking socket ops via a single fd_wait primitive#27342guybedford wants to merge 4 commits into
guybedford wants to merge 4 commits into
Conversation
guybedford
force-pushed
the
blocking-socket-ops
branch
from
July 14, 2026 02:59
ab5b071 to
c6bfc66
Compare
6 tasks
guybedford
added a commit
to guybedford/emscripten
that referenced
this pull request
Jul 24, 2026
guybedford
added a commit
to guybedford/emscripten
that referenced
this pull request
Jul 28, 2026
guybedford
force-pushed
the
blocking-socket-ops
branch
3 times, most recently
from
August 20, 2026 03:40
d305cb6 to
4b28cc5
Compare
guybedford
force-pushed
the
blocking-socket-ops
branch
5 times, most recently
from
August 22, 2026 02:06
afad990 to
676365f
Compare
sbc100
reviewed
Aug 22, 2026
| }); | ||
| } | ||
| #endif | ||
| return -{{{ cDefs.EAGAIN }}}; |
Collaborator
There was a problem hiding this comment.
If this really does nothing in non-pthread builds maybe you can put the #if PTHREADS around the whole thing and define the stub version in system/lib/pthread/library_pthread_stub.c
Collaborator
There was a problem hiding this comment.
Actually it looks like maybe this symbol can simply be undefined in non-pthreads builds?
Collaborator
There was a problem hiding this comment.
I looks like this function could maybe just return a boolean?
Collaborator
Author
There was a problem hiding this comment.
Updated to call a boolean and folded the function into JS, so the C macro is one JS call.
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.
guybedford
force-pushed
the
blocking-socket-ops
branch
from
August 23, 2026 17:12
676365f to
c187b60
Compare
…pport - Move _emscripten_fd_wait to libfs.js (not a syscall; drops __nothrow). - Fold the O_NONBLOCK check into the primitive and return a boolean, so the libc retry loop makes a single JS call. - Support single-threaded ASYNCIFY/JSPI by suspending the calling stack, mirroring __syscall_poll; the retry loop now compiles into every libc variant, with a no-op stub for WASMFS. - Declare _emscripten_fd_wait only in emscripten_internal.h; add the copyright header; drop the _cp suffix from the retry macro. - Move the ChangeLog entry to 6.0.9; add a JSPI variant of the blocking test. - Rebaseline hello_dylink_all codesize.
guybedford
force-pushed
the
blocking-socket-ops
branch
from
August 23, 2026 17:12
c187b60 to
b740cea
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.
Adds support for blocking socket ops (
accept,accept4,recv,recvfrom,recvmsg) wherever the calling stack can suspend - secondary pthreads (includingmain()underPROXY_TO_PTHREAD), and single-threadedASYNCIFY/JSPIbuilds - via a single_emscripten_fd_wait(fd, events)primitive.The data syscalls stay strictly synchronous imports: single attempt,
-EAGAINwhen they would block. Blocking is factored into retry loops in the musl wrappers (via a shared__emscripten_sock_retryhelper documenting the convention): onEAGAINwith noMSG_DONTWAIT, call_emscripten_fd_waitand retry if it returns true._emscripten_fd_waitreturns false without waiting when the fd is non-blocking or there is no stack to suspend, so theEAGAINsurfaces unchanged. Otherwise it waits for readiness on the inode's listener queue, mirroring__syscall_poll:PROXY_SYNC_ASYNC): a secondary thread parks on its sync-proxy futex until the readiness promise resolves on the main thread. The main browser thread itself cannot block, so there it fails.ASYNCIFY/JSPIthe calling stack suspends on the same promise.The retry loops compile into every libc variant; WASMFS (no socket support) carries a no-op stub.
Only the receive-side socket calls are covered:
send/writenever block (the Node.js backend buffers), and a blockingread()/write()on a socket fd is out of scope.accept4now also appliesSOCK_NONBLOCKon top of the flags inherited from the listener; previously aSOCK_NONBLOCKaccept off a blocking listener wrongly yielded a blocking socket.Tested with
test_noderawsockets_tcp_blocking(blocking accept + recv that must suspend) under bothPROXY_TO_PTHREADandJSPI, andtest_noderawsockets_tcp_accept_nonblock, plus the mio suite underPROXY_TO_PTHREAD+NODERAWSOCKETS+NODERAWFS: 144 passed, 0 failed, 5 ignored.Made with AI assistance under my review.