Skip to content

Commit bd9180c

Browse files
committed
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.
1 parent 312eed6 commit bd9180c

3 files changed

Lines changed: 89 additions & 4 deletions

File tree

src/jsifier.mjs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -360,16 +360,29 @@ ${body};
360360
});
361361
}
362362

363-
function handleAsyncFunction(snippet, sig) {
363+
function handleAsyncFunction(snippet, sig, proxyingMode) {
364364
const return64 = sig && (MEMORY64 && sig.startsWith('p') || sig.startsWith('j'))
365365
let handleAsync = 'Asyncify.handleAsync(innerFunc)'
366366
if (return64 && ASYNCIFY == 1) {
367367
handleAsync = makeReturn64(handleAsync);
368368
}
369+
// When a function uses both __proxy:'sync' and __async:'auto', the proxy
370+
// mechanism (PROXY_SYNC_ASYNC) handles the async return itself. In that
371+
// case, skip the Asyncify unwind and call the inner function directly so
372+
// the proxy can use .then() on the returned Promise.
373+
const skipHandleAsync = PTHREADS && ASYNCIFY == 1 && proxyingMode === 'sync';
369374
return modifyJSFunction(snippet, (args, body, async_, oneliner) => {
370375
if (!oneliner) {
371376
body = `{\n${body}\n}`;
372377
}
378+
if (skipHandleAsync) {
379+
return `\
380+
function(${args}) {
381+
let innerFunc = ${async_} () => ${body};
382+
if (PThread.currentProxiedOperationCallerThread) return innerFunc();
383+
return ${handleAsync};
384+
}\n`;
385+
}
373386
return `\
374387
function(${args}) {
375388
let innerFunc = ${async_} () => ${body};
@@ -474,11 +487,11 @@ function(${args}) {
474487
compileTimeContext.i53ConversionDeps.forEach((d) => deps.push(d));
475488
}
476489

490+
const proxyingMode = LibraryManager.library[symbol + '__proxy'];
491+
477492
if (ASYNCIFY && isAsyncFunction == 'auto') {
478-
snippet = handleAsyncFunction(snippet, sig);
493+
snippet = handleAsyncFunction(snippet, sig, proxyingMode);
479494
}
480-
481-
const proxyingMode = LibraryManager.library[symbol + '__proxy'];
482495
if (proxyingMode) {
483496
if (!['sync', 'async', 'none'].includes(proxyingMode)) {
484497
error(`JS library error: invalid proxying mode '${symbol}__proxy: ${proxyingMode}' specified`);
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2026 The Emscripten Authors. All rights reserved.
3+
* Emscripten is available under two separate licenses, the MIT license and the
4+
* University of Illinois/NCSA Open Source License. Both these licenses can be
5+
* found in the LICENSE file.
6+
*/
7+
8+
#define _GNU_SOURCE
9+
#include <assert.h>
10+
#include <poll.h>
11+
#include <pthread.h>
12+
#include <stdio.h>
13+
#include <time.h>
14+
#include <unistd.h>
15+
16+
static int fds[2];
17+
18+
static void *writer(void *arg) {
19+
write(fds[1], "x", 1);
20+
return NULL;
21+
}
22+
23+
int main(void) {
24+
pthread_t t;
25+
char buf;
26+
struct pollfd pfd = {.events = POLLIN};
27+
28+
pipe(fds);
29+
pfd.fd = fds[0];
30+
31+
// poll should timeout on an empty pipe
32+
assert(poll(&pfd, 1, 100) == 0);
33+
34+
// poll should return immediately when data is already available
35+
write(fds[1], "a", 1);
36+
assert(poll(&pfd, 1, 1000) == 1);
37+
assert(pfd.revents & POLLIN);
38+
assert(read(fds[0], &buf, 1) == 1 && buf == 'a');
39+
40+
// poll should wake up from a cross-thread write
41+
pfd.revents = 0;
42+
pthread_create(&t, NULL, writer, NULL);
43+
assert(poll(&pfd, 1, 5000) == 1);
44+
assert(pfd.revents & POLLIN);
45+
assert(read(fds[0], &buf, 1) == 1 && buf == 'x');
46+
pthread_join(t, NULL);
47+
48+
// ppoll should also timeout on an empty pipe
49+
struct timespec ts = {0, 200 * 1000000L};
50+
struct timespec begin, end;
51+
pfd.revents = 0;
52+
clock_gettime(CLOCK_MONOTONIC, &begin);
53+
assert(ppoll(&pfd, 1, &ts, NULL) == 0);
54+
clock_gettime(CLOCK_MONOTONIC, &end);
55+
long elapsed_ms = (end.tv_sec - begin.tv_sec) * 1000 +
56+
(end.tv_nsec - begin.tv_nsec) / 1000000;
57+
assert(elapsed_ms >= 195);
58+
59+
printf("done\n");
60+
}

test/test_core.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9645,6 +9645,18 @@ def test_poll_blocking_asyncify(self):
96459645
self.skipTest('test requires setTimeout which is not supported under v8')
96469646
self.do_runf('core/test_poll_blocking_asyncify.c', 'done\n')
96479647

9648+
@no_esm_integration('WASM_ESM_INTEGRATION is not compatible with ASYNCIFY=1')
9649+
@requires_pthreads
9650+
def test_poll_blocking_asyncify_pthread(self):
9651+
# Only testing ASYNCIFY=1: JSPI's handleAsync is a plain async function
9652+
# and doesn't have this bug. Also, with_asyncify_and_jspi can't be
9653+
# combined with requires_pthreads since require_jspi may select d8 which
9654+
# doesn't support pthreads (require_pthreads then hard-fails instead of
9655+
# skipping).
9656+
self.set_setting('ASYNCIFY')
9657+
self.do_runf('core/test_poll_blocking_asyncify_pthread.c', 'done\n',
9658+
cflags=['-sPROXY_TO_PTHREAD', '-sEXIT_RUNTIME'])
9659+
96489660
@parameterized({
96499661
'': ([],),
96509662
'pthread': (['-pthread'],),

0 commit comments

Comments
 (0)