From fb3de642a03da583fa0a9f4e62b7ae570a5f602f Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Thu, 14 May 2026 10:31:56 -0700 Subject: [PATCH] Add `emscripten_promise_await_unchecked` API This works with ASYNCIFY like the existing `emscripten_promise_await` but is a lot simpler since it only handles the fulfilled case. In this case we can simple return the result directly without needing to allocate a `em_promise_result_t` struct to deal with the out param (i.e. no memory access needed). This can be useful in cases where we don't want to handle the rejections case. --- .circleci/config.yml | 2 ++ src/lib/libpromise.js | 20 +++++++++++--- src/lib/libsigs.js | 1 + system/include/emscripten/promise.h | 6 +++++ .../test_codesize_hello_dylink_all.json | 5 ++-- test/core/test_promise.out | 1 + test/core/test_promise_await.c | 27 +++++++++++++++++++ test/core/test_promise_await.out | 4 +++ test/test_core.py | 27 +++++++++++++++++++ 9 files changed, 87 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 30b2f6efd28ae..7ff8653919873 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1095,6 +1095,8 @@ jobs: core0.test_cubescript_jspi core0.test_pthread_wait_suspending* core0.test_poll_blocking_asyncify_jspi + core0.test_promise_await* + wasm64.test_promise_await* wasm64.test_pthread_join_and_asyncify " - upload-test-results diff --git a/src/lib/libpromise.js b/src/lib/libpromise.js index 3b5e25a10084b..f621feff1a9b3 100644 --- a/src/lib/libpromise.js +++ b/src/lib/libpromise.js @@ -247,12 +247,10 @@ addToLibrary({ return id; }, - emscripten_promise_await__async: 'auto', #if ASYNCIFY + emscripten_promise_await__async: 'auto', emscripten_promise_await__deps: ['$getPromise', '$setPromiseResult'], -#endif emscripten_promise_await: (returnValuePtr, id) => { -#if ASYNCIFY #if RUNTIME_DEBUG dbg(`emscripten_promise_await: ${id}`); #endif @@ -260,8 +258,22 @@ addToLibrary({ value => setPromiseResult(returnValuePtr, true, value), error => setPromiseResult(returnValuePtr, false, error) ); + }, + + emscripten_promise_await_unchecked__async: 'auto', + emscripten_promise_await_unchecked__deps: ['$getPromise'], + emscripten_promise_await_unchecked: (id) => { +#if RUNTIME_DEBUG + dbg(`emscripten_promise_await_unchecked: ${id}`); +#endif + return getPromise(id); + }, #else + emscripten_promise_await: (returnValuePtr, id) => { abort('emscripten_promise_await is only available with ASYNCIFY'); -#endif }, + emscripten_promise_await_unchecked: (id) => { + abort('emscripten_promise_await_unchecked is only available with ASYNCIFY'); + }, +#endif }); diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index 3d17121526a05..2e60ffccb6ab0 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -724,6 +724,7 @@ sigs = { emscripten_promise_all_settled__sig: 'pppp', emscripten_promise_any__sig: 'pppp', emscripten_promise_await__sig: 'vpp', + emscripten_promise_await_unchecked__sig: 'pp', emscripten_promise_create__sig: 'p', emscripten_promise_destroy__sig: 'vp', emscripten_promise_race__sig: 'ppp', diff --git a/system/include/emscripten/promise.h b/system/include/emscripten/promise.h index d7c5787d04bd2..a805b364ecbb3 100644 --- a/system/include/emscripten/promise.h +++ b/system/include/emscripten/promise.h @@ -147,6 +147,12 @@ typedef struct em_settled_result_t { [[nodiscard]] em_settled_result_t emscripten_promise_await(em_promise_t promise); +// Just like emscripten_promise_await but does not include a rejection handler +// and simply returns result if/when the promise is fulfilled. +// If the promise is rejected it would then get handled elsewhere in the promise +// chain, or result in a top level unhandled rejection. +[[nodiscard]] void* emscripten_promise_await_unchecked(em_promise_t promise); + #ifdef __cplusplus } #endif diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 1e24024f938fc..7b9aaa25e1a6e 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 268089, + "a.out.js": 268202, "a.out.nodebug.wasm": 587563, - "total": 855652, + "total": 855765, "sent": [ "IMG_Init", "IMG_Load", @@ -842,6 +842,7 @@ "emscripten_promise_all_settled", "emscripten_promise_any", "emscripten_promise_await", + "emscripten_promise_await_unchecked", "emscripten_promise_create", "emscripten_promise_destroy", "emscripten_promise_race", diff --git a/test/core/test_promise.out b/test/core/test_promise.out index cc0c5aa475b18..81c6b80879681 100644 --- a/test/core/test_promise.out +++ b/test/core/test_promise.out @@ -35,3 +35,4 @@ test_null_handlers expected success: 42 expected error: 43 finish +warning: emscripten_force_exit cannot actually shut down the runtime, as the build does not have EXIT_RUNTIME set diff --git a/test/core/test_promise_await.c b/test/core/test_promise_await.c index 24a9eaf83e048..15d6b90b9d7ec 100644 --- a/test/core/test_promise_await.c +++ b/test/core/test_promise_await.c @@ -23,6 +23,19 @@ void test_already_fulfilled() { emscripten_promise_destroy(p); } +void test_already_fulfilled_unchecked() { + // Test waiting on an already fulfilled promise. + em_promise_t p = emscripten_promise_create(); + emscripten_promise_resolve(p, EM_PROMISE_FULFILL, (void*)42); + + printf("waiting on promise (unchecked): %p\n", p); + void* res = emscripten_promise_await_unchecked(p); + printf(".. done wait: %ld\n", (intptr_t)res); + + assert(res == (void*)42); + emscripten_promise_destroy(p); +} + void test_not_yet_fulfilled() { em_promise_t p = emscripten_promise_create(); emscripten_async_call(fulfill_from_timeout, p, 0); @@ -36,6 +49,18 @@ void test_not_yet_fulfilled() { emscripten_promise_destroy(p); } +void test_not_yet_fulfilled_unchecked() { + em_promise_t p = emscripten_promise_create(); + emscripten_async_call(fulfill_from_timeout, p, 0); + + printf("waiting on promise (unchecked): %p\n", p); + void* res = emscripten_promise_await_unchecked(p); + printf(".. done wait: %ld\n", (intptr_t)res); + + assert(res == (void*)43); + emscripten_promise_destroy(p); +} + void test_rejected() { em_promise_t p = emscripten_promise_create(); emscripten_promise_resolve(p, EM_PROMISE_REJECT, (void*)44); @@ -53,7 +78,9 @@ int main() { printf("main\n"); test_already_fulfilled(); + test_already_fulfilled_unchecked(); test_not_yet_fulfilled(); + test_not_yet_fulfilled_unchecked(); test_rejected(); printf("main done\n"); diff --git a/test/core/test_promise_await.out b/test/core/test_promise_await.out index d046d9a11c798..ec338b724dfe8 100644 --- a/test/core/test_promise_await.out +++ b/test/core/test_promise_await.out @@ -1,8 +1,12 @@ main waiting on promise: 0x1 .. done wait: 0 42 +waiting on promise (unchecked): 0x1 +.. done wait: 42 waiting on promise: 0x1 .. done wait: 0 43 +waiting on promise (unchecked): 0x1 +.. done wait: 43 waiting on promise: 0x1 .. done wait: 3 44 main done diff --git a/test/test_core.py b/test/test_core.py index 653accd5aeb68..119c962af68c0 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -9676,6 +9676,33 @@ def test_promise_await_error(self): self.do_runf('core/test_promise_await.c', 'emscripten_promise_await is only available with ASYNCIFY', assert_returncode=NON_ZERO) + # Include @requires_node_25 explictly here so that this test will be disabled + # by EMTEST_SKIP_NODE_25. Without this, the `requires_node` and `requires_jspi` can + # end with conflicting requirements because we often run with both v8 (which satisfies + # the `requires_jspi` part have node (which satisfies the `requires_node` part). + # FIXME: This should not be needed. + @requires_node_25 + @with_asyncify_and_jspi + def test_promise_await_unchecked_rejected(self): + create_file('test.c', r''' +#include +#include +#include + +int main() { + em_promise_t p = emscripten_promise_create(); + emscripten_promise_resolve(p, EM_PROMISE_REJECT, (void*)45); + + printf("waiting on promise (unchecked, rejected)\n"); + (void)emscripten_promise_await_unchecked(p); + printf("ERROR: should not be reached\n"); + __builtin_abort(); + return 1; +} +''') + # We expect an unhandled rejection, which in Node.js results in a non-zero exit code. + self.do_runf('test.c', 'UnhandledPromiseRejection', assert_returncode=NON_ZERO) + @no_modularize_instance('uses Module object directly') def test_emscripten_async_load_script(self): create_file('script1.js', 'Module._set(456);''')