From ce1fe4fd12c84557093bbe99b2011dbcf7137ce7 Mon Sep 17 00:00:00 2001 From: Thibault Saunier Date: Sat, 21 Mar 2026 09:41:24 -0300 Subject: [PATCH] Fix Asyncify.handleAsync conflict with PROXY_SYNC_ASYNC When a JS library function has both __proxy:'sync' and __async:'auto', the compiler generates an Asyncify.handleAsync wrapper. When called from the PROXY_SYNC_ASYNC path on the main thread, handleAsync triggers an Asyncify unwind instead of returning a Promise, causing "rtn.then is not a function" in the proxy infrastructure. Fix by generating a PThread.currentProxiedOperationCallerThread check in handleAsyncFunction (jsifier.mjs): when in a proxied context, call the inner function directly and skip the Asyncify unwind, letting the proxy mechanism handle the async return. --- src/jsifier.mjs | 21 +++++++++++++++++---- test/test_core.py | 9 +++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/jsifier.mjs b/src/jsifier.mjs index fe71069110e28..084b8a45fe9a3 100644 --- a/src/jsifier.mjs +++ b/src/jsifier.mjs @@ -360,16 +360,29 @@ ${body}; }); } -function handleAsyncFunction(snippet, sig) { +function handleAsyncFunction(snippet, sig, proxyingMode) { const return64 = sig && (MEMORY64 && sig.startsWith('p') || sig.startsWith('j')) let handleAsync = 'Asyncify.handleAsync(innerFunc)' if (return64 && ASYNCIFY == 1) { handleAsync = makeReturn64(handleAsync); } + // When a function uses both __proxy:'sync' and __async:'auto', the proxy + // mechanism (PROXY_SYNC_ASYNC) handles the async return itself. In that + // case, skip the Asyncify unwind and call the inner function directly so + // the proxy can use .then() on the returned Promise. + const skipHandleAsync = PTHREADS && proxyingMode === 'sync'; return modifyJSFunction(snippet, (args, body, async_, oneliner) => { if (!oneliner) { body = `{\n${body}\n}`; } + if (skipHandleAsync) { + return `\ +function(${args}) { + let innerFunc = async () => ${body}; + if (PThread.currentProxiedOperationCallerThread) return innerFunc(); + return ${handleAsync}; +}\n`; + } return `\ function(${args}) { let innerFunc = ${async_} () => ${body}; @@ -474,11 +487,11 @@ function(${args}) { compileTimeContext.i53ConversionDeps.forEach((d) => deps.push(d)); } + const proxyingMode = LibraryManager.library[symbol + '__proxy']; + if (ASYNCIFY && isAsyncFunction == 'auto') { - snippet = handleAsyncFunction(snippet, sig); + snippet = handleAsyncFunction(snippet, sig, proxyingMode); } - - const proxyingMode = LibraryManager.library[symbol + '__proxy']; if (proxyingMode) { if (!['sync', 'async', 'none'].includes(proxyingMode)) { error(`JS library error: invalid proxying mode '${symbol}__proxy: ${proxyingMode}' specified`); diff --git a/test/test_core.py b/test/test_core.py index de3238532da5c..cceed2816ac1a 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9652,6 +9652,15 @@ def test_poll_blocking_asyncify(self): self.skipTest('test requires setTimeout which is not supported under v8') self.do_runf('core/test_poll_blocking_asyncify.c', 'done\n') + @with_asyncify_and_jspi + def test_poll_blocking_asyncify_pthread(self): + # require_pthreads can fail when require_jspi selects d8 (which doesn't + # support pthreads). Convert to skip since the test needs both. + if not any(engine_is_node(e) or engine_is_bun(e) or engine_is_deno(e) for e in self.js_engines): + self.skipTest('no JS engine capable of running pthreads') + self.require_pthreads() + self.do_runf('core/test_poll_blocking.c', 'done\n', cflags=['-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME']) + @parameterized({ '': ([],), 'pthread': (['-pthread'],),