Skip to content

Support blocking socket ops via a single fd_wait primitive - #27342

Open
guybedford wants to merge 4 commits into
emscripten-core:mainfrom
guybedford:blocking-socket-ops
Open

Support blocking socket ops via a single fd_wait primitive#27342
guybedford wants to merge 4 commits into
emscripten-core:mainfrom
guybedford:blocking-socket-ops

Conversation

@guybedford

@guybedford guybedford commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Adds support for blocking socket ops (accept, accept4, recv, recvfrom, recvmsg) wherever the calling stack can suspend - secondary pthreads (including main() under PROXY_TO_PTHREAD), and single-threaded ASYNCIFY/JSPI builds - via a single _emscripten_fd_wait(fd, events) primitive.

The data syscalls stay strictly synchronous imports: single attempt, -EAGAIN when they would block. Blocking is factored into retry loops in the musl wrappers (via a shared __emscripten_sock_retry helper documenting the convention): on EAGAIN with no MSG_DONTWAIT, call _emscripten_fd_wait and retry if it returns true.

_emscripten_fd_wait returns false without waiting when the fd is non-blocking or there is no stack to suspend, so the EAGAIN surfaces unchanged. Otherwise it waits for readiness on the inode's listener queue, mirroring __syscall_poll:

  • Under pthreads it is a sync-proxied async import (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.
  • Under ASYNCIFY/JSPI the 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/write never block (the Node.js backend buffers), and a blocking read()/write() on a socket fd is out of scope.

accept4 now also applies SOCK_NONBLOCK on top of the flags inherited from the listener; previously a SOCK_NONBLOCK accept off a blocking listener wrongly yielded a blocking socket.

Tested with test_noderawsockets_tcp_blocking (blocking accept + recv that must suspend) under both PROXY_TO_PTHREAD and JSPI, and test_noderawsockets_tcp_accept_nonblock, plus the mio suite under PROXY_TO_PTHREAD + NODERAWSOCKETS + NODERAWFS: 144 passed, 0 failed, 5 ignored.

Made with AI assistance under my review.

@guybedford
guybedford force-pushed the blocking-socket-ops branch from ab5b071 to c6bfc66 Compare July 14, 2026 02:59
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
guybedford force-pushed the blocking-socket-ops branch 3 times, most recently from d305cb6 to 4b28cc5 Compare August 20, 2026 03:40
@guybedford guybedford changed the title Support blocking socket ops via a single fd_wait primitive Support pthread blocking socket ops via a single fd_wait primitive Aug 20, 2026
@guybedford
guybedford force-pushed the blocking-socket-ops branch 5 times, most recently from afad990 to 676365f Compare August 22, 2026 02:06
Comment thread src/lib/libsyscall.js Outdated
Comment thread src/lib/libsyscall.js Outdated
});
}
#endif
return -{{{ cDefs.EAGAIN }}};

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.

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

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.

Actually it looks like maybe this symbol can simply be undefined in non-pthreads builds?

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.

I looks like this function could maybe just return a boolean?

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.

Updated to call a boolean and folded the function into JS, so the C macro is one JS call.

Comment thread src/lib/libsyscall.js Outdated
Comment thread ChangeLog.md Outdated
Comment thread system/lib/libc/musl/src/internal/emscripten_fd_wait.h Outdated
Comment thread system/lib/libc/musl/src/internal/emscripten_fd_wait.h
Comment thread system/lib/libc/musl/src/internal/emscripten_fd_wait.h Outdated
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
guybedford force-pushed the blocking-socket-ops branch from 676365f to c187b60 Compare August 23, 2026 17:12
…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
guybedford force-pushed the blocking-socket-ops branch from c187b60 to b740cea Compare August 23, 2026 17:12
@guybedford guybedford changed the title Support pthread blocking socket ops via a single fd_wait primitive Support blocking socket ops via a single fd_wait primitive Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants