Skip to content

Add emscripten_proxy_fd_with_ctx and 'fd' async variants for JS library functions - #27922

Open
guybedford wants to merge 1 commit into
gbedford/dns-formsfrom
gbedford/fd-form
Open

guybedford wants to merge 1 commit into
gbedford/dns-formsfrom
gbedford/fd-form

Conversation

@guybedford

@guybedford guybedford commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Adds the _fd variant alongside the _promise and _callback variants of #27880 / #27954, so the result of an asynchronous JS library function can be awaited through poll/select/epoll from any stack, alongside sockets and other descriptors. It is built on a new proxying primitive.

emscripten_proxy_fd_with_ctx(q, target, func, arg) proxies a task and returns a file descriptor that becomes readable once the task calls emscripten_proxy_finish / emscripten_proxy_finish_result (readable with POLLERR if it fails or is cancelled), so proxied work can be waited on with the usual descriptor APIs. read() of an intptr_t yields the result (EIO on failure) and the fd is then at EOF. It is a new FD context kind alongside sync and callback: finishing settles the fd directly on the target thread, with nothing proxied back. Descriptors live on the main thread, which is the only supported target for now, so the calling thread may be blocked polling the fd; with WasmFS pollable descriptors the same API will be able to target any thread.

Since an fd has no then through which the task's arg could hand the result over, proxying.h also gains emscripten_proxy_finish_result(ctx, void*) (the fd is readable with it; the promise of emscripten_proxy_promise_with_ctx is fulfilled with it, where it was always NULL) and emscripten_proxy_fail(ctx) (the fd is readable with POLLERR; the promise is rejected, or the cancel callback of emscripten_proxy_callback_with_ctx runs, as if the target thread had exited).

foo_fd(), listed as __async: ['fd'], runs the same body (with sync always false). From a pthread it is emscripten_proxy_fd_with_ctx of the main-thread body, finished with emscripten_proxy_finish_result. On the main thread the body's value or Promise settles the fd directly, so a synchronous completion is readable on return. read() of an intptr_t takes the value (0 for a void function, EIO if the Promise rejected); the fd is then at EOF and must be closed. It is a link-time error under WASMFS, which has no readiness layer. As with the other variants, it is emitted only where used.

int lookup_fd(const char* name);

Tests: test_pthread_proxying covers emscripten_proxy_fd_with_ctx, emscripten_proxy_finish_result and emscripten_proxy_fail; jslib.test_jslib_async_auto_variants_fd in plain, ASYNCIFY, JSPI and PROXY_TO_PTHREAD builds, wasm32 and wasm64; test_jslib_async_auto_variants_fd_wasmfs.

Made with AI assistance under my review

@guybedford
guybedford added this pull request to stack #27923 October 8, 2026 00:03

@tlively tlively left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks like this implementation works by proxying not just the library function, but also a wrapper fdFromPromise call that produces an FD that can be polled for the result of the promise. This makes sense because currently (without WasmFS support for general pollable FDs), FDs can only be created on the main thread, where the FS lives.

I don't think that's general enough, though. JS library functions proxied to the main thread are only one special case of proxied work that a thread might be waiting on. The advantage of using pollable fds is that a thread can synchronously wait on many pending tasks at once, but that only works if all the pending tasks can be represented as fds. If the JS library proxying were built on a lower-level emscripten_proxy_fd_with_ctx, then all tasks proxied via em_proxying_queue would be compatible with epoll, no matter what queue or thread they run on.

Without WasmFS support for pollable FDs, emscripten_proxy_fd_with_ctx targeting any thread other than the main thread will have to trampoline through the main thread to allocate an FD, then do a promise or callback proxy call from the main thread to the target thread to arrange for the FD to become ready when the work is done.

Once we have WasmFS support, though, emscripten_proxy_fd_with_ctx will be able to allocate an FD directly, then proxy directly to the target thread, which will be able to mark the FD ready without having to proxy anything back to the calling thread.

Comment thread src/lib/libpromise.js Outdated
@guybedford

Copy link
Copy Markdown
Collaborator Author

Once we have WasmFS support, though, emscripten_proxy_fd_with_ctx will be able to allocate an FD directly, then proxy directly to the target thread, which will be able to mark the FD ready without having to proxy anything back to the calling thread.

Completely agreed on this overall direction, and I think that sounds like a sensible refactoring later on, that has no public surface area at risk from this current approach for now.

@guybedford
guybedford force-pushed the gbedford/async-canwait branch from 900ac5a to 32efe20 Compare October 8, 2026 02:14
@guybedford guybedford changed the title Add __proxy: 'fd' result form for __async: 'auto' bodies Add _fd forms for __async: 'auto' library functions Oct 8, 2026
@tlively

tlively commented Oct 8, 2026

Copy link
Copy Markdown
Member

How about implementing emscripten_proxy_fd_with_ctx, but only with support for proxying to the main thread for now? That hopefully strikes a good balance between going in the direction we want for the long term and not adding extra complexity to the critical path.

@guybedford guybedford changed the title Add _fd forms for __async: 'auto' library functions Add emscripten_proxy_fd_with_ctx and _fd forms for __async: 'auto' library functions Oct 8, 2026
@guybedford
guybedford force-pushed the gbedford/async-canwait branch from 32efe20 to d84a291 Compare October 8, 2026 17:49
@guybedford

Copy link
Copy Markdown
Collaborator Author

I went ahead and added emscripten_proxy_fd_with_ctx as described with the main thread as the only supported target by assertion for now, which could then extend to WasmFS support in future to support arbitrary threads.

@guybedford
guybedford force-pushed the gbedford/async-canwait branch 2 times, most recently from 67698db to 3f4ecef Compare October 9, 2026 00:00
@guybedford guybedford changed the title Add emscripten_proxy_fd_with_ctx and _fd forms for __async: 'auto' library functions Add emscripten_proxy_fd_with_ctx and _fd variants for __async: 'auto' library functions Oct 9, 2026
Comment thread system/lib/libc/emscripten_internal.h Outdated
Comment thread system/lib/pthread/proxying.c Outdated
Comment thread system/lib/pthread/proxying.c Outdated
Comment thread system/lib/pthread/proxying.c Outdated
Comment thread system/lib/pthread/proxying.c Outdated
Comment on lines +832 to +838
// Settles the task's fd with the result, on the main thread where the fd
// lives, before emscripten_proxy_finish settles it as merely finished (which
// then finds it settled).
static void js_fd_done(proxied_js_func_t* f, em_proxying_ctx* ctx) {
assert(ctx->kind == FD);
_emscripten_proxy_fd_settle(ctx->fd.handle, (intptr_t)f->result, f->fulfilled);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh, I see. Because there's no equivalent of promise.then() for pollable fds, there's no way to chain on an extraction of the result from the arg the way you can with promises. Given that, I would support adding something like emscripten_proxy_finish_result for use with both promises (if feasible) and fds. It would be nice to do that in a separate PR, either before or after this one.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ok I've gone ahead and reinstated the emscripten_proxy_finish_result style approach in the base PR at #27880. Please take another look there further.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Since it's still not clear if this one will land, I've instead moved the emscripten_proxy_finish_result back into this PR directly, so it doesn't affect the lower stack PR unnecessarily.

@guybedford
guybedford force-pushed the gbedford/async-canwait branch from 3f4ecef to a249964 Compare October 9, 2026 01:40
@guybedford
guybedford force-pushed the gbedford/async-canwait branch from a249964 to a52a242 Compare October 9, 2026 16:56
@guybedford
guybedford force-pushed the gbedford/fd-form branch 2 times, most recently from 86f886b to 708a3fe Compare October 12, 2026 16:52
@guybedford
guybedford force-pushed the gbedford/async-canwait branch from 322e972 to 72b406f Compare October 12, 2026 23:15
@guybedford
guybedford removed this pull request from stack #27923 October 12, 2026 23:15
@guybedford
guybedford changed the base branch from gbedford/async-canwait to gbedford/dns-forms October 12, 2026 23:39
@guybedford
guybedford added this pull request to stack #27957 October 12, 2026 23:40
@guybedford
guybedford removed this pull request from stack #27957 October 12, 2026 23:40
@guybedford
guybedford force-pushed the gbedford/dns-forms branch 3 times, most recently from 77a780b to 07bca5f Compare October 13, 2026 00:35
@guybedford guybedford changed the title Add emscripten_proxy_fd_with_ctx and _fd variants for __async: 'auto' library functions Add emscripten_proxy_fd_with_ctx and 'fd' async variants for JS library functions Oct 14, 2026
…ry functions

emscripten_proxy_fd_with_ctx(q, target, func, arg) proxies a task and
returns a file descriptor readable once it is finished (POLLERR if it fails
or is cancelled), so proxied work can be awaited with poll/select/epoll
alongside other descriptors; read() yields the emscripten_proxy_finish_result
value. Descriptors live on the main thread, which is the only supported
target for now.

foo_fd, the 'fd' async variant of a JS library function, is built on it: readable once the result is available (POLLERR if rejected); read() of
an intptr_t takes the value, after which the fd is at EOF. On the main
thread the body's value or Promise settles the fd directly.

This branch has not been deployed

No deployments
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.

2 participants