Skip to content

Commit 34ba269

Browse files
authored
[embind] Allow awaiting Promises in non-Promise coroutines & catching Promise C++ exceptions (#26195)
Fixes: #26064 Fixes: #25396 Fixes: #26289
1 parent e11edd8 commit 34ba269

6 files changed

Lines changed: 196 additions & 30 deletions

File tree

src/lib/libemval.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,20 @@ ${functionBody}
420420
},
421421
#endif
422422

423+
_emval_is_catchable_cpp_exception_object__deps: [
424+
'$Emval',
425+
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
426+
'$isCppExceptionObject',
427+
#endif
428+
],
429+
_emval_is_catchable_cpp_exception_object: (object) => {
430+
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
431+
return isCppExceptionObject(Emval.toValue(object));
432+
#else
433+
return false;
434+
#endif
435+
},
436+
423437
_emval_throw__deps: ['$Emval',
424438
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
425439
#if !DISABLE_EXCEPTION_CATCHING

src/lib/libsigs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ sigs = {
359359
_emval_instanceof__sig: 'ipp',
360360
_emval_invoke__sig: 'dppppp',
361361
_emval_invoke_i64__sig: 'jppppp',
362+
_emval_is_catchable_cpp_exception_object__sig: 'ip',
362363
_emval_is_number__sig: 'ip',
363364
_emval_is_string__sig: 'ip',
364365
_emval_iter_begin__sig: 'pp',

system/include/emscripten/val.h

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ bool _emval_is_number(EM_VAL object);
107107
bool _emval_is_string(EM_VAL object);
108108
bool _emval_in(EM_VAL item, EM_VAL object);
109109
bool _emval_delete(EM_VAL object, EM_VAL property);
110+
bool _emval_is_catchable_cpp_exception_object(EM_VAL object);
110111
[[noreturn]] bool _emval_throw(EM_VAL object);
111112
EM_VAL _emval_await(EM_VAL promise);
112113
EM_VAL _emval_iter_begin(EM_VAL iterable);
@@ -670,40 +671,63 @@ inline val::iterator val::begin() const {
670671
// of the type of the parent coroutine).
671672
// This one is used for Promises represented by the `val` type.
672673
class val::awaiter {
674+
struct state_promise { val promise; };
675+
struct state_coro {
676+
std::coroutine_handle<> handle;
677+
// Is std::coroutine_handle<val::promise_type>?
678+
// In other words, are we also enclosed by a JS Promise?
679+
bool is_val_promise = false;
680+
};
681+
struct state_result { val result; };
682+
struct state_error { val error; };
683+
673684
// State machine holding awaiter's current state. One of:
674-
// - initially created with promise
675-
// - waiting with a given coroutine handle
676-
// - completed with a result
677-
std::variant<val, std::coroutine_handle<val::promise_type>, val> state;
685+
std::variant<
686+
state_promise, // Initially created with the JS Promise we're awaiting
687+
state_coro, // Waiting with a given coroutine handle
688+
state_result, // Resolved with result
689+
state_error // Rejected with error
690+
> state;
678691

679-
constexpr static std::size_t STATE_PROMISE = 0;
680-
constexpr static std::size_t STATE_CORO = 1;
681-
constexpr static std::size_t STATE_RESULT = 2;
692+
void await_suspend_impl(state_coro coro) {
693+
// Use get_if instead of get because we want it to work with exceptions disabled.
694+
auto* promise_ptr = std::get_if<state_promise>(&state);
695+
assert(promise_ptr && "Invalid awaiter state: expected JS Promise. An awaiter cannot be awaited multiple times.");
696+
internal::_emval_coro_suspend(promise_ptr->promise.as_handle(), this);
697+
state.emplace<state_coro>(coro);
698+
}
682699

683700
public:
684-
awaiter(const val& promise)
685-
: state(std::in_place_index<STATE_PROMISE>, promise) {}
701+
awaiter(val promise)
702+
: state(std::in_place_type<state_promise>, std::move(promise)) {}
686703

687704
// just in case, ensure nobody moves / copies this type around
688-
awaiter(awaiter&&) = delete;
705+
awaiter(const awaiter&) = delete;
706+
awaiter& operator=(const awaiter&) = delete;
689707

690708
// Promises don't have a synchronously accessible "ready" state.
691-
bool await_ready() { return false; }
709+
bool await_ready() const { return false; }
692710

693711
// On suspend, store the coroutine handle and invoke a helper that will do
694712
// a rough equivalent of
695713
// `promise.then(value => this.resume_with(value)).catch(error => this.reject_with(error))`.
714+
696715
void await_suspend(std::coroutine_handle<val::promise_type> handle) {
697-
internal::_emval_coro_suspend(std::get<STATE_PROMISE>(state).as_handle(), this);
698-
state.emplace<STATE_CORO>(handle);
716+
await_suspend_impl({handle, true});
717+
}
718+
719+
void await_suspend(std::coroutine_handle<> handle) {
720+
await_suspend_impl({handle, false});
699721
}
700722

701723
// When JS invokes `resume_with` with some value, store that value and resume
702724
// the coroutine.
703725
void resume_with(val&& result) {
704-
auto coro = std::move(std::get<STATE_CORO>(state));
705-
state.emplace<STATE_RESULT>(std::move(result));
706-
coro.resume();
726+
auto* coro_ptr = std::get_if<state_coro>(&state);
727+
assert(coro_ptr && "Invalid awaiter state: expected suspended coroutine handle.");
728+
auto coro = *coro_ptr;
729+
state.emplace<state_result>(std::move(result));
730+
coro.handle.resume();
707731
}
708732

709733
// When JS invokes `reject_with` with some error value, reject currently suspended
@@ -714,7 +738,13 @@ class val::awaiter {
714738
// `await_resume` finalizes the awaiter and should return the result
715739
// of the `co_await ...` expression - in our case, the stored value.
716740
val await_resume() {
717-
return std::move(std::get<STATE_RESULT>(state));
741+
if (auto* result = std::get_if<state_result>(&state)) {
742+
return std::move(result->result);
743+
}
744+
// If a JS exception ended up here, it will be uncaught as C++ code cannot catch it
745+
auto* error_ptr = std::get_if<state_error>(&state);
746+
assert(error_ptr && "Invalid awaiter state: expected result or error.");
747+
error_ptr->error.throw_();
718748
}
719749
};
720750

@@ -746,12 +776,10 @@ class val::promise_type {
746776
auto initial_suspend() noexcept { return std::suspend_never{}; }
747777
auto final_suspend() noexcept { return std::suspend_never{}; }
748778

749-
// When exceptions are disabled we don't define unhandled_exception and rely
750-
// on the default terminate behavior.
751-
#ifdef __cpp_exceptions
752779
// On an unhandled exception, reject the stored promise instead of throwing
753780
// it asynchronously where it can't be handled.
754781
void unhandled_exception() {
782+
#ifdef __cpp_exceptions
755783
try {
756784
std::rethrow_exception(std::current_exception());
757785
} catch (const val& error) {
@@ -760,8 +788,10 @@ class val::promise_type {
760788
val error = val(internal::_emval_from_current_cxa_exception());
761789
reject(error);
762790
}
763-
}
791+
#else
792+
std::terminate();
764793
#endif
794+
}
765795

766796
// Reject the stored promise due to rejection deeper in the call chain
767797
void reject_with(val&& error) {
@@ -776,10 +806,23 @@ class val::promise_type {
776806
};
777807

778808
inline void val::awaiter::reject_with(val&& error) {
779-
auto coro = std::move(std::get<STATE_CORO>(state));
780-
auto& promise = coro.promise();
781-
promise.reject_with(std::move(error));
782-
coro.destroy();
809+
auto* coro_ptr = std::get_if<state_coro>(&state);
810+
assert(coro_ptr && "Invalid awaiter state: expected suspended coroutine handle.");
811+
auto coro = *coro_ptr;
812+
813+
if (coro.is_val_promise) {
814+
if (!internal::_emval_is_catchable_cpp_exception_object(error.as_handle())) {
815+
// C++ code cannot catch JS exceptions.
816+
// Thus, we can just reject an enclosing JS Promise.
817+
auto& promise = std::coroutine_handle<promise_type>::from_address(coro.handle.address()).promise();
818+
promise.reject_with(std::move(error));
819+
coro.handle.destroy();
820+
return;
821+
}
822+
}
823+
824+
state.emplace<state_error>(std::move(error));
825+
coro.handle.resume();
783826
}
784827

785828
#endif

test/embind/test_val_coro.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <emscripten/bind.h>
33
#include <emscripten/val.h>
44
#include <assert.h>
5+
#include <functional>
56
#include <stdexcept>
67

78
using namespace emscripten;
@@ -94,8 +95,55 @@ val failingPromise<0>() {
9495
co_return 65;
9596
}
9697

98+
val catchCppExceptionPromise() {
99+
try {
100+
co_await throwingCoro<0>();
101+
} catch (const std::runtime_error &) {
102+
co_return val("successfully caught!");
103+
}
104+
co_return val("ignored??");
105+
}
106+
107+
108+
class callback_coro {
109+
public:
110+
class promise_type {
111+
std::function<void(int)> callback_;
112+
std::function<void()> errorCallback_;
113+
public:
114+
promise_type(
115+
std::function<void(int)> callback,
116+
std::function<void()> errorCallback = std::terminate)
117+
: callback_(std::move(callback)),
118+
errorCallback_(std::move(errorCallback)) {}
119+
120+
callback_coro get_return_object() const noexcept {
121+
return callback_coro();
122+
}
123+
124+
auto initial_suspend() const noexcept { return std::suspend_never{}; }
125+
auto final_suspend() const noexcept { return std::suspend_never{}; }
126+
127+
void return_value(int ret) { callback_(ret); }
128+
129+
void unhandled_exception() const noexcept { errorCallback_(); }
130+
};
131+
};
132+
133+
callback_coro sleepWithCallback(std::function<void(int)>) {
134+
co_await promise_sleep(1);
135+
co_return 42;
136+
}
137+
138+
void awaitInNonValCoro() {
139+
sleepWithCallback([](int ret) { val::global("console").call<void>("log", ret); });
140+
}
141+
142+
97143
EMSCRIPTEN_BINDINGS(test_val_coro) {
98144
function("asyncCoro", asyncCoro<3>);
99145
function("throwingCoro", throwingCoro<3>);
100146
function("failingPromise", failingPromise<3>);
147+
function("catchCppExceptionPromise", catchCppExceptionPromise);
148+
function("awaitInNonValCoro", awaitInNonValCoro);
101149
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#include <emscripten.h>
2+
#include <emscripten/bind.h>
3+
#include <emscripten/val.h>
4+
5+
using namespace emscripten;
6+
7+
EM_JS(EM_VAL, promise_fail_impl, (), {
8+
let promise = new Promise((_, reject) => setTimeout(reject, 1, new Error("bang from JS promise!")));
9+
let handle = Emval.toHandle(promise);
10+
// FIXME. See https://github.com/emscripten-core/emscripten/issues/16975.
11+
#if __wasm64__
12+
handle = BigInt(handle);
13+
#endif
14+
return handle;
15+
});
16+
17+
val promise_fail() {
18+
return val::take_ownership(promise_fail_impl());
19+
}
20+
21+
template <size_t N>
22+
val failingPromise() {
23+
co_await failingPromise<N - 1>();
24+
co_return 65;
25+
}
26+
27+
template <>
28+
val failingPromise<0>() {
29+
co_await promise_fail();
30+
co_return 65;
31+
}
32+
33+
EMSCRIPTEN_BINDINGS(test_val_coro) {
34+
function("failingPromise", failingPromise<3>);
35+
}

test/test_core.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7693,7 +7693,7 @@ def test_embind_val_coro(self):
76937693
self.do_runf('embind/test_val_coro.cpp', '34\n')
76947694

76957695
def test_embind_val_coro_propagate_cpp_exception(self):
7696-
self.set_setting('EXCEPTION_STACK_TRACES')
7696+
self.set_setting('EXCEPTION_STACK_TRACES') # For err.stack
76977697
create_file('pre.js', r'''Module.onRuntimeInitialized = () => {
76987698
Module.throwingCoro().then(
76997699
console.log,
@@ -7703,16 +7703,41 @@ def test_embind_val_coro_propagate_cpp_exception(self):
77037703
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', '-fexceptions', '-sINCOMING_MODULE_JS_API=onRuntimeInitialized', '--no-entry']
77047704
self.do_runf('embind/test_val_coro.cpp', 'rejected with: std::runtime_error: bang from throwingCoro!\n')
77057705

7706-
def test_embind_val_coro_propagate_js_error(self):
7707-
self.set_setting('EXCEPTION_STACK_TRACES')
7706+
@parameterized({
7707+
'emscripten_eh': (['-fexceptions'],),
7708+
'disable_catching': ([],), # Use defaults: DISABLE_EXCEPTION_CATCHING, NO_DISABLE_EXCEPTION_THROWING
7709+
'no_exceptions': (['-fno-exceptions'],),
7710+
})
7711+
def test_embind_val_coro_propagate_js_error(self, extra_flags):
77087712
create_file('pre.js', r'''Module.onRuntimeInitialized = () => {
77097713
Module.failingPromise().then(
77107714
console.log,
77117715
err => console.error(`rejected with: ${err.message}`)
77127716
);
77137717
}''')
7714-
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', '-fexceptions', '-sINCOMING_MODULE_JS_API=onRuntimeInitialized', '--no-entry']
7715-
self.do_runf('embind/test_val_coro.cpp', 'rejected with: bang from JS promise!\n')
7718+
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', *extra_flags, '-sINCOMING_MODULE_JS_API=onRuntimeInitialized', '--no-entry']
7719+
self.do_runf('embind/test_val_coro_noexcept.cpp', 'rejected with: bang from JS promise!\n')
7720+
7721+
@parameterized({
7722+
'emscripten_eh': (['-fexceptions'],),
7723+
'wasm_eh': (['-fwasm-exceptions'],),
7724+
})
7725+
def test_embind_val_coro_catch_cpp_exception(self, extra_flags):
7726+
if self.is_wasm2js() and '-fwasm-exceptions' in extra_flags:
7727+
self.skipTest('wasm2js does not support WASM exceptions')
7728+
self.set_setting('EXCEPTION_STACK_TRACES') # For debugging
7729+
create_file('pre.js', r'''Module.onRuntimeInitialized = () => {
7730+
Module.catchCppExceptionPromise().then(console.log);
7731+
}''')
7732+
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', *extra_flags, '-sINCOMING_MODULE_JS_API=onRuntimeInitialized', '--no-entry']
7733+
self.do_runf('embind/test_val_coro.cpp', 'successfully caught!\n')
7734+
7735+
def test_embind_val_coro_await_in_non_val_coro(self):
7736+
create_file('pre.js', r'''Module.onRuntimeInitialized = () => {
7737+
Module.awaitInNonValCoro();
7738+
}''')
7739+
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', '-sINCOMING_MODULE_JS_API=onRuntimeInitialized', '--no-entry']
7740+
self.do_runf('embind/test_val_coro.cpp', '42\n')
77167741

77177742
def test_embind_dynamic_initialization(self):
77187743
self.cflags += ['-lembind']

0 commit comments

Comments
 (0)