Skip to content
Merged
40 changes: 20 additions & 20 deletions src/lib/libemval.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

@aheejin does this look reasonable to you? Or is the maybe a better way to do this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we wait for @aheejin's review or merge as-is? This piece of code was before this PR (I added it a while back because it was the only way to make it work that I found at the time), so I'm leaning towards let's merge it anyway.

Copy link
Collaborator

Choose a reason for hiding this comment

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

ping @aheejin

},
};

addToLibrary(LibraryEmVal);
1 change: 1 addition & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
41 changes: 35 additions & 6 deletions system/include/emscripten/val.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
#include <pthread.h>
#if __cplusplus >= 202002L
#include <coroutine>
#include <exception>
#include <variant>
#endif


namespace emscripten {

class val;
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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<val::promise_type> handle) {
internal::_emval_coro_suspend(std::get<STATE_PROMISE>(state).as_handle(), this);
state.emplace<STATE_CORO>(handle);
Expand All @@ -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_RESULT>(state)); }
val await_resume() {
return std::move(std::get<STATE_RESULT>(state));
}
};

inline val::awaiter val::operator co_await() const {
Expand All @@ -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`
Expand All @@ -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.
Expand All @@ -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`.
Expand All @@ -788,6 +809,14 @@ class val::promise_type {
resolve(std::forward<T>(value));
}
};

inline void val::awaiter::reject_with(val&& error) {
auto coro = std::move(std::get<STATE_CORO>(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
Expand Down
4 changes: 4 additions & 0 deletions system/lib/embind/bind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
44 changes: 42 additions & 2 deletions test/embind/test_val_coro.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <typename T>
class typed_promise: public val {
Expand All @@ -37,7 +51,13 @@ class typed_promise: public val {
}
};

template <size_t N>
val asyncCoro() {
co_return co_await asyncCoro<N - 1>();
}

template <>
val asyncCoro<0>() {
// check that just sleeping works
co_await promise_sleep(1);
// check that sleeping and receiving value works
Expand All @@ -50,12 +70,32 @@ val asyncCoro() {
co_return 34;
}

template <size_t N>
val throwingCoro() {
co_await throwingCoro<N - 1>();
co_return 56;
}

template <>
val throwingCoro<0>() {
throw std::runtime_error("bang from throwingCoro!");
co_return 56;
}

template <size_t N>
val failingPromise() {
co_await failingPromise<N - 1>();
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>);
}
13 changes: 12 additions & 1 deletion test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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')
Expand Down
1 change: 1 addition & 0 deletions tools/emscripten.py
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,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',
Expand Down
2 changes: 1 addition & 1 deletion tools/system_libs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1927,7 +1927,7 @@ class libwebgpu_cpp(MTLibrary):
src_files = ['webgpu_cpp.cpp']


class libembind(Library):
class libembind(MTLibrary):
name = 'libembind'
never_force = True

Expand Down