Skip to content

Add Direct Sockets API support for Isolated Web Apps (-sDIRECT_SOCKETS)#26374

Open
maceip wants to merge 3 commits intoemscripten-core:mainfrom
maceip:direct-sockets-v2
Open

Add Direct Sockets API support for Isolated Web Apps (-sDIRECT_SOCKETS)#26374
maceip wants to merge 3 commits intoemscripten-core:mainfrom
maceip:direct-sockets-v2

Conversation

@maceip
Copy link

@maceip maceip commented Mar 2, 2026

replaces the websocket-to-posix-socket proxy with chromes Direct Sockets API (TCPSocket TCPServerSocket UDPSocket) for real tcp/udp networking from wasm in isolated web apps

context on changes from #26344

rewrite that incorporates all review feedback from #26344 and expands scope to fully support the direct socket api - driven by A) young jedis annoying me about web transport and B) the archive org folks asking about tor-in-wasm this past weekend in berlin - with my janky syscall wiring plus this patch you get unbelievable perf across udp [incl session tickets] and tcp - shout outs to emscripten core devs and blink/v8 devs this shouldnt be possible

feedback addressed from #26344

feedback fix
share fd allocator with FS / fd collision at 100 (@sbc100) socket fds now allocated via FS.createStream() using the SOCKFS pattern
use autoAddDeps instead of repeating __deps (@sbc100) added autoAddDeps(DirectSocketsLibrary '$DIRECT_SOCKETS')
use cDefs constants instead of hardcoded numbers (@sbc100) all sock opt constants now in struct_info json and referenced via cDefs
eof vs error distinction in read path (copilot) stream_ops read and readFromSocket check sock error before returning eof
parseSockaddr drops specific errno (copilot) returns {errno: X} all callers propagate the specific error
feature detection for TCPSocket/UDPSocket (copilot) abort() under ASSERTIONS -ENOSYS at runtime
bind-then-connect ignores local endpoint (copilot) connect() passes sock localAddress/localPort to constructor opts
udp bind+connect leaks socket (copilot) connect() closes existing sock udpSocket before creating new one
fd_close async issue (copilot) fire-and-forget via stream_ops close fd freed synchronously
add link to direct sockets spec (@sbc100) in file header

key architectural change: SOCKFS pattern

the original pr used a private fd allocator (nextFd: 100) - this version registers socket fds in emscriptens FS using FS.createNode() + FS.createStream() with custom stream_ops the same pattern SOCKFS uses for websocket-backed sockets - this means write(fd) and read(fd) route through direct sockets which is reqd by openssl (its socket BIO uses write()/read() not send()/recv())

stream_ops must be synchronous bc theyre called from js to js (FS.write -> stream_ops.write) not wasm to js so JSPI cant suspend:

  • write: fire-and-forget via writer.write() returns byte count immediately
  • read: consumes from recvQueue (filled by bg reader) throws EAGAIN if empty
  • poll: checks recvQueue.length for readability

