Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions site/source/docs/api_reference/emscripten.h.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,36 @@ Functions
:param em_socket_callback callback: Pointer to a callback function. The callback returns a file descriptor and the arbitrary ``userData`` passed to this function.


.. c:function:: int emscripten_dns_lookup_async(const char *node, const char *service, const struct addrinfo *hints)

Asynchronous :c:func:`getaddrinfo`. Takes the same ``node``/``service``/``hints``
inputs and returns a file descriptor that signals completion in two
interchangeable ways - it becomes readable (via ``poll``/``select``), and it
delivers the socket message callback registered with
:c:func:`emscripten_set_socket_message_callback` for that fd. Read the result
with :c:func:`emscripten_dns_lookup_result`. The caller owns the fd and should
``close()`` it.

With ``-sNODERAWSOCKETS`` a hostname is resolved asynchronously via ``node:dns``;
otherwise (and for numeric or ``/etc/hosts`` names) resolution is synchronous,
as :c:func:`getaddrinfo`, and the fd is simply readable on the next turn.

:param node: The hostname or numeric address to resolve.
:param service: The service name or port string (may be ``NULL``).
:param hints: ``addrinfo`` filter (``ai_family``/``ai_socktype``/etc.; may be ``NULL``).
:returns: A pollable file descriptor, or ``-1`` on failure to start the lookup.


.. c:function:: int emscripten_dns_lookup_result(int fd, struct addrinfo **res)

Reads the outcome of a lookup started by :c:func:`emscripten_dns_lookup_async`,
once its ``fd`` is readable.

:param int fd: The file descriptor returned by :c:func:`emscripten_dns_lookup_async`.
:param res: On success, receives the head of the resulting ``addrinfo`` list (free it with :c:func:`freeaddrinfo`, as for :c:func:`getaddrinfo`).
:returns: ``0`` on success, or an ``EAI_*`` error code on failure (``EAI_AGAIN`` if the lookup has not completed yet).


Unaligned types
===============

Expand Down
28 changes: 28 additions & 0 deletions site/source/docs/tools_reference/settings_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,34 @@ sockets calls from browser to native world.

Default value: false

.. _noderawsockets:

NODERAWSOCKETS
==============

If enabled, 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 is the sockets counterpart to NODERAWFS:
where NODERAWFS gives direct access to the host filesystem, this gives
direct access to host sockets. It only works under node and is ignored
elsewhere.

It supports full TCP (outgoing connect plus bind, listen and accept for
servers) and UDP. TCP clients use the public node:net API when possible,
falling back to the private tcp_wrap/udp_wrap handles on older Node.js.

It is event-driven. Socket readiness comes through the same
``emscripten_set_socket_*_callback`` hooks the WebSocket backend uses, so it
works with existing readiness reactors. It cannot be combined with the
WebSocket emulation, PROXY_POSIX_SOCKETS or SOCKET_WEBRTC.

It works under -pthread with PROXY_TO_PTHREAD, where main() and every socket
syscall run on a single worker alongside the node handles and their event
loop. As with the WebSocket backend, sharing a socket across threads under a
plain -pthread build (without PROXY_TO_PTHREAD) is not supported.

Default value: false

.. _websocket_subprotocol:

