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
28 changes: 28 additions & 0 deletions site/source/docs/api_reference/emscripten.h.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1605,6 +1605,34 @@ Functions
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 becomes readable once resolution
completes. Wait on it with ``poll``/``select``/``epoll``, then 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
153 changes: 107 additions & 46 deletions src/lib/libcore.js
Original file line number Diff line number Diff line change
Expand Up @@ -1007,53 +1007,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
'$nodeSockHelpers',
#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 @@ -1123,9 +1131,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 @@ -1156,9 +1162,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 @@ -1167,6 +1171,22 @@ addToLibrary({
//
// try as a hostname
//
#if NODERAWSOCKETS
// /etc/hosts resolves synchronously (read fresh through emscripten's FS).
var hosts = nodeSockHelpers.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 @@ -1175,9 +1195,50 @@ 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
'$nodeSockHelpers',
#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 nodeSockHelpers.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.
// A name pre-warmed by emscripten_dns_lookup_async() answers from the shared
// cache; otherwise it must be resolved out-of-band via that async syscall.
if (nodeSockHelpers.dnsCacheGet(desc)) {
{{{ makeSetValue('out', '0', 'writeAddrInfoList(desc)', '*') }}};
return 0;
}
return {{{ cDefs.EAI_AGAIN }}};
#endif
}
return desc;
},

getnameinfo__deps: ['$DNS', '$readSockaddr', '$stringToUTF8'],
Expand Down
2 changes: 2 additions & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,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
81 changes: 81 additions & 0 deletions src/lib/libsockfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ addToLibrary({
// 'listen' has no readiness mapping; skip it.
if (flags) FS.getStream(fd)?.node.notifyListeners(flags);
},
// Mark an async-completion pseudo-socket ready: flip it readable and wake
// its waiters through the generic wait-queue. A future ring/aio completion
// fd would reuse the same mechanism rather than re-adding one.
finishDns(sock) {
sock.dnsDone = true;
sock.stream.node?.notifyListeners({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}});
},
mount(mount) {
#if expectToReceiveOnModule('websocket')
// The incoming Module['websocket'] can be used for configuring
Expand Down Expand Up @@ -166,6 +173,11 @@ addToLibrary({
},
poll(stream) {
var sock = stream.node.sock;
// A DNS request fd (emscripten_dns_lookup_async) is readable once the
// lookup completes; read the result with emscripten_dns_lookup_result.
if (sock.dns) {
return sock.dnsDone ? ({{{ cDefs.POLLRDNORM }}} | {{{ cDefs.POLLIN }}}) : 0;
}
return sock.sock_ops.poll(sock);
},
ioctl(stream, request, varargs) {
Expand All @@ -188,6 +200,8 @@ addToLibrary({
},
close(stream) {
var sock = stream.node.sock;
// A DNS request fd is a pseudo-socket with no backend resources.
if (sock.dns) return;
sock.sock_ops.close(sock);
}
},
Expand Down Expand Up @@ -847,4 +861,71 @@ addToLibrary({
emscripten_set_socket_close_callback__deps: ['$_setNetworkCallback'],
emscripten_set_socket_close_callback: (userData, callback) =>
_setNetworkCallback('close', userData, callback),

// Asynchronous getaddrinfo: same (node, service, hint) inputs as the sync call.
// Returns a pollable fd that becomes readable when resolution completes (wait
// on it with poll/select/epoll); read the result
// with emscripten_dns_lookup_result. Returns -1 on failure to allocate the fd.
// Without -sNODERAWSOCKETS this resolves synchronously (like getaddrinfo) and
// the fd is simply readable on the next turn.
emscripten_dns_lookup_async__deps: ['$SOCKFS', '$getAddrInfo', '$safeSetTimeout',
#if NODERAWSOCKETS
'$nodeSockHelpers',
#endif
],
emscripten_dns_lookup_async__proxy: 'sync',
emscripten_dns_lookup_async: (node, service, hint) => {
var sock;
try {
sock = SOCKFS.createSocket({{{ cDefs.AF_INET }}}, {{{ cDefs.SOCK_STREAM }}}, 0);
} catch (e) {
return -1;
}
sock.dns = true;
// Read the request synchronously (the input pointers are only valid now). No
// C memory is allocated here; the resolved descriptor is stashed on the sock
// and minted into an addrinfo only when the caller takes it via
// emscripten_dns_lookup_result.
var desc = getAddrInfo(node, service, hint);
#if NODERAWSOCKETS
if (typeof desc === 'object' && !desc.entries) {
// A real hostname: resolve via node:dns (fills desc.entries), then stash
// the same descriptor for the caller to mint from.
nodeSockHelpers.resolveAddrInfo(desc).then((eai) => {
sock.dnsResult = eai;
sock.dnsDesc = desc;
SOCKFS.finishDns(sock);
});
return sock.stream.fd;
}
#endif
// Resolved synchronously (numeric/`/etc/hosts`/fake/null-node success, or a
// validation error). Deliver readiness on a later turn regardless, so the
// contract is uniformly async (the caller can poll or attach a listener
// first); safeSetTimeout keeps the runtime alive until it fires.
if (typeof desc === 'object') {
sock.dnsDesc = desc;
sock.dnsResult = 0;
} else {
sock.dnsResult = desc;
}
safeSetTimeout(() => SOCKFS.finishDns(sock), 0);
return sock.stream.fd;
},

// Read the outcome of a completed async lookup: 0 on success - minting the
// addrinfo list and writing its head to *res (freed with freeaddrinfo, as for
// getaddrinfo) - or an EAI_* code on failure (EAI_AGAIN if not yet complete).
// The memory is allocated here, so a caller that closes the fd without reading
// leaks nothing. The caller owns the fd and should close() it.
emscripten_dns_lookup_result__deps: ['$SOCKFS', '$writeAddrInfoList'],
emscripten_dns_lookup_result__proxy: 'sync',
emscripten_dns_lookup_result: (fd, res) => {
var sock = SOCKFS.getSocket(fd);
if (!sock || !sock.dns || !sock.dnsDone) return {{{ cDefs.EAI_AGAIN }}};
if (sock.dnsResult === 0) {
{{{ makeSetValue('res', '0', 'writeAddrInfoList(sock.dnsDesc)', '*') }}};
}
return sock.dnsResult;
},
});
Loading
Loading