diff --git a/src/lib/libemval.js b/src/lib/libemval.js index dd1a5612e6bf5..48eb6334601ad 100644 --- a/src/lib/libemval.js +++ b/src/lib/libemval.js @@ -456,33 +456,33 @@ var LibraryEmVal = { return result.done ? 0 : Emval.toHandle(result.value); }, - _emval_coro_suspend__deps: ['$Emval', '_emval_coro_resume'], - _emval_coro_suspend: async (promiseHandle, awaiterPtr) => { - var result = await Emval.toValue(promiseHandle); - __emval_coro_resume(awaiterPtr, Emval.toHandle(result)); + _emval_coro_suspend__deps: ['$Emval', '_emval_coro_resume', '_emval_coro_reject'], + _emval_coro_suspend: (promiseHandle, awaiterPtr) => { + Emval.toValue(promiseHandle) + .then((result) => __emval_coro_resume(awaiterPtr, Emval.toHandle(result)), + (error) => __emval_coro_reject(awaiterPtr, Emval.toHandle(error))); }, - _emval_coro_make_promise__deps: ['$Emval', '__cxa_rethrow'], + _emval_coro_make_promise__deps: ['$Emval'], _emval_coro_make_promise: (resolveHandlePtr, rejectHandlePtr) => { return Emval.toHandle(new Promise((resolve, reject) => { - const rejectWithCurrentException = () => { - try { - // Use __cxa_rethrow which already has mechanism for generating - // user-friendly error message and stacktrace from C++ exception - // if EXCEPTION_STACK_TRACES is enabled and numeric exception - // with metadata optimised out otherwise. - ___cxa_rethrow(); - } catch (e) { - // But catch it so that it rejects the promise instead of throwing - // in an unpredictable place during async execution. - reject(e); - } - }; - {{{ makeSetValue('resolveHandlePtr', '0', 'Emval.toHandle(resolve)', '*') }}}; - {{{ makeSetValue('rejectHandlePtr', '0', 'Emval.toHandle(rejectWithCurrentException)', '*') }}}; + {{{ makeSetValue('rejectHandlePtr', '0', 'Emval.toHandle(reject)', '*') }}}; })); }, + + _emval_from_current_cxa_exception__deps: ['$Emval', '__cxa_rethrow'], + _emval_from_current_cxa_exception: () => { + try { + // Use __cxa_rethrow which already has mechanism for generating + // user-friendly error message and stacktrace from C++ exception + // if EXCEPTION_STACK_TRACES is enabled and numeric exception + // with metadata optimised out otherwise. + ___cxa_rethrow(); + } catch (e) { + return Emval.toHandle(e); + } + }, }; addToLibrary(LibraryEmVal); diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index c5474f4847718..449caa86f6516 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -344,6 +344,7 @@ sigs = { _emval_decref__sig: 'vp', _emval_delete__sig: 'ipp', _emval_equals__sig: 'ipp', + _emval_from_current_cxa_exception__sig: 'p', _emval_get_global__sig: 'pp', _emval_get_method_caller__sig: 'pipi', _emval_get_module_property__sig: 'pp', diff --git a/system/include/emscripten/val.h b/system/include/emscripten/val.h index c44c8166a7d7f..d2b3da4d922a4 100644 --- a/system/include/emscripten/val.h +++ b/system/include/emscripten/val.h @@ -21,10 +21,10 @@ #include #if __cplusplus >= 202002L #include +#include #include #endif - namespace emscripten { class val; @@ -118,6 +118,7 @@ EM_VAL _emval_iter_next(EM_VAL iterator); #if __cplusplus >= 202002L void _emval_coro_suspend(EM_VAL promise, void* coro_ptr); +EM_VAL _emval_from_current_cxa_exception(); EM_VAL _emval_coro_make_promise(EM_VAL *resolve, EM_VAL *reject); #endif @@ -729,7 +730,8 @@ class val::awaiter { bool await_ready() { return false; } // On suspend, store the coroutine handle and invoke a helper that will do - // a rough equivalent of `promise.then(value => this.resume_with(value))`. + // a rough equivalent of + // `promise.then(value => this.resume_with(value)).catch(error => this.reject_with(error))`. void await_suspend(std::coroutine_handle handle) { internal::_emval_coro_suspend(std::get(state).as_handle(), this); state.emplace(handle); @@ -743,9 +745,16 @@ class val::awaiter { coro.resume(); } + // When JS invokes `reject_with` with some error value, reject currently suspended + // coroutine's promise with the error value and destroy coroutine frame, because + // in this scenario coroutine never reaches final_suspend point to be destroyed automatically. + void reject_with(val&& error); + // `await_resume` finalizes the awaiter and should return the result // of the `co_await ...` expression - in our case, the stored value. - val await_resume() { return std::move(std::get(state)); } + val await_resume() { + return std::move(std::get(state)); + } }; inline val::awaiter val::operator co_await() const { @@ -756,7 +765,7 @@ inline val::awaiter val::operator co_await() const { // that compiler uses to drive the coroutine itself // (`T::promise_type` is used for any coroutine with declared return type `T`). class val::promise_type { - val promise, resolve, reject_with_current_exception; + val promise, resolve, reject; public: // Create a `new Promise` and store it alongside the `resolve` and `reject` @@ -766,7 +775,7 @@ class val::promise_type { EM_VAL reject_handle; promise = val(internal::_emval_coro_make_promise(&resolve_handle, &reject_handle)); resolve = val(resolve_handle); - reject_with_current_exception = val(reject_handle); + reject = val(reject_handle); } // Return the stored promise as the actual return value of the coroutine. @@ -779,7 +788,19 @@ class val::promise_type { // On an unhandled exception, reject the stored promise instead of throwing // it asynchronously where it can't be handled. void unhandled_exception() { - reject_with_current_exception(); + try { + std::rethrow_exception(std::current_exception()); + } catch (const val& error) { + reject(error); + } catch (...) { + val error = val(internal::_emval_from_current_cxa_exception()); + reject(error); + } + } + + // Reject the stored promise due to rejection deeper in the call chain + void reject_with(val&& error) { + reject(std::move(error)); } // Resolve the stored promise on `co_return value`. @@ -788,6 +809,14 @@ class val::promise_type { resolve(std::forward(value)); } }; + +inline void val::awaiter::reject_with(val&& error) { + auto coro = std::move(std::get(state)); + auto& promise = coro.promise(); + promise.reject_with(std::move(error)); + coro.destroy(); +} + #endif // Declare a custom type that can be used in conjunction with diff --git a/system/lib/embind/bind.cpp b/system/lib/embind/bind.cpp index 3e1336ef9cfb3..2533eddf0bc7f 100644 --- a/system/lib/embind/bind.cpp +++ b/system/lib/embind/bind.cpp @@ -67,6 +67,10 @@ void _emval_coro_resume(val::awaiter* awaiter, EM_VAL result) { awaiter->resume_with(val::take_ownership(result)); } +void _emval_coro_reject(val::awaiter* awaiter, EM_VAL error) { + awaiter->reject_with(val::take_ownership(error)); +} + } namespace { diff --git a/test/embind/test_val_coro.cpp b/test/embind/test_val_coro.cpp index 442667e2a56cd..c962bd7931abe 100644 --- a/test/embind/test_val_coro.cpp +++ b/test/embind/test_val_coro.cpp @@ -16,10 +16,24 @@ EM_JS(EM_VAL, promise_sleep_impl, (int ms, int result), { return handle; }); +EM_JS(EM_VAL, promise_fail_impl, (), { + let promise = new Promise((_, reject) => setTimeout(reject, 1, new Error("bang from JS promise!"))); + let handle = Emval.toHandle(promise); + // FIXME. See https://github.com/emscripten-core/emscripten/issues/16975. +#if __wasm64__ + handle = BigInt(handle); +#endif + return handle; +}); + val promise_sleep(int ms, int result = 0) { return val::take_ownership(promise_sleep_impl(ms, result)); } +val promise_fail() { + return val::take_ownership(promise_fail_impl()); +} + // Test that we can subclass and make custom awaitable types. template class typed_promise: public val { @@ -37,7 +51,13 @@ class typed_promise: public val { } }; +template val asyncCoro() { + co_return co_await asyncCoro(); +} + +template <> +val asyncCoro<0>() { // check that just sleeping works co_await promise_sleep(1); // check that sleeping and receiving value works @@ -50,12 +70,32 @@ val asyncCoro() { co_return 34; } +template val throwingCoro() { + co_await throwingCoro(); + co_return 56; +} + +template <> +val throwingCoro<0>() { throw std::runtime_error("bang from throwingCoro!"); co_return 56; } +template +val failingPromise() { + co_await failingPromise(); + co_return 65; +} + +template <> +val failingPromise<0>() { + co_await promise_fail(); + co_return 65; +} + EMSCRIPTEN_BINDINGS(test_val_coro) { - function("asyncCoro", asyncCoro); - function("throwingCoro", throwingCoro); + function("asyncCoro", asyncCoro<3>); + function("throwingCoro", throwingCoro<3>); + function("failingPromise", failingPromise<3>); } diff --git a/test/test_core.py b/test/test_core.py index 635d26278f753..fac470a08b8aa 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -7480,7 +7480,7 @@ def test_embind_val_coro(self): self.emcc_args += ['-std=c++20', '--bind', '--post-js=post.js'] self.do_runf('embind/test_val_coro.cpp', '34\n') - def test_embind_val_coro_caught(self): + def test_embind_val_coro_propogate_cpp_exception(self): self.set_setting('EXCEPTION_STACK_TRACES') create_file('post.js', r'''Module.onRuntimeInitialized = () => { Module.throwingCoro().then( @@ -7491,6 +7491,17 @@ def test_embind_val_coro_caught(self): self.emcc_args += ['-std=c++20', '--bind', '--post-js=post.js', '-fexceptions'] self.do_runf('embind/test_val_coro.cpp', 'rejected with: std::runtime_error: bang from throwingCoro!\n') + def test_embind_val_coro_propogate_js_error(self): + self.set_setting('EXCEPTION_STACK_TRACES') + create_file('post.js', r'''Module.onRuntimeInitialized = () => { + Module.failingPromise().then( + console.log, + err => console.error(`rejected with: ${err.message}`) + ); + }''') + self.emcc_args += ['-std=c++20', '--bind', '--post-js=post.js', '-fexceptions'] + self.do_runf('embind/test_val_coro.cpp', 'rejected with: bang from JS promise!\n') + def test_embind_dynamic_initialization(self): self.emcc_args += ['-lembind'] self.do_run_in_out_file_test('embind/test_dynamic_initialization.cpp') diff --git a/tools/emscripten.py b/tools/emscripten.py index eac5c5a446996..dc5e06df4745a 100644 --- a/tools/emscripten.py +++ b/tools/emscripten.py @@ -1166,6 +1166,7 @@ def create_pointer_conversion_wrappers(metadata): 'emscripten_proxy_finish': '_p', 'emscripten_proxy_execute_queue': '_p', '_emval_coro_resume': '_pp', + '_emval_coro_reject': '_pp', 'emscripten_main_runtime_thread_id': 'p', '_emscripten_set_offscreencanvas_size_on_thread': '_pp__', 'fileno': '_p', diff --git a/tools/system_libs.py b/tools/system_libs.py index d82ab077bb3a2..5d918de9bdfce 100644 --- a/tools/system_libs.py +++ b/tools/system_libs.py @@ -1927,7 +1927,7 @@ class libwebgpu_cpp(MTLibrary): src_files = ['webgpu_cpp.cpp'] -class libembind(Library): +class libembind(MTLibrary): name = 'libembind' never_force = True