WEBSOCKET_SUBPROTOCOL
Expand Down
148 changes: 102 additions & 46 deletions src/lib/libcore.js
Original file line number Diff line number Diff line change
Expand Up @@ -947,53 +947,61 @@ addToLibrary({
return inetPton4(DNS.lookup_name(nameString));
},

getaddrinfo__deps: ['$DNS', '$inetPton4', '$inetNtop4', '$inetPton6', '$inetNtop6', '$writeSockaddr', 'malloc', 'htonl'],
getaddrinfo__proxy: 'sync',
getaddrinfo: (node, service, hint, out) => {
// Note getaddrinfo currently only returns a single addrinfo with ai_next defaulting to NULL. When NULL
// hints are specified or ai_family set to AF_UNSPEC or ai_socktype or ai_protocol set to 0 then we
// really should provide a linked list of suitable addrinfo values.
var addrs = [];
var canon = null;
var addr = 0;
var port = 0;
var flags = 0;
var family = {{{ cDefs.AF_UNSPEC }}};
var type = 0;
var proto = 0;
var ai, last;

function allocaddrinfo(family, type, proto, canon, addr, port) {
var sa, salen, ai;
var errno;

salen = family === {{{ cDefs.AF_INET6 }}} ?
// The encode/mint stage: turn a resolved descriptor ({entries, type, proto,
// port}, addr in parsed inetPton form) into an addrinfo linked list and return
// the head (0 for an empty list). This is the sole point that mints C memory,
// and the whole chain is freed uniformly by freeaddrinfo - one ownership rule.
// (A future ring/aio backend would add a sibling encoder here, e.g. one that
// writes into a caller buffer, without touching parse/resolve.)
$writeAddrInfoList__deps: ['$inetNtop4', '$inetNtop6', '$writeSockaddr', 'malloc'],
$writeAddrInfoList: (desc) => {
var head = 0, prev = 0;
for (var entry of desc.entries) {
var family = entry.family;
var salen = family === {{{ cDefs.AF_INET6 }}} ?
{{{ C_STRUCTS.sockaddr_in6.__size__ }}} :
{{{ C_STRUCTS.sockaddr_in.__size__ }}};
addr = family === {{{ cDefs.AF_INET6 }}} ?
inetNtop6(addr) :
inetNtop4(addr);
sa = _malloc(salen);
errno = writeSockaddr(sa, family, addr, port);
var sa = _malloc(salen);
var errno = writeSockaddr(sa, family, family === {{{ cDefs.AF_INET6 }}} ? inetNtop6(entry.addr) : inetNtop4(entry.addr), desc.port);
#if ASSERTIONS
assert(!errno);
#endif

ai = _malloc({{{ C_STRUCTS.addrinfo.__size__ }}});
var ai = _malloc({{{ C_STRUCTS.addrinfo.__size__ }}});
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_family, 'family', 'i32') }}};
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_socktype, 'type', 'i32') }}};
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_protocol, 'proto', 'i32') }}};
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_canonname, 'canon', '*') }}};
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_socktype, 'desc.type', 'i32') }}};
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_protocol, 'desc.proto', 'i32') }}};
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_canonname, '0', '*') }}};
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_addr, 'sa', '*') }}};
if (family === {{{ cDefs.AF_INET6 }}}) {
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_addrlen, C_STRUCTS.sockaddr_in6.__size__, 'i32') }}};
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_addrlen, 'salen', 'i32') }}};
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_next, '0', 'i32') }}};
if (prev) {
{{{ makeSetValue('prev', C_STRUCTS.addrinfo.ai_next, 'ai', '*') }}};
} else {
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_addrlen, C_STRUCTS.sockaddr_in.__size__, 'i32') }}};
head = ai;
}
{{{ makeSetValue('ai', C_STRUCTS.addrinfo.ai_next, '0', 'i32') }}};

return ai;
prev = ai;
}
return head;
},

// Shared getaddrinfo core. Allocates nothing: returns a resolved descriptor
// {entries, type, proto, port} (entries are {family, addr} with addr in parsed
// inetPton form), a negative EAI_* code on failure, or - under NODERAWSOCKETS,
// for a hostname needing DNS - a {node, family, type, proto, port} descriptor
// (no entries) for the caller to resolve. The result is minted from a
// descriptor by writeAddrInfoList at the point ownership passes to the caller.
$getAddrInfo__deps: ['$DNS', '$inetPton4', '$inetPton6', 'htonl', '$UTF8ToString',
#if NODERAWSOCKETS
'$nodeSockOps',
#endif
],
$getAddrInfo: (node, service, hint) => {
var addr = 0;
var port = 0;
var flags = 0;
var family = {{{ cDefs.AF_UNSPEC }}};
var type = 0;
var proto = 0;

if (hint) {
flags = {{{ makeGetValue('hint', C_STRUCTS.addrinfo.ai_flags, 'i32') }}};
Expand Down Expand Up @@ -1063,9 +1071,7 @@ addToLibrary({
addr = [0, 0, 0, _htonl(1)];
}
}
ai = allocaddrinfo(family, type, proto, null, addr, port);
{{{ makeSetValue('out', '0', 'ai', '*') }}};
return 0;
return { entries: [{ family, addr }], type, proto, port };
}

//
Expand Down Expand Up @@ -1096,9 +1102,7 @@ addToLibrary({
}
}
if (addr != null) {
ai = allocaddrinfo(family, type, proto, node, addr, port);
{{{ makeSetValue('out', '0', 'ai', '*') }}};
return 0;
return { entries: [{ family, addr }], type, proto, port };
}
if (flags & {{{ cDefs.AI_NUMERICHOST }}}) {
return {{{ cDefs.EAI_NONAME }}};
Expand All @@ -1107,6 +1111,22 @@ addToLibrary({
//
// try as a hostname
//
#if NODERAWSOCKETS
// /etc/hosts resolves synchronously (read fresh through emscripten's FS).
var hosts = nodeSockOps.readHosts(node).filter((e) =>
family === {{{ cDefs.AF_UNSPEC }}} || e.family === family);
if (hosts.length) {
var entries = hosts.map((e) => ({
family: e.family,
addr: e.family === {{{ cDefs.AF_INET6 }}} ? inetPton6(e.addr) : inetPton4(e.addr),
}));
return { entries, type, proto, port };
}
// A real hostname needs a DNS lookup; hand the request back to the caller to
// resolve asynchronously (getaddrinfo suspends under JSPI / returns
// EAI_AGAIN otherwise; emscripten_dns_lookup_async drives the poll-fd flow).
return { node, family, type, proto, port };
#else
// resolve the hostname to a temporary fake address
node = DNS.lookup_name(node);
addr = inetPton4(node);
Expand All @@ -1115,9 +1135,45 @@ addToLibrary({
} else if (family === {{{ cDefs.AF_INET6 }}}) {
addr = [0, 0, _htonl(0xffff), addr];
}
ai = allocaddrinfo(family, type, proto, null, addr, port);
{{{ makeSetValue('out', '0', 'ai', '*') }}};
return 0;
return { entries: [{ family, addr }], type, proto, port };
#endif
},

getaddrinfo__deps: ['$getAddrInfo', '$writeAddrInfoList',
#if NODERAWSOCKETS && ASYNCIFY == 2
'$nodeSockOps',
#endif
],
getaddrinfo__proxy: 'sync',
#if NODERAWSOCKETS && ASYNCIFY == 2
// Under JSPI a hostname miss suspends the wasm stack on the real node:dns
// lookup (returning a promise) rather than reporting EAI_AGAIN. A resolved
// descriptor (numeric/hosts) or error does not suspend.
getaddrinfo__async: true,
#endif
getaddrinfo: (node, service, hint, out) => {
// parse -> (resolve) -> mint. One descriptor threads through all three.
var desc = getAddrInfo(node, service, hint);
if (typeof desc === 'object') {
if (desc.entries) {
{{{ makeSetValue('out', '0', 'writeAddrInfoList(desc)', '*') }}};
return 0;
}
#if NODERAWSOCKETS && ASYNCIFY == 2
// JSPI: suspend on the real node:dns lookup, which fills desc.entries, then
// mint from the same descriptor.
return nodeSockOps.resolveAddrInfo(desc).then((eai) => {
if (eai) return eai;
{{{ makeSetValue('out', '0', 'writeAddrInfoList(desc)', '*') }}};
return 0;
});
#elif NODERAWSOCKETS
// No synchronous DNS available: numeric and /etc/hosts names resolve above,
// anything else must be resolved out-of-band via emscripten_dns_lookup_async.
return {{{ cDefs.EAI_AGAIN }}};
#endif
}
return desc;
},

getnameinfo__deps: ['$DNS', '$readSockaddr', '$stringToUTF8'],
Expand Down
3 changes: 3 additions & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ sigs = {
__syscall_rmdir__sig: 'ip',
__syscall_sendmsg__sig: 'iipiiii',
__syscall_sendto__sig: 'iippipi',
__syscall_setsockopt__sig: 'iiiipii',
__syscall_shutdown__sig: 'iiiiiii',
__syscall_socket__sig: 'iiiiiii',
__syscall_stat64__sig: 'ipp',
Expand Down Expand Up @@ -632,6 +633,8 @@ sigs = {
emscripten_destroy_audio_context__sig: 'vi',
emscripten_destroy_web_audio_node__sig: 'vi',
emscripten_destroy_worker__sig: 'vi',
emscripten_dns_lookup_async__sig: 'ippp',
emscripten_dns_lookup_result__sig: 'iip',
emscripten_enter_soft_fullscreen__sig: 'ipp',
emscripten_err__sig: 'vp',
emscripten_errn__sig: 'vpp',
Expand Down
Loading
Loading