diff --git a/site/source/docs/api_reference/emscripten.h.rst b/site/source/docs/api_reference/emscripten.h.rst index 87a680a412647..d2fa885534a48 100644 --- a/site/source/docs/api_reference/emscripten.h.rst +++ b/site/source/docs/api_reference/emscripten.h.rst @@ -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 =============== diff --git a/src/lib/libcore.js b/src/lib/libcore.js index cd6f66dee51fe..f62410900df77 100644 --- a/src/lib/libcore.js +++ b/src/lib/libcore.js @@ -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') }}}; @@ -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 }; } // @@ -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 }}}; @@ -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); @@ -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'], diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index 89e5cba52aa9b..5ecb97b2fb582 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -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', diff --git a/src/lib/libsockfs.js b/src/lib/libsockfs.js index 07e39ef5afc3d..113a528a888e1 100644 --- a/src/lib/libsockfs.js +++ b/src/lib/libsockfs.js @@ -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 @@ -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) { @@ -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); } }, @@ -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; + }, }); diff --git a/src/lib/libsockfs_node.js b/src/lib/libsockfs_node.js index f939744f41bf5..bba92abf1c93c 100644 --- a/src/lib/libsockfs_node.js +++ b/src/lib/libsockfs_node.js @@ -54,7 +54,7 @@ null; var NodeSockFSLibrary = { // Node plumbing shared by the interface methods below. - $nodeSockHelpers__deps: ['$SOCKFS', '$ERRNO_CODES'], + $nodeSockHelpers__deps: ['$SOCKFS', '$ERRNO_CODES', '$FS', '$inetPton4', '$inetPton6'], $nodeSockHelpers: { // node builtins, resolved once each. getBuiltinModule works in both // CommonJS and ESM output, with require as the fallback. @@ -67,6 +67,91 @@ var NodeSockFSLibrary = { getDgram() { return nodeSockHelpers.dgramModule ??= (process.getBuiltinModule || require)('dgram'); }, + getDns() { + return nodeSockHelpers.dnsModule ??= (process.getBuiltinModule || require)('dns'); + }, + // Look up `name` in /etc/hosts, read fresh on each call through emscripten's + // FS so live edits (MEMFS or a mounted real fs) are honored. Returns a list + // of {family, addr}; a missing or unreadable file is just empty. + readHosts(name) { + var out = []; + var text; + try { + text = FS.readFile('/etc/hosts', { encoding: 'utf8' }); + } catch (e) { + return out; + } + for (var line of text.split('\n')) { + var hash = line.indexOf('#'); + if (hash !== -1) line = line.slice(0, hash); + var parts = line.split(/\s+/).filter((p) => p.length); + if (parts.length < 2 || !parts.slice(1).includes(name)) continue; + var addr = parts[0]; + out.push({ family: addr.includes(':') ? {{{ cDefs.AF_INET6 }}} : {{{ cDefs.AF_INET }}}, addr }); + } + return out; + }, + // Map a node:dns error to an EAI_* code. node:dns surfaces either getaddrinfo + // EAI_* names or libuv/system codes; the transient ones become EAI_AGAIN and + // everything else a hard "name not found". + eaiForDns(e) { + switch (e && e.code) { + case 'EAI_AGAIN': + case 'ETIMEDOUT': + case 'ESERVFAIL': + case 'EREFUSED': + return {{{ cDefs.EAI_AGAIN }}}; + default: + return {{{ cDefs.EAI_NONAME }}}; + } + }, + // Resolved hostnames keyed by family + name, shared by the sync and async + // getaddrinfo paths: an async resolution pre-warms this so a later sync + // getaddrinfo() (which has no synchronous DNS of its own) can answer from it + // instead of returning EAI_AGAIN. + dnsCache: {}, + dnsCacheKey(desc) { + return desc.family + ':' + desc.node; + }, + // Fill desc.entries from the cache if present; true on a hit. + dnsCacheGet(desc) { + var entries = nodeSockHelpers.dnsCache[nodeSockHelpers.dnsCacheKey(desc)]; + if (entries) { + desc.entries = entries; + return true; + } + return false; + }, + // The resolve stage: take a needs-DNS descriptor (from getAddrInfo) and fill + // in desc.entries via node:dns, populating the shared cache on success. + // Returns a promise of the EAI_* code (0 on success). Pure: no fd, no C + // allocation - just name -> addresses, so a future ring/aio backend can reuse + // it verbatim. + resolveAddrInfo(desc) { + if (nodeSockHelpers.dnsCacheGet(desc)) { + return Promise.resolve(0); + } + var opts = { all: true }; + if (desc.family === {{{ cDefs.AF_INET }}}) opts.family = 4; + else if (desc.family === {{{ cDefs.AF_INET6 }}}) opts.family = 6; + return new Promise((resolve) => { + nodeSockHelpers.getDns().lookup(desc.node, opts, (err, addresses) => { + if (err) { + resolve(nodeSockHelpers.eaiForDns(err)); + } else { + desc.entries = addresses.map((a) => { + var fam = a.family === 6 ? {{{ cDefs.AF_INET6 }}} : {{{ cDefs.AF_INET }}}; + return { family: fam, addr: fam === {{{ cDefs.AF_INET6 }}} ? inetPton6(a.address) : inetPton4(a.address) }; + }); + if (addresses.length) { + nodeSockHelpers.dnsCache[nodeSockHelpers.dnsCacheKey(desc)] = desc.entries; + } + resolve(addresses.length ? 0 : {{{ cDefs.EAI_NONAME }}}); + } + }); + }); + }, + // True when node:dgram exposes both synchronous bindSync and connectSync // (a recent addition), letting UDP run entirely on the public API. A runtime // missing either falls back to the private udp_wrap handle, which provides diff --git a/src/struct_info.json b/src/struct_info.json index be92ff18a8d9c..9d68f3a0f7659 100644 --- a/src/struct_info.json +++ b/src/struct_info.json @@ -206,7 +206,8 @@ "NI_NAMEREQD", "EAI_NONAME", "EAI_SOCKTYPE", - "EAI_BADFLAGS" + "EAI_BADFLAGS", + "EAI_AGAIN" ], "structs": { "addrinfo": [ diff --git a/src/struct_info_generated.json b/src/struct_info_generated.json index e266b9eb7d2d8..e8cad551d543c 100644 --- a/src/struct_info_generated.json +++ b/src/struct_info_generated.json @@ -64,6 +64,7 @@ "EADV": 122, "EAFNOSUPPORT": 5, "EAGAIN": 6, + "EAI_AGAIN": -3, "EAI_BADFLAGS": -1, "EAI_FAMILY": -6, "EAI_NONAME": -2, diff --git a/src/struct_info_generated_wasm64.json b/src/struct_info_generated_wasm64.json index 115caf29cd902..c08719f390c1f 100644 --- a/src/struct_info_generated_wasm64.json +++ b/src/struct_info_generated_wasm64.json @@ -64,6 +64,7 @@ "EADV": 122, "EAFNOSUPPORT": 5, "EAGAIN": 6, + "EAI_AGAIN": -3, "EAI_BADFLAGS": -1, "EAI_FAMILY": -6, "EAI_NONAME": -2, diff --git a/system/include/emscripten/emscripten.h b/system/include/emscripten/emscripten.h index 43d2f2899dd0e..4e080277dfe7a 100644 --- a/system/include/emscripten/emscripten.h +++ b/system/include/emscripten/emscripten.h @@ -68,6 +68,22 @@ void emscripten_set_socket_connection_callback(void *userData, em_socket_callbac void emscripten_set_socket_message_callback(void *userData, em_socket_callback callback); void emscripten_set_socket_close_callback(void *userData, em_socket_callback callback); +// Asynchronous getaddrinfo. emscripten_dns_lookup_async() takes the same +// node/service/hints inputs as getaddrinfo() and returns a file descriptor that +// becomes readable once resolution completes, or -1 on error. Wait on it with +// poll/select/epoll. Once readable, call emscripten_dns_lookup_result() +// to read the outcome: 0 on success - writing the addrinfo list head to *res +// (free it with freeaddrinfo, as for getaddrinfo) - or an EAI_* code on failure. +// The caller owns the fd and should close() it. +// With -sNODERAWSOCKETS a hostname is resolved asynchronously via node:dns; +// otherwise resolution is synchronous (as getaddrinfo) and the fd is simply +// readable on the next turn. A successful resolution pre-warms getaddrinfo()'s +// cache, so a subsequent synchronous getaddrinfo() of the same name resolves +// from it rather than returning EAI_AGAIN. +struct addrinfo; +int emscripten_dns_lookup_async(const char *node, const char *service, const struct addrinfo *hints); +int emscripten_dns_lookup_result(int fd, struct addrinfo **res); + void _emscripten_push_main_loop_blocker(em_arg_callback_func func, void *arg, const char *name); void _emscripten_push_uncounted_main_loop_blocker(em_arg_callback_func func, void *arg, const char *name); #define emscripten_push_main_loop_blocker(func, arg) \ diff --git a/system/lib/libc/musl/src/network/freeaddrinfo.c b/system/lib/libc/musl/src/network/freeaddrinfo.c index c4016d9f7c246..6e075a261adea 100644 --- a/system/lib/libc/musl/src/network/freeaddrinfo.c +++ b/system/lib/libc/musl/src/network/freeaddrinfo.c @@ -7,11 +7,15 @@ void freeaddrinfo(struct addrinfo *p) { #if __EMSCRIPTEN__ - // Emscripten's usage of this structure is very simple: we always allocate - // ai_addr, and do not use the linked list aspect at all. There is also no - // aliasing with aibuf. - free(p->ai_addr); - free(p); + // Emscripten allocates each node and its ai_addr separately (no aibuf block, + // no aliasing), and getaddrinfo may return a linked list, so walk it freeing + // each node and its address. + while (p) { + struct addrinfo *next = p->ai_next; + free(p->ai_addr); + free(p); + p = next; + } #else size_t cnt; for (cnt=1; p->ai_next; cnt++, p=p->ai_next); diff --git a/test/codesize/test_codesize_hello_O0.json b/test/codesize/test_codesize_hello_O0.json index 8ad48f5be16de..f569787287205 100644 --- a/test/codesize/test_codesize_hello_O0.json +++ b/test/codesize/test_codesize_hello_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 23471, - "a.out.js.gz": 8555, + "a.out.js": 23501, + "a.out.js.gz": 8568, "a.out.nodebug.wasm": 15115, "a.out.nodebug.wasm.gz": 7464, - "total": 38586, - "total_gz": 16019, + "total": 38616, + "total_gz": 16032, "sent": [ "fd_write" ], diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index af64e9d6e9a57..ace84267f5229 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 270568, - "a.out.nodebug.wasm": 588342, - "total": 858910, + "a.out.js": 271187, + "a.out.nodebug.wasm": 588365, + "total": 859552, "sent": [ "IMG_Init", "IMG_Load", @@ -467,6 +467,8 @@ "emscripten_date_now", "emscripten_debugger", "emscripten_destroy_worker", + "emscripten_dns_lookup_async", + "emscripten_dns_lookup_result", "emscripten_enter_soft_fullscreen", "emscripten_err", "emscripten_errn", diff --git a/test/codesize/test_codesize_minimal_O0.expected.js b/test/codesize/test_codesize_minimal_O0.expected.js index 1b627a7f0a2cf..f02acb503c28c 100644 --- a/test/codesize/test_codesize_minimal_O0.expected.js +++ b/test/codesize/test_codesize_minimal_O0.expected.js @@ -840,6 +840,8 @@ Module['FS_createPreloadedFile'] = FS.createPreloadedFile; 'inetNtop6', 'readSockaddr', 'writeSockaddr', + 'writeAddrInfoList', + 'getAddrInfo', 'readEmAsmArgs', 'jstoi_q', 'getExecutableName', diff --git a/test/codesize/test_codesize_minimal_O0.json b/test/codesize/test_codesize_minimal_O0.json index 5cf6c7417b280..ea9b231f904f6 100644 --- a/test/codesize/test_codesize_minimal_O0.json +++ b/test/codesize/test_codesize_minimal_O0.json @@ -1,10 +1,10 @@ { - "a.out.js": 18680, - "a.out.js.gz": 6732, + "a.out.js": 18710, + "a.out.js.gz": 6744, "a.out.nodebug.wasm": 1015, "a.out.nodebug.wasm.gz": 602, - "total": 19695, - "total_gz": 7334, + "total": 19725, + "total_gz": 7346, "sent": [], "imports": [], "exports": [ diff --git a/test/codesize/test_unoptimized_code_size.json b/test/codesize/test_unoptimized_code_size.json index 76798f8761f3a..dcb47287d04b1 100644 --- a/test/codesize/test_unoptimized_code_size.json +++ b/test/codesize/test_unoptimized_code_size.json @@ -1,16 +1,16 @@ { - "hello_world.js": 54550, - "hello_world.js.gz": 17328, + "hello_world.js": 54590, + "hello_world.js.gz": 17350, "hello_world.wasm": 15115, "hello_world.wasm.gz": 7464, "no_asserts.js": 23629, "no_asserts.js.gz": 8288, "no_asserts.wasm": 12229, "no_asserts.wasm.gz": 6004, - "strict.js": 51701, - "strict.js.gz": 16338, + "strict.js": 51741, + "strict.js.gz": 16359, "strict.wasm": 15115, "strict.wasm.gz": 7461, - "total": 172339, - "total_gz": 62883 + "total": 172419, + "total_gz": 62926 } diff --git a/test/sockets/test_dns_async.c b/test/sockets/test_dns_async.c new file mode 100644 index 0000000000000..21c2a2cdbe6ee --- /dev/null +++ b/test/sockets/test_dns_async.c @@ -0,0 +1,124 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * Exercises the -sNODERAWSOCKETS DNS path. getaddrinfo() resolves numeric and + * /etc/hosts names synchronously (the latter read through emscripten's FS) and + * returns EAI_AGAIN for a real hostname. emscripten_dns_lookup_async() is the + * asynchronous getaddrinfo: it takes the same node/service/hints and returns a + * pollable fd; emscripten_dns_lookup_result() then yields the addrinfo payload. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int lookup_fd = -1; + +static void fail(const char* why) { + printf("DNS ASYNC FAIL: %s\n", why); + abort(); +} + +// getaddrinfo() of an AF_INET hostname, returning its first address (or *err). +static unsigned ipv4_of(const char* name, int* err_out) { + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + struct addrinfo* res = NULL; + int err = getaddrinfo(name, NULL, &hints, &res); + if (err_out) *err_out = err; + if (err != 0) return 0; + unsigned addr = ((struct sockaddr_in*)res->ai_addr)->sin_addr.s_addr; + freeaddrinfo(res); + return addr; +} + +static void main_loop(void) { + fd_set fdr; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_SET(lookup_fd, &fdr); + select(lookup_fd + 1, &fdr, NULL, NULL, &tv); + if (!FD_ISSET(lookup_fd, &fdr)) { + return; // resolution still pending + } + + // The result is delivered directly as an addrinfo payload, in the same format + // getaddrinfo() produces (and freed the same way). + struct addrinfo* res = NULL; + int result = emscripten_dns_lookup_result(lookup_fd, &res); + close(lookup_fd); + if (result != 0) fail("async lookup failed"); + assert(res); + if (res->ai_socktype != SOCK_STREAM) fail("async result lost the requested socktype"); + unsigned addr = ((struct sockaddr_in*)res->ai_addr)->sin_addr.s_addr; + freeaddrinfo(res); + if (addr != htonl(INADDR_LOOPBACK)) fail("localhost did not resolve to 127.0.0.1"); + + printf("DNS ASYNC PASS\n"); + emscripten_cancel_main_loop(); +} + +int main(void) { + // Seed /etc/hosts (through emscripten's FS) with names node:dns could never + // resolve, including one mapped to multiple addresses. + mkdir("/etc", 0777); + FILE* f = fopen("/etc/hosts", "w"); + assert(f); + fputs("# test hosts\n" + "10.1.2.3 statichost.test\n" + "192.0.2.1 multi.test\n" + "192.0.2.2 multi.test\n", + f); + fclose(f); + + // /etc/hosts resolves synchronously through getaddrinfo. + int err = 0; + unsigned static_addr = ipv4_of("statichost.test", &err); + if (err != 0) fail("static host not resolved from /etc/hosts"); + if (static_addr != inet_addr("10.1.2.3")) fail("static host wrong address"); + + // A name with several addresses comes back as an addrinfo linked list, freed + // as a whole by freeaddrinfo. + struct addrinfo mhints = {0}; + mhints.ai_family = AF_INET; + mhints.ai_socktype = SOCK_STREAM; + struct addrinfo* mres = NULL; + if (getaddrinfo("multi.test", NULL, &mhints, &mres) != 0) fail("multi host not resolved"); + int count = 0, seen1 = 0, seen2 = 0; + for (struct addrinfo* ai = mres; ai; ai = ai->ai_next) { + unsigned a = ((struct sockaddr_in*)ai->ai_addr)->sin_addr.s_addr; + if (a == inet_addr("192.0.2.1")) seen1 = 1; + if (a == inet_addr("192.0.2.2")) seen2 = 1; + count++; + } + freeaddrinfo(mres); + if (count != 2 || !seen1 || !seen2) fail("multi host did not return both addresses"); + + // A real hostname not in /etc/hosts has no synchronous resolution. + ipv4_of("localhost", &err); + if (err != EAI_AGAIN) fail("real hostname should be EAI_AGAIN"); + + // Resolve it asynchronously; the result arrives via the pollable fd. + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + lookup_fd = emscripten_dns_lookup_async("localhost", NULL, &hints); + if (lookup_fd < 0) fail("async lookup did not return an fd"); + + emscripten_set_main_loop(main_loop, 0, 0); + return 0; +} diff --git a/test/sockets/test_dns_async_default.c b/test/sockets/test_dns_async_default.c new file mode 100644 index 0000000000000..543d3ae033dbb --- /dev/null +++ b/test/sockets/test_dns_async_default.c @@ -0,0 +1,62 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * The async getaddrinfo API is available without -sNODERAWSOCKETS too: there it + * resolves synchronously (the same fake address getaddrinfo() returns) and the + * fd is simply readable on the next turn, so integration code need not branch on + * the backend. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int lookup_fd = -1; + +static void fail(const char* why) { + printf("DNS ASYNC DEFAULT FAIL: %s\n", why); + abort(); +} + +static void main_loop(void) { + fd_set fdr; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_SET(lookup_fd, &fdr); + select(lookup_fd + 1, &fdr, NULL, NULL, &tv); + if (!FD_ISSET(lookup_fd, &fdr)) { + return; + } + + struct addrinfo* res = NULL; + int result = emscripten_dns_lookup_result(lookup_fd, &res); + close(lookup_fd); + if (result != 0) fail("async lookup failed"); + assert(res && res->ai_addr); + if (res->ai_socktype != SOCK_STREAM) fail("async result lost the requested socktype"); + freeaddrinfo(res); + + printf("DNS ASYNC DEFAULT PASS\n"); + emscripten_cancel_main_loop(); +} + +int main(void) { + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + lookup_fd = emscripten_dns_lookup_async("example.com", NULL, &hints); + if (lookup_fd < 0) fail("async lookup did not return an fd"); + + emscripten_set_main_loop(main_loop, 0, 0); + return 0; +} diff --git a/test/sockets/test_dns_async_net.c b/test/sockets/test_dns_async_net.c new file mode 100644 index 0000000000000..9d3069b8a8566 --- /dev/null +++ b/test/sockets/test_dns_async_net.c @@ -0,0 +1,77 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * Asynchronous getaddrinfo over the real network with -sNODERAWSOCKETS: a real + * public hostname has no synchronous resolution (EAI_AGAIN), then resolves via + * emscripten_dns_lookup_async(), whose result is delivered as an addrinfo + * payload. This hits the real network (like test_getaddrinfo). + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char* HOST = "google.com"; + +static int lookup_fd = -1; + +static void fail(const char* why) { + printf("DNS ASYNC NET FAIL: %s\n", why); + abort(); +} + +static void main_loop(void) { + fd_set fdr; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_SET(lookup_fd, &fdr); + select(lookup_fd + 1, &fdr, NULL, NULL, &tv); + if (!FD_ISSET(lookup_fd, &fdr)) { + return; // resolution still in flight + } + + struct addrinfo* res = NULL; + int result = emscripten_dns_lookup_result(lookup_fd, &res); + close(lookup_fd); + if (result != 0) fail("async lookup failed"); + assert(res && res->ai_addr); + freeaddrinfo(res); + + // The async resolution pre-warms getaddrinfo's cache: the same name that was + // EAI_AGAIN up front now resolves synchronously. + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + struct addrinfo* sync_res = NULL; + if (getaddrinfo(HOST, NULL, &hints, &sync_res) != 0) fail("host should be cached"); + assert(sync_res && sync_res->ai_addr); + freeaddrinfo(sync_res); + + printf("DNS ASYNC NET PASS\n"); + emscripten_cancel_main_loop(); +} + +int main(void) { + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + // No synchronous DNS without JSPI: the name is EAI_AGAIN up front. + struct addrinfo* res = NULL; + if (getaddrinfo(HOST, NULL, &hints, &res) != EAI_AGAIN) fail("host should be EAI_AGAIN"); + + lookup_fd = emscripten_dns_lookup_async(HOST, NULL, &hints); + if (lookup_fd < 0) fail("async lookup did not return an fd"); + + emscripten_set_main_loop(main_loop, 0, 0); + return 0; +} diff --git a/test/sockets/test_dns_jspi.c b/test/sockets/test_dns_jspi.c new file mode 100644 index 0000000000000..f88bd6f0ab994 --- /dev/null +++ b/test/sockets/test_dns_jspi.c @@ -0,0 +1,38 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * With -sNODERAWSOCKETS under JSPI, getaddrinfo() of a real public hostname + * blocks on the node:dns lookup by suspending the wasm stack, and resolves + * directly - no EAI_AGAIN + async prewarm + retry needed. This resolves over + * the real network. + */ + +#include +#include +#include +#include +#include +#include + +static const char* HOST = "google.com"; + +int main(void) { + struct addrinfo hints = {0}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + + struct addrinfo* res = NULL; + int err = getaddrinfo(HOST, NULL, &hints, &res); + if (err != 0) { + printf("DNS JSPI FAIL: getaddrinfo err=%d\n", err); + return 1; + } + assert(res); + freeaddrinfo(res); + + printf("DNS JSPI PASS\n"); + return 0; +} diff --git a/test/test_sockets_node.py b/test/test_sockets_node.py index 97750c862641c..8d13010eb7f50 100644 --- a/test/test_sockets_node.py +++ b/test/test_sockets_node.py @@ -205,6 +205,37 @@ def test_noderawsockets_udp_ipv6(self): self.skipTest('no IPv6 loopback available') self.do_runf('sockets/test_udp_ipv6.c', 'done\n', cflags=['-sNODERAWSOCKETS']) + @also_with_proxy_to_pthread + def test_noderawsockets_dns_async(self): + # getaddrinfo() resolves numeric and /etc/hosts names (read via emscripten's + # FS) synchronously and returns EAI_AGAIN for a real hostname. + # emscripten_dns_lookup_async() is the async getaddrinfo: a pollable fd whose + # emscripten_dns_lookup_result() yields the addrinfo payload directly. + self.do_runf('sockets/test_dns_async.c', 'DNS ASYNC PASS', cflags=['-sNODERAWSOCKETS']) + + def test_noderawsockets_dns_async_net(self): + # A real public hostname is EAI_AGAIN synchronously, then resolves via the + # async getaddrinfo, whose result is delivered as an addrinfo payload. The + # async resolution pre-warms the shared cache, so a following synchronous + # getaddrinfo() of the same name then succeeds. This hits the real network + # (like test_getaddrinfo). + self.do_runf('sockets/test_dns_async_net.c', 'DNS ASYNC NET PASS', cflags=['-sNODERAWSOCKETS']) + + def test_dns_async_default(self): + # The async getaddrinfo API is available without -sNODERAWSOCKETS, resolving + # synchronously (the same fake address getaddrinfo() returns) and delivering + # it via the pollable fd. + self.do_runf('sockets/test_dns_async_default.c', 'DNS ASYNC DEFAULT PASS') + + @also_with_proxy_to_pthread + @requires_jspi_node + def test_noderawsockets_dns_jspi(self): + # Under JSPI, getaddrinfo() of a real public hostname blocks on the + # node:dns lookup (suspending the wasm stack) and resolves directly, + # without the EAI_AGAIN + async retry needed in non-JSPI builds. This + # hits the real network (like test_getaddrinfo). + self.do_runf('sockets/test_dns_jspi.c', 'DNS JSPI PASS', cflags=['-sNODERAWSOCKETS']) + def test_noderawsockets_epoll_socket_blocking(self): # A blocking epoll_wait() on a socket is woken by an incoming datagram # through the unified readiness wait-queue (the SOCKFS.emit bridge), with