Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 54 additions & 21 deletions system/include/emscripten/val.h
Original file line number Diff line number Diff line change
Expand Up @@ -667,40 +667,58 @@ inline val::iterator val::begin() const {
// of the type of the parent coroutine).
// This one is used for Promises represented by the `val` type.
class val::awaiter {
struct state_promise { val promise; };
struct state_coro {
std::coroutine_handle<> handle;
/// Is \c std::coroutine_handle<val::promise_type> ?
/// In other words, are we also enclosed by a JS Promise?
bool isValPromise = false;
};
struct state_result { val result; };
struct state_error { val error; };

// State machine holding awaiter's current state. One of:
// - initially created with promise
// - waiting with a given coroutine handle
// - completed with a result
std::variant<val, std::coroutine_handle<val::promise_type>, val> state;
std::variant<
state_promise, // Initially created with the JS Promise we're awaiting
state_coro, // Waiting with a given coroutine handle
state_result, // Resolved with result
state_error // Rejected with error
> state;

constexpr static std::size_t STATE_PROMISE = 0;
constexpr static std::size_t STATE_CORO = 1;
constexpr static std::size_t STATE_RESULT = 2;
void awaitSuspendImpl(state_coro coro) {
internal::_emval_coro_suspend(std::get<state_promise>(state).promise.as_handle(), this);
state.emplace<state_coro>(coro);
}

public:
awaiter(const val& promise)
: state(std::in_place_index<STATE_PROMISE>, promise) {}
awaiter(val promise)
: state(std::in_place_type<state_promise>, std::move(promise)) {}

// just in case, ensure nobody moves / copies this type around
awaiter(awaiter&&) = delete;
awaiter(const awaiter&) = delete;
awaiter& operator=(const awaiter&) = delete;

// Promises don't have a synchronously accessible "ready" state.
bool await_ready() { return false; }
bool await_ready() const { 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)).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);
awaitSuspendImpl({handle, true});
}

void await_suspend(std::coroutine_handle<> handle) {
awaitSuspendImpl({handle, false});
}

// When JS invokes `resume_with` with some value, store that value and resume
// the coroutine.
void resume_with(val&& result) {
auto coro = std::move(std::get<STATE_CORO>(state));
state.emplace<STATE_RESULT>(std::move(result));
coro.resume();
auto coro = std::get<state_coro>(state);
state.emplace<state_result>(std::move(result));
coro.handle.resume();
}

// When JS invokes `reject_with` with some error value, reject currently suspended
Expand All @@ -711,7 +729,10 @@ class val::awaiter {
// `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));
if (auto* result = std::get_if<state_result>(&state)) {
return std::move(result->result);
}
std::get<state_error>(state).error.throw_();
}
};

Expand Down Expand Up @@ -773,10 +794,22 @@ class val::promise_type {
};

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();
auto coro = std::get<state_coro>(state);

#ifndef __cpp_exceptions

// If we don't have C++ exceptions, we cannot catch the error.
// Thus, we can just reject an enclosing JS Promise.
if (coro.isValPromise) {
auto& promise = std::coroutine_handle<promise_type>(coro.handle).promise();
promise.reject_with(std::move(error));
coro.handle.destroy();
return;
}
#endif

state.emplace<state_error>(std::move(error));
coro.handle.resume();
}

#endif
Expand Down
46 changes: 46 additions & 0 deletions test/embind/test_val_coro.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <assert.h>
#include <functional>
#include <stdexcept>

using namespace emscripten;
Expand Down Expand Up @@ -94,8 +95,53 @@ val failingPromise<0>() {
co_return 65;
}

val catchCppExceptionPromise() {
try {
co_await throwingCoro<0>();
} catch (const std::runtime_error &) {
co_return val("successfully caught!");
}
co_return val("ignored??");
}


class callback_coro {
public:
class promise_type {
std::function<void(int)> callback_;
public:
promise_type(std::function<void(int)> callback)
: callback_(std::move(callback)) {}

callback_coro get_return_object() const noexcept {
return callback_coro();
}

auto initial_suspend() const noexcept { return std::suspend_never{}; }
auto final_suspend() const noexcept { return std::suspend_never{}; }

void return_value(int ret) { std::move(callback_)(ret); }

#ifdef __cpp_exceptions
[[noreturn]] void unhandled_exception() const noexcept { std::terminate(); }
#endif
};
};

callback_coro awaitWithCallback(std::function<void(int)>) {
co_await promise_sleep(1);
co_return 42;
}

void awaitInOtherPromise() {
awaitWithCallback([](int ret) { val::global("console").call<void>("log", ret); });
}


EMSCRIPTEN_BINDINGS(test_val_coro) {
function("asyncCoro", asyncCoro<3>);
function("throwingCoro", throwingCoro<3>);
function("failingPromise", failingPromise<3>);
function("catchCppExceptionPromise", catchCppExceptionPromise);
function("awaitInOtherPromise", awaitInOtherPromise);
}
15 changes: 15 additions & 0 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7642,6 +7642,21 @@ def test_embind_val_coro_propagate_js_error(self):
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', '-fexceptions', '-sINCOMING_MODULE_JS_API=onRuntimeInitialized', '--no-entry']
self.do_runf('embind/test_val_coro.cpp', 'rejected with: bang from JS promise!\n')

def test_embind_val_coro_catch_cpp_exception(self):
self.set_setting('EXCEPTION_STACK_TRACES')
create_file('pre.js', r'''Module.onRuntimeInitialized = () => {
Module.catchCppExceptionPromise().then(console.log);
}''')
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', '-fexceptions', '-sINCOMING_MODULE_JS_API=onRuntimeInitialized', '--no-entry']
self.do_runf('embind/test_val_coro.cpp', 'successfully caught!\n')

def test_embind_val_coro_await_in_other_promise(self):
create_file('pre.js', r'''Module.onRuntimeInitialized = () => {
Module.awaitInOtherPromise();
}''')
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', '-sINCOMING_MODULE_JS_API=onRuntimeInitialized', '--no-entry']
self.do_runf('embind/test_val_coro.cpp', '42\n')

def test_embind_dynamic_initialization(self):
self.cflags += ['-lembind']
self.do_run_in_out_file_test('embind/test_dynamic_initialization.cpp')
Expand Down
Loading