new syscalls (beyond #26344)

syscall notes
setsockopt / getsockopt TCP_NODELAY SO_KEEPALIVE SO_RCVBUF SO_SNDBUF IP_ADD_MEMBERSHIP IP_DROP_MEMBERSHIP IP_MULTICAST_TTL IPV6_JOIN_GROUP IPV6_LEAVE_GROUP
poll via recvQueue length checks + async wait w timeout
pipe2 / socketpair in-memory pipe buffers w FS-backed fds
fcntl64 F_GETFL / F_SETFL for O_NONBLOCK
ioctl FIONBIO FIONREAD
write / read via FS stream_ops (the SOCKFS pattern)
_emscripten_lookup_name uses emscriptens std DNS (inetPton4 packed uint32)

files changed

file change
src/lib/libdirectsockets.js new all syscall impls
src/settings.js add DIRECT_SOCKETS flag
src/modules.mjs register libdirectsockets js when flag enabled
src/lib/libsyscall.js guard default socket impls when DIRECT_SOCKETS active
src/lib/libwasi.js fd_close path for direct socket fds
src/struct_info.json added sock opt constants (SO_REUSEADDR TCP_NODELAY IP_ADD_MEMBERSHIP etc)
site/source/docs/porting/networking.rst added direct sockets docs section

usage

emcc -sDIRECT_SOCKETS -sASYNCIFY -sPROXY_TO_PTHREAD -pthread app.c -o app.js

notes

  • setsockopt stub in emscripten_syscall_stubs.c is declared weak so the js lib impl takes priority automatically no need to modify stubs file
  • requires ASYNCIFY (or JSPI) - compile err if neither enabled
  • doh dns resolution split out to follow-up pr per @sbc100 feedback
  • pselect6 not impl yet - poll() covers most use cases and select() can be routed thru it

testing

web demo

demo.mp4

Replaces the WebSocket-to-POSIX-socket proxy with Chrome's Direct Sockets
API (TCPSocket, TCPServerSocket, UDPSocket) for real TCP/UDP networking
from WASM in Isolated Web Apps.

Socket fds are registered in Emscripten's FS using the SOCKFS pattern
(FS.createNode + FS.createStream with custom stream_ops), so write(fd)
and read(fd) work on socket file descriptors -- required by OpenSSL and
other libraries that use write()/read() instead of send()/recv().

New files:
  src/lib/libdirectsockets.js - all socket syscall implementations

Modified files:
  src/settings.js - adds DIRECT_SOCKETS flag
  src/modules.mjs - registers libdirectsockets.js when flag is enabled
  src/lib/libsyscall.js - guards default socket impls when active
  src/lib/libwasi.js - fd_close path for Direct Socket fds
  emscripten_syscall_stubs.c - comments out conflicting setsockopt stub

Usage: emcc -sDIRECT_SOCKETS -sJSPI -sPROXY_TO_PTHREAD -pthread app.c -o app.js

Tested with Tor (unmodified upstream C) compiled to WASM, bootstrapping
100% in ~15 seconds in a Chrome IWA, and a QUIC stack (ngtcp2 + wolfSSL +
nghttp3) achieving 90% of native throughput on UDP.
Copilot AI review requested due to automatic review settings March 2, 2026 02:48
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new -sDIRECT_SOCKETS build mode that routes POSIX socket syscalls to Chrome’s Direct Sockets API (TCPSocket/TCPServerSocket/UDPSocket) for real TCP/UDP networking in Isolated Web Apps, using an FS stream-backed “SOCKFS pattern” so read(fd)/write(fd) work for libraries like OpenSSL.

Changes:

  • Introduces src/lib/libdirectsockets.js implementing a Direct Sockets-backed syscall layer (socket/connect/bind/listen/accept/send/recv, poll, pipe2/socketpair, fcntl/ioctl, DoH name lookup).
  • Adds a new DIRECT_SOCKETS setting and wires the new library into calculateLibraries().
  • Adjusts existing syscall/WASI plumbing to avoid conflicting socket implementations when DIRECT_SOCKETS is enabled, and modifies a syscall stub.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/lib/libdirectsockets.js New Direct Sockets syscall backend + FS stream integration + DoH name lookup.
src/settings.js Adds DIRECT_SOCKETS build setting.
src/modules.mjs Adds libdirectsockets.js to the JS library list when enabled.
src/lib/libsyscall.js Disables SOCKFS socket syscalls when DIRECT_SOCKETS is active.
src/lib/libwasi.js Adds a fd_close branch for Direct Socket fds in no-filesystem WASI path.
system/lib/libc/emscripten_syscall_stubs.c Comments out the __syscall_setsockopt stub to avoid intercepting the JS implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@sbc100
Copy link
Collaborator

sbc100 commented Mar 4, 2026

Can you mention this new networking method in site/source/docs/porting/networking.rst?

createSocketState(family, type, protocol) {
#if ASSERTIONS
if (typeof globalThis.TCPSocket === 'undefined' &&
typeof globalThis.UDPSocket === 'undefined') {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can just do if (!globalThis.TCPSocket && !globalThis.UDPSocket)

var IP_MULTICAST_TTL = 33, IP_MULTICAST_LOOP = 34;
var IP_ADD_MEMBERSHIP = 35, IP_DROP_MEMBERSHIP = 36;
var IPV6_MULTICAST_LOOP = 18, IPV6_MULTICAST_HOPS = 19;
var IPV6_JOIN_GROUP = 20, IPV6_LEAVE_GROUP = 21;
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can add these to src/struct_info.json to make them available via cDefs (just run ./tools/gen_struct_info.py after ending).

return revents;
},

// Async DNS resolution via DNS-over-HTTPS (DoH)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This Async DNS resolution via DNS-over-HTTPS thing seems like a separate feature to the rest of direct sockets.

Perhaps split it out into its own PR?

* (https://wicg.github.io/direct-sockets/) to provide real TCP/UDP networking
* in Isolated Web Apps without needing a proxy server.
*/

Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps add #error here if ASYNCIFY is not defined (or error our somewhere else earlier).

@maceip
Copy link
Author

maceip commented Mar 5, 2026

apologies for not getting into this sooner im in between jobs and tokens

@maceip
Copy link
Author

maceip commented Mar 5, 2026

done - added direct sockets section to site/source/docs/porting/networking.rst w usage info and supported syscall list

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.

3 participants