Skip to content

Allow synchronous proxying of async JS functions#26000

Merged
sbc100 merged 1 commit intoemscripten-core:mainfrom
sbc100:poll_promising
Jan 21, 2026
Merged

Allow synchronous proxying of async JS functions#26000
sbc100 merged 1 commit intoemscripten-core:mainfrom
sbc100:poll_promising

Conversation

@sbc100
Copy link
Collaborator

@sbc100 sbc100 commented Dec 23, 2025

This new mode allows the proxied work itself to be asynchronous (i.e.
return a promise). The new behaviour is triggered by marking a
functions as both __proxy: 'sync' and also __async.

We can use this new mode to replace the bespoke proxying code that
exists for dlsync as well as the poll system call. For the poll
system call this bespoke code was added in #25523 and #25990, but can
now be completely removed.

In addition to this, because the poll syscall is now marked as
__async it automatically works under ASYNCIFY/JSPI too.

One downside of this new mode is that the proxied work requires the main
thread event loop to run. Unlike the synchronous proxied work that can
complete even when we are deeply nested.

Fixes: #25970

@sbc100 sbc100 changed the title Add new proxying mode for JS library functions Add new async proxying mode for JS library functions Dec 23, 2025
@sbc100 sbc100 requested a review from kripken December 23, 2025 18:53
@sbc100 sbc100 force-pushed the poll_promising branch 3 times, most recently from 4c77b8f to 9ba3164 Compare December 23, 2025 21:43
@sbc100 sbc100 changed the title Add new async proxying mode for JS library functions Allow synchronous proxying of async JS functions Dec 23, 2025
@sbc100
Copy link
Collaborator Author

sbc100 commented Dec 23, 2025

I found that combining __proxy: 'sync' and __async: true works pretty nicely. This also now means that poll() can block on the main thread when JSPI is enabled.

@sbc100 sbc100 requested a review from RReverser December 23, 2025 22:16
sbc100 added a commit to sbc100/emscripten that referenced this pull request Dec 23, 2025
sbc100 added a commit that referenced this pull request Dec 23, 2025
Non-function change split out from #26000.
@sbc100 sbc100 force-pushed the poll_promising branch 2 times, most recently from d17af1e to dbc51db Compare December 23, 2025 23:20
@sbc100 sbc100 force-pushed the poll_promising branch 5 times, most recently from b9db5cb to 9cb65fc Compare January 16, 2026 22:16
@sbc100 sbc100 enabled auto-merge (squash) January 16, 2026 22:16
@sbc100 sbc100 force-pushed the poll_promising branch 4 times, most recently from 89b2776 to e30484e Compare January 19, 2026 05:17
@sbc100 sbc100 force-pushed the poll_promising branch 2 times, most recently from 1f6e95c to 89a5526 Compare January 20, 2026 20:13
sbc100 referenced this pull request in python/cpython Jan 20, 2026
…H-136988)

Basic support for pyrepl in Emscripten. Limitations:
* requires JSPI
* no signal handling implemented

As followup work, it would be nice to implement a webworker variant
for when JSPI is not available and proper signal handling.

Because it requires JSPI, it doesn't work in Safari. Firefox requires
setting an experimental flag. All the Chromiums have full support since
May. Until we make it work without JSPI, let's keep the original web_example
around.
(cherry picked from commit c933a6b)

Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
Co-authored-by: Éric <merwok@netwok.org>
@sbc100
Copy link
Collaborator Author

sbc100 commented Jan 21, 2026

@dschuff @kripken @tlively would you mind taking a look at this?

This is the piece that finally allows blocking calls to proxied async functions when called from threads. I'd like to land this before the next release because it actually restores the __syscall_poll JS library function which was removed in #25990. Even though this should have been an implementation detail it did manage to break python downstream. See #26132

src/jsifier.mjs Outdated
if (oneliner) {
body = `return ${body}`;
}
let sync = 0;
Copy link
Member

@kripken kripken Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do these numbers 0,1,2 mean - where are they documented?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some docs in proxyToMainThread. This is internal-only so the docs only, so the docs don't need to be public in that sense.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth extracting into named const anyway, it doesn't cost anything but would make it a bit more readable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbc100

I added some docs in proxyToMainThread.

Are those not pushed to the branch perhaps? I can't see them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See proxyToMainThread changes in src/lib/libpthread.js

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is that diff:

diff --git a/src/lib/libpthread.js b/src/lib/libpthread.js
index 4ecbb2d63754e..f95fd8c8e3f0e 100644
--- a/src/lib/libpthread.js
+++ b/src/lib/libpthread.js
@@ -935,7 +935,7 @@ var LibraryPThread = {
 
   $proxyToMainThread__deps: ['$stackSave', '$stackRestore', '$stackAlloc', '_emscripten_run_js_on_main_thread'],
   $proxyToMainThread__docs: '/** @type{function(number, (number|boolean), ...number)} */',
-  $proxyToMainThread: (funcIndex, emAsmAddr, sync, ...callArgs) => {
+  $proxyToMainThread: (funcIndex, emAsmAddr, proxyMode, ...callArgs) => {
     // EM_ASM proxying is done by passing a pointer to the address of the EM_ASM
     // content as `emAsmAddr`.  JS library proxying is done by passing an index
     // into `proxiedJSCallArgs` as `funcIndex`. If `emAsmAddr` is non-zero then
@@ -944,8 +944,11 @@ var LibraryPThread = {
     // function arguments.
     // The serialization buffer contains the number of call params, and then
     // all the args here.
-    // We also pass 'sync' to C separately, since C needs to look at it.
-    // Allocate a buffer, which will be copied by the C code.
+    //
+    // We also pass 'proxyMode' to C separately, since C needs to look at it.
+    //
+    // Allocate a buffer (on the stack), which will be copied if necessary by
+    // the C code.
     //
     // First passed parameter specifies the number of arguments to the function.
     // When BigInt support is enabled, we must handle types in a more complex
@@ -972,7 +975,7 @@ var LibraryPThread = {
       HEAPF64[b++] = arg;
 #endif
     }
-    var rtn = __emscripten_run_js_on_main_thread(funcIndex, emAsmAddr, bufSize, args, sync);
+    var rtn = __emscripten_run_js_on_main_thread(funcIndex, emAsmAddr, bufSize, args, proxyMode);
     stackRestore(sp);
     return rtn;
   },

I don't see an explanation of the values 0, 1, 2?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry they were added initially to that function but now I've moved the docs in the C source code alongside __emscripten_run_js_on_main_thread which actually uses the values.

The values are not used here in the JS it turns out

@sbc100 sbc100 force-pushed the poll_promising branch 2 times, most recently from 0372b26 to e76d10a Compare January 21, 2026 01:11
Copy link
Member

@tlively tlively left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I don't really follow anything else going on here.

src/jsifier.mjs Outdated
if (oneliner) {
body = `return ${body}`;
}
let sync = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbc100

I added some docs in proxyToMainThread.

Are those not pushed to the branch perhaps? I can't see them.

@sbc100 sbc100 force-pushed the poll_promising branch 2 times, most recently from 7cea7a6 to 36a6491 Compare January 21, 2026 18:26
This new mode allows the proxied work itself to be asynchronous (i.e.
return a promise).  The new behaviour is triggered by marking a
functions as both `__proxy: 'sync'` and also `__async`.

We can use this new mode to replace the bespoke proxying code that
exists for `dlsync` as well as the `poll` system call.  For the poll
system call this bespoke code was added in emscripten-core#25523 and emscripten-core#25990, but can
now be completely removed.

In addition to this, because the `poll` syscall is now marked as
`__async` it automatically works under ASYNCIFY/JSPI too.

One downside of this new mode is that the proxied work requires the main
thread event loop to run.  Unlike the synchronous proxied work that can
complete even when we are deeply nested.

Fixes: emscripten-core#25970
Copy link
Member

@dschuff dschuff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is a fairly significant capability already, and this change makes it much more useful, I think we should expand the section on proxying docs/porting/pthreads.rst with more information on what proxying is, why and how you would want to use it, and maybe a couple of examples.

#endif
const callingThread = PThread.currentProxiedOperationCallerThread;
if (callingThread) {
return dlsyncThreadsAsync()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return dlsyncThreadsAsync()
return dlsyncThreadsAsync();

"a.out.js.gz": 4056,
"a.out.nodebug.wasm": 19730,
"a.out.nodebug.wasm.gz": 9140,
"total": 27973,
Copy link
Member

@kripken kripken Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the extra size is to support the new proxying mode? edit: at least it's a small change.

@sbc100 sbc100 merged commit b120a17 into emscripten-core:main Jan 21, 2026
36 checks passed
@sbc100 sbc100 deleted the poll_promising branch January 21, 2026 21:04
@sbc100
Copy link
Collaborator Author

sbc100 commented Jan 21, 2026

Sorry, will follow up with docs and feedback

sbc100 added a commit to sbc100/emscripten that referenced this pull request Jan 21, 2026
sbc100 added a commit to sbc100/emscripten that referenced this pull request Jan 21, 2026
sbc100 added a commit that referenced this pull request Jan 21, 2026
inolen pushed a commit to inolen/emscripten that referenced this pull request Feb 13, 2026
inolen pushed a commit to inolen/emscripten that referenced this pull request Feb 13, 2026
This new mode allows the proxied work itself to be asynchronous (i.e.
return a promise). The new behaviour is triggered by marking a
functions as both `__proxy: 'sync'` and also `__async`. 

We can use this new mode to replace the bespoke proxying code that
exists for `dlsync` as well as the `poll` system call. For the poll
system call this bespoke code was added in emscripten-core#25523 and emscripten-core#25990, but can
now be completely removed.

In addition to this, because the `poll` syscall is now marked as
`__async` it automatically works under ASYNCIFY/JSPI too. 

One downside of this new mode is that the proxied work requires the main
thread event loop to run. Unlike the synchronous proxied work that can
complete even when we are deeply nested.

Fixes: emscripten-core#25970
inolen pushed a commit to inolen/emscripten that referenced this pull request Feb 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Seamless async integration in JS library code

5 participants