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
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ See docs/process.md for more on how version tagging works.
`emscripten_clear_immediate` are no-ops for ids that already fired. (#27720)
- The `NODE_CODE_CACHING` setting was removed, as node's support for
serializing WebAssembly modules has been missing for over 5 years now.
- Added `emscripten_dns_lookup_async`/`emscripten_dns_lookup_result`, an
asynchronous `getaddrinfo` that completes through a pollable fd, so a real
DNS lookup under `-sNODERAWSOCKETS` can be awaited without blocking from any
stack.

6.0.9 - 09/01/26
----------------
Expand Down
29 changes: 29 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,35 @@ Functions
arbitrary ``userData`` passed to this function.



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

Asynchronous ``getaddrinfo()``. Takes the same inputs and starts the lookup,
returning a file descriptor that becomes readable (``poll()``, ``select()``,
``epoll``) once it completes; it may already be readable on return. Read the
outcome with :c:func:`emscripten_dns_lookup_result`, then ``close()`` the fd.
Nothing is allocated until a result is read.

Unlike ``getaddrinfo()``, which under ``-sNODERAWSOCKETS`` can only wait for
a real DNS lookup from a pthread or with ``ASYNCIFY``/JSPI, this never blocks
and so works from the main thread of any build.

The descriptor is a file-system stream, so the file system must be linked
in (any use of ``poll()`` or ``close()`` already does this; otherwise pass
``-sFORCE_FILESYSTEM``).

:returns: The file descriptor, or -1 if no descriptor is available.

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

The outcome of an :c:func:`emscripten_dns_lookup_async` lookup.

:returns: 0 on success, with a newly allocated ``addrinfo`` list in ``*res``
(free it with ``freeaddrinfo()``; each successful call returns its own
list); an ``EAI_*`` error code; or ``EAI_AGAIN`` while the lookup is still
pending. ``EAI_SYSTEM`` if ``fd`` is not a lookup fd.


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

Expand Down
156 changes: 86 additions & 70 deletions src/lib/libcore.js
Original file line number Diff line number Diff line change
Expand Up @@ -1007,34 +1007,15 @@ addToLibrary({
return inetPton4(DNS.lookup_name(nameString));
},

getaddrinfo__deps: ['$DNS', '$inetPton4', '$inetNtop4', '$inetPton6', '$inetNtop6', '$writeSockaddr', 'malloc', 'htonl',
#if NODERAWSOCKETS
'$nodeSockHelpers',
#endif
#if NODERAWSOCKETS && ASYNCIFY
'$Asyncify',
#endif
],
getaddrinfo__proxy: 'sync',
#if NODERAWSOCKETS && (PTHREADS || ASYNCIFY)
// Returns an EAI_* code synchronously, or - for a hostname needing a real
// DNS lookup - a Promise of one, which a sync-proxied pthread awaits and
// ASYNCIFY/JSPI suspend on.
getaddrinfo__async: true,
#endif
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 = [];
// Complete a getAddrInfo result: an EAI_* code is returned as is; a resolved
// descriptor is written to *out as an addrinfo list (freed by freeaddrinfo)
// and 0 returned.
$writeAddrInfo__internal: true,
$writeAddrInfo__deps: ['$inetNtop4', '$inetNtop6', '$writeSockaddr', 'malloc'],
$writeAddrInfo: (desc, out) => {
if (typeof desc == 'number') return desc;
var {type, proto, port} = desc;
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;
Expand Down Expand Up @@ -1068,26 +1049,41 @@ addToLibrary({
return ai;
}

#if NODERAWSOCKETS
// Resolve via node:dns (which honors the host's /etc/hosts), chaining one
// addrinfo per {family, addr} result into *out.
async function lookupHostname() {
var entries = await nodeSockHelpers.lookupHost(node, family);
if (typeof entries == 'number') return entries;
var head = 0, prev = 0;
for (var entry of entries) {
var ai = allocaddrinfo(entry.family, type, proto, null, entry.addr, port);
if (prev) {
{{{ makeSetValue('prev', C_STRUCTS.addrinfo.ai_next, 'ai', '*') }}};
} else {
head = ai;
}
prev = ai;
var head = 0, prev = 0;
for (var {family, addr} of desc.entries) {
var ai = allocaddrinfo(family, type, proto, canon, addr, port);
if (prev) {
{{{ makeSetValue('prev', C_STRUCTS.addrinfo.ai_next, 'ai', '*') }}};
} else {
head = ai;
}
{{{ makeSetValue('out', '0', 'head', '*') }}};
return 0;
prev = ai;
}
{{{ makeSetValue('out', '0', 'head', '*') }}};
return 0;
},

// The getaddrinfo body, allocating nothing. Returns an EAI_* code or a
// descriptor {type, proto, port, entries: [{family, addr}]}. Under
// NODERAWSOCKETS a hostname needs an asynchronous node:dns lookup: the
// descriptor then has `lookup` instead of `entries`, an async function
// resolving to a code or entries, for the caller to wait on.
$getAddrInfo__internal: true,
$getAddrInfo__deps: ['$DNS', '$inetPton4', '$inetPton6', 'htonl',
#if NODERAWSOCKETS
'$nodeSockHelpers',
#endif
],
$getAddrInfo: (node, service, hint) => {
// 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 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 @@ -1157,9 +1153,7 @@ addToLibrary({
addr = [0, 0, 0, _htonl(1)];
}
}
ai = allocaddrinfo(family, type, proto, null, addr, port);
{{{ makeSetValue('out', '0', 'ai', '*') }}};
return 0;
return {type, proto, port, entries: [{family, addr}]};
}

//
Expand Down Expand Up @@ -1190,9 +1184,7 @@ addToLibrary({
}
}
if (addr != null) {
ai = allocaddrinfo(family, type, proto, node, addr, port);
{{{ makeSetValue('out', '0', 'ai', '*') }}};
return 0;
return {type, proto, port, entries: [{family, addr}]};
}
if (flags & {{{ cDefs.AI_NUMERICHOST }}}) {
return {{{ cDefs.EAI_NONAME }}};
Expand All @@ -1202,23 +1194,11 @@ addToLibrary({
// try as a hostname
//
#if NODERAWSOCKETS
// The lookup is asynchronous, so only start it where the calling stack can
// wait on the Promise: a sync-proxied pthread (PROXY_SYNC_ASYNC) awaits it,
// ASYNCIFY/JSPI suspend on it. Otherwise (the event-loop thread itself) it
// must not start at all, since it would write to *out after we have
// returned.
#if PTHREADS
if (PThread.currentProxiedOperationCallerThread) return lookupHostname();
#endif
#if ASYNCIFY
// handleAsync holds a runtime keepalive across the suspension, so a user
// callback completing meanwhile does not exit the runtime under main().
// Everything above this point is pure, so the ASYNCIFY rewind re-running
// this body reaches handleAsync again and takes the stored result.
return Asyncify.handleAsync(lookupHostname);
#else
return {{{ cDefs.EAI_AGAIN }}};
#endif
// node:dns honors the host's /etc/hosts.
return {type, proto, port, lookup: async () => {
var entries = await nodeSockHelpers.lookupHost(node, family);
return typeof entries == 'number' ? entries : {type, proto, port, entries};
}};
#else
// resolve the hostname to a temporary fake address
node = DNS.lookup_name(node);
Expand All @@ -1228,10 +1208,46 @@ 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 {type, proto, port, entries: [{family, addr}]};
#endif
},

getaddrinfo__deps: ['$getAddrInfo', '$writeAddrInfo',
#if NODERAWSOCKETS && ASYNCIFY
'$Asyncify',
#endif
],
getaddrinfo__proxy: 'sync',
#if NODERAWSOCKETS && (PTHREADS || ASYNCIFY)
// Returns an EAI_* code synchronously, or - for a hostname needing a real
// DNS lookup - a Promise of one, which a sync-proxied pthread awaits and
// ASYNCIFY/JSPI suspend on.
getaddrinfo__async: true,
#endif
getaddrinfo: (node, service, hint, out) => {
var desc = getAddrInfo(node, service, hint);
#if NODERAWSOCKETS
if (desc.lookup) {
var lookup = async () => writeAddrInfo(await desc.lookup(), out);
// Only start the lookup where the calling stack can wait on it: a
// sync-proxied pthread (PROXY_SYNC_ASYNC) awaits the Promise, ASYNCIFY/JSPI
// suspend on it. Otherwise (the event-loop thread itself) it must not
// start at all, since it would write to *out after we have returned.
#if PTHREADS
if (PThread.currentProxiedOperationCallerThread) return lookup();
#endif
#if ASYNCIFY
// handleAsync holds a runtime keepalive across the suspension, so a user
// callback completing meanwhile does not exit the runtime under main().
// Everything before the lookup is pure, so the ASYNCIFY rewind re-running
// the body reaches handleAsync again and takes the stored result.
return Asyncify.handleAsync(lookup);
#else
return {{{ cDefs.EAI_AGAIN }}};
#endif
}
#endif
return writeAddrInfo(desc, out);
},

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
51 changes: 51 additions & 0 deletions src/lib/libsockfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -850,4 +850,55 @@ addToLibrary({
emscripten_set_socket_close_callback__deps: ['$_setNetworkCallback'],
emscripten_set_socket_close_callback: (userData, callback) =>
_setNetworkCallback('close', userData, callback),

#if PROXY_POSIX_SOCKETS == 0
// Asynchronous getaddrinfo: same inputs, returns an fd that becomes readable
// (poll/select/epoll) once the lookup completes; it may already be readable on
// return. Read the outcome with emscripten_dns_lookup_result, then close the
// fd. Nothing is allocated until a result is read. Returns -1 if no fd is
// available.
emscripten_dns_lookup_async__deps: ['$FS', '$getAddrInfo', '$callUserCallback'],
emscripten_dns_lookup_async__proxy: 'sync',
emscripten_dns_lookup_async: (node, service, hint) => {
var stream;
try {
stream = FS.createStream({
node: new FS.FSNode(0, '', 0, 0),
stream_ops: {poll: () => 'result' in dns ? {{{ cDefs.POLLRDNORM | cDefs.POLLIN }}} : 0},
});
} catch (e) {
return -1;
}
// On the open file description, so dup'd fds share the lookup.
var dns = stream.shared.dns = {};
var desc = getAddrInfo(node, service, hint);
#if NODERAWSOCKETS
if (desc.lookup) {
// Pending: the lookup holds the runtime until it lands, like a timer.
{{{ runtimeKeepalivePush() }}}
desc.lookup().then((result) => {
{{{ runtimeKeepalivePop() }}}
dns.result = result;
callUserCallback(() => stream.node.notifyListeners({{{ cDefs.POLLRDNORM | cDefs.POLLIN }}}));
});
return stream.fd;
}
#endif
dns.result = desc;
return stream.fd;
},

// The outcome of an emscripten_dns_lookup_async fd: 0 with a newly allocated
// addrinfo list in *res (the caller frees it with freeaddrinfo), an EAI_* code,
// or EAI_AGAIN while still pending. EAI_SYSTEM if fd is not a lookup fd (the
// one use here of an EAI_* code outside its getaddrinfo meaning).
emscripten_dns_lookup_result__deps: ['$FS', '$writeAddrInfo'],
emscripten_dns_lookup_result__proxy: 'sync',
emscripten_dns_lookup_result: (fd, res) => {
var dns = FS.getStream(fd)?.shared.dns;
if (!dns) return {{{ cDefs.EAI_SYSTEM }}};
if (!('result' in dns)) return {{{ cDefs.EAI_AGAIN }}};
return writeAddrInfo(dns.result, res);
},
#endif
});
3 changes: 2 additions & 1 deletion src/struct_info.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@
"EAI_NONAME",
"EAI_SOCKTYPE",
"EAI_BADFLAGS",
"EAI_AGAIN"
"EAI_AGAIN",
"EAI_SYSTEM"
],
"structs": {
"addrinfo": [
Expand Down
1 change: 1 addition & 0 deletions src/struct_info_generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"EAI_OVERFLOW": -12,
"EAI_SERVICE": -8,
"EAI_SOCKTYPE": -7,
"EAI_SYSTEM": -11,
"EALREADY": 7,
"EBADE": 113,
"EBADF": 8,
Expand Down
1 change: 1 addition & 0 deletions src/struct_info_generated_wasm64.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"EAI_OVERFLOW": -12,
"EAI_SERVICE": -8,
"EAI_SOCKTYPE": -7,
"EAI_SYSTEM": -11,
"EALREADY": 7,
"EBADE": 113,
"EBADF": 8,
Expand Down
9 changes: 9 additions & 0 deletions system/include/emscripten/emscripten.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ 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(): same inputs, returns an fd that becomes readable
// (poll/select/epoll) once the lookup completes, or -1 if no fd is available.
// Read the outcome with emscripten_dns_lookup_result(), then close() the fd.
struct addrinfo;
int emscripten_dns_lookup_async(const char *name, const char *service, const struct addrinfo *hints);
// 0 with a newly allocated addrinfo list in *res (free with freeaddrinfo()),
// an EAI_* code, or EAI_AGAIN while the lookup is still pending.
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) \
Expand Down
6 changes: 4 additions & 2 deletions test/codesize/test_codesize_hello_dylink_all.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"a.out.js": 270695,
"a.out.js": 271271,
"a.out.nodebug.wasm": 588098,
"total": 858793,
"total": 859369,
"sent": [
"IMG_Init",
"IMG_Load",
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading