Skip to content

Add -sNODENET backend for real outgoing TCP via node:net#27080

Open
guybedford wants to merge 1 commit into
emscripten-core:mainfrom
guybedford:nodenet
Open

Add -sNODENET backend for real outgoing TCP via node:net#27080
guybedford wants to merge 1 commit into
emscripten-core:mainfrom
guybedford:nodenet

Conversation

@guybedford

@guybedford guybedford commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

This adds a new -sNODENET setting that for supporting direct full sockets on Node.js via the node:net module, without needing ws, an external proxy process, or pthreads.

It is certainly possible to extend this to UDP and incoming connections, down to some API quirks, but for now this is kept very minimal. If there is interest in a more comprehensive PR that covers the full surface area for sockets, I'm open to assisting with that further as well as a follow-on.

For comprehensive setsockopts support I just posted nodejs/node#63825 as well so we can close the loop on all options being supported and that is effectively used in a backwards compatible way here despite not landing yet.

Perhaps in future we can consider alternative backends like https://sockets-api.proposal.wintertc.org/, but since that API is not yet supported on Node.js, for now this approach can cover Emscripten outbound connections for both Node.js and Cloudflare Workers.

Note: AI was used to create this PR, under my review.

Adds a new NODENET setting that backs the POSIX sockets API directly with
Node.js's node:net module, giving real, non-blocking outgoing (client) TCP
sockets without WebSockets, an external proxy process, or pthreads.

Unlike PROXY_POSIX_SOCKETS this is single-threaded and event-driven: socket
readiness is delivered through the same emscripten_set_socket_*_callback hooks
the default WebSocket backend uses, so it drops into existing readiness reactors
unchanged.

This initial backend supports outgoing TCP only: connect, send, recv and close,
plus get/setsockopt (SO_ERROR, TCP_NODELAY, SO_KEEPALIVE and the TCP keep-alive
tunables). There is no bind/listen/accept (server) support and no UDP yet; those
land in follow-ups.

- new nodenet_sock_ops in libsockfs.js implementing the sock_ops contract over
  net.createConnection
- route get/setsockopt through the backend under -sNODENET, and compile out the
  weak __syscall_setsockopt stub via a libstubs variation so the JS symbol wins
- test/sockets/test_nodenet.c: outgoing connect/send/recv against a loopback
  echo server started by the test harness

@sbc100 sbc100 left a comment

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.

Nice! I've not yet reviewed the meat of libsockfs.js but looks good so far.

Comment thread src/settings.js
// [link]
var PROXY_POSIX_SOCKETS = false;

// If 1, the POSIX sockets API is backed by Node.js's ``node:net`` module,

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 1," => "If enabled,"

Comment thread src/settings.js

// If 1, the POSIX sockets API is backed by Node.js's ``node:net`` module,
// giving real non-blocking outgoing TCP sockets with no WebSockets, proxy
// process or pthreads. This only works under node and is ignored elsewhere.

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.

You should mention something about how this is similar to what NODERAWFS does for filesystem access?

Speaking of which, should we perhaps combine them? Or at least have NODERAWFS automatically enable NODENET. Its hard to imagine wanting one without that other maybe?

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.

Also, regarding the name of this settings, should we use the word SOCK rather then NET since that seems to be what we have done previously. I guess -sNODERAWSOCKETS is kind of a mouthfull.

Maybe putting it behind the existing NODERAWFS avoids having to name something new ? :)

Comment thread tools/system_libs.py

@classmethod
def get_default_variation(cls, **kwargs):
return super().get_default_variation(nodenet=settings.NODENET, **kwargs)

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.

Its a little unfortunate that we need to crate this new variant here.

I wonder if we should perhaps instead move __syscall_setsockopt into JS (even when its stub). The downside is that the folks who call the stub version needless call out the JS, but the upside is simplicity in the number of variations of this native library.

Folks we really care about the cost of the stub __syscall_setsockopt can just stop calling it (or implement their own stub).

Comment thread test/test_other.py
finally:
server.shutdown()
server.server_close()
thread.join()

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 wonder if this should go in test_sockets.py? I'm not sure..

Comment thread test/test_other.py
if data:
self.request.sendall(data)

server = socketserver.TCPServer(('127.0.0.1', 0), EchoHandler)

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.

In test_sockets.py we preallocate specific ports, but I guess this maybe even better than doing that?

* found in the LICENSE file.
*/

// Outgoing TCP over the NODENET backend. We connect to a loopback echo server

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.

This test is not really specific to NODENET, right? Maybe just call it test_tcp_echo.c?

@@ -0,0 +1,120 @@
/*
* Copyright 2024 The Emscripten Authors. All rights reserved.

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.

2026

static int client_fd = -1;
static struct sockaddr_in dest;
static int connected = 0;
static int ping_sent = 0;

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.

Use bool (stdbool.h) here for these state variables.

static int connected = 0;
static int ping_sent = 0;

static void finish(int result) {

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.

Would it be simpler just to remove all these static qualifiers? I imagine we are very inconsistent across our test code.

Comment thread src/lib/libsockfs.js
return 0;
}
},
#endif

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.

Would to be easy to split this out into a new file? libnode_sockfs.js or libsockfs_node.js

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