Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4a9611a
WIP: Enable awaiting Promises in non-Promise coroutines (closes #2606…
stevenwdv Feb 1, 2026
c6c44e2
Enable EXCEPTION_STACK_TRACES in test_embind_val_coro_catch_cpp_excep…
stevenwdv Feb 4, 2026
80b42d6
Remove TODO from `val::awaiter::reject_with`.
stevenwdv Feb 4, 2026
e8a0724
Merge remote-tracking branch 'origin/main' into val-coro-fixes
stevenwdv Feb 18, 2026
051c8ce
Progress on awaiting val in non-val coroutine
stevenwdv Feb 18, 2026
6fcdf4b
val::awaiter: detect JS errors and immediately reject
stevenwdv Feb 18, 2026
478cffe
Merge branch main into val-coro-fixes
stevenwdv Mar 31, 2026
9aaaef6
Fix val coroutine exceptions
stevenwdv Mar 31, 2026
0b94d69
Fix val::awaiter naming
stevenwdv Mar 31, 2026
912fc74
Fix libsigs.js
stevenwdv Mar 31, 2026
a1291fe
Remove TODO
stevenwdv Mar 31, 2026
e5461cd
Formatting
stevenwdv Mar 31, 2026
66177af
Disable `-fwasm-exceptions` test for wasm2js
stevenwdv Mar 31, 2026
8e14595
Fix no_wasm2js decorator for test_embind_val_coro_catch_cpp_exception…
stevenwdv Mar 31, 2026
ea8f476
Automatic rebaseline of codesize expectations. NFC
stevenwdv Apr 2, 2026
8268283
Merge remote-tracking branch 'origin/main' into val-coro-fixes
stevenwdv Apr 2, 2026
98f041d
Automatic rebaseline of codesize expectations. NFC
stevenwdv Apr 2, 2026
7451552
Simplify is_catchable_cpp_exception check
stevenwdv Apr 8, 2026
1c7998f
Comment formatting
stevenwdv Apr 8, 2026
4f8251b
Merge branch 'main' into val-coro-fixes
stevenwdv Apr 8, 2026
06acc36
Automatic rebaseline of codesize expectations. NFC
stevenwdv Apr 8, 2026
072afe6
Merge branch 'main' into val-coro-fixes
stevenwdv Apr 9, 2026
28ff776
Automatic rebaseline of codesize expectations. NFC
stevenwdv Apr 9, 2026
dcb575d
Clean up coroutine tests
stevenwdv Apr 9, 2026
09b278f
Remove default list argument
stevenwdv Apr 9, 2026
b9dab68
Automatic rebaseline of codesize expectations. NFC
stevenwdv Apr 9, 2026
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
14 changes: 14 additions & 0 deletions src/lib/libemval.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,20 @@ ${functionBody}
},
#endif

_emval_is_catchable_cpp_exception_object__deps: [
'$Emval',
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
'$isCppExceptionObject',
#endif
],
_emval_is_catchable_cpp_exception_object: (object) => {
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
return isCppExceptionObject(Emval.toValue(object));
#else
return false;
#endif
},

_emval_throw__deps: ['$Emval',
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
#if !DISABLE_EXCEPTION_CATCHING
Expand Down
1 change: 1 addition & 0 deletions src/lib/libsigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ sigs = {
_emval_instanceof__sig: 'ipp',
_emval_invoke__sig: 'dppppp',
_emval_invoke_i64__sig: 'jppppp',
_emval_is_catchable_cpp_exception_object__sig: 'ip',
_emval_is_number__sig: 'ip',
_emval_is_string__sig: 'ip',
_emval_iter_begin__sig: 'pp',
Expand Down
85 changes: 64 additions & 21 deletions system/include/emscripten/val.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ bool _emval_is_number(EM_VAL object);
bool _emval_is_string(EM_VAL object);
bool _emval_in(EM_VAL item, EM_VAL object);
bool _emval_delete(EM_VAL object, EM_VAL property);
bool _emval_is_catchable_cpp_exception_object(EM_VAL object);
[[noreturn]] bool _emval_throw(EM_VAL object);
EM_VAL _emval_await(EM_VAL promise);
EM_VAL _emval_iter_begin(EM_VAL iterable);
Expand Down Expand Up @@ -670,40 +671,63 @@ 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 std::coroutine_handle<val::promise_type>?
// In other words, are we also enclosed by a JS Promise?
bool is_val_promise = 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 await_suspend_impl(state_coro coro) {
// Use get_if instead of get because we want it to work with exceptions disabled.
auto* promise_ptr = std::get_if<state_promise>(&state);
assert(promise_ptr && "Invalid awaiter state: expected JS Promise. An awaiter cannot be awaited multiple times.");
internal::_emval_coro_suspend(promise_ptr->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);
await_suspend_impl({handle, true});
}

void await_suspend(std::coroutine_handle<> handle) {
await_suspend_impl({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_ptr = std::get_if<state_coro>(&state);
assert(coro_ptr && "Invalid awaiter state: expected suspended coroutine handle.");
auto coro = *coro_ptr;
state.emplace<state_result>(std::move(result));
coro.handle.resume();
}

// When JS invokes `reject_with` with some error value, reject currently suspended
Expand All @@ -714,7 +738,13 @@ 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);
}
// If a JS exception ended up here, it will be uncaught as C++ code cannot catch it
auto* error_ptr = std::get_if<state_error>(&state);
assert(error_ptr && "Invalid awaiter state: expected result or error.");
error_ptr->error.throw_();
}
};

Expand Down Expand Up @@ -776,10 +806,23 @@ 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_ptr = std::get_if<state_coro>(&state);
assert(coro_ptr && "Invalid awaiter state: expected suspended coroutine handle.");
auto coro = *coro_ptr;

if (coro.is_val_promise) {
if (!internal::_emval_is_catchable_cpp_exception_object(error.as_handle())) {
// C++ code cannot catch JS exceptions.
// Thus, we can just reject an enclosing JS Promise.
auto& promise = std::coroutine_handle<promise_type>::from_address(coro.handle.address()).promise();
promise.reject_with(std::move(error));
coro.handle.destroy();
return;
}
}

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

#endif
Expand Down
48 changes: 48 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,55 @@ 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_;
std::function<void()> errorCallback_;
public:
promise_type(
std::function<void(int)> callback,
std::function<void()> errorCallback = std::terminate)
: callback_(std::move(callback)),
errorCallback_(std::move(errorCallback)) {}

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) { callback_(ret); }

void unhandled_exception() const noexcept { errorCallback_(); }
};
};

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

void awaitInNonValCoro() {
sleepWithCallback([](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("awaitInNonValCoro", awaitInNonValCoro);
}
35 changes: 35 additions & 0 deletions test/embind/test_val_coro_noexcept.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/val.h>

using namespace emscripten;

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_fail() {
return val::take_ownership(promise_fail_impl());
}

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("failingPromise", failingPromise<3>);
}
35 changes: 30 additions & 5 deletions test/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7674,7 +7674,7 @@ def test_embind_val_coro(self):
self.do_runf('embind/test_val_coro.cpp', '34\n')

def test_embind_val_coro_propagate_cpp_exception(self):
self.set_setting('EXCEPTION_STACK_TRACES')
self.set_setting('EXCEPTION_STACK_TRACES') # For err.stack
create_file('pre.js', r'''Module.onRuntimeInitialized = () => {
Module.throwingCoro().then(
console.log,
Expand All @@ -7684,16 +7684,41 @@ def test_embind_val_coro_propagate_cpp_exception(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: std::runtime_error: bang from throwingCoro!\n')

def test_embind_val_coro_propagate_js_error(self):
self.set_setting('EXCEPTION_STACK_TRACES')
@parameterized({
'emscripten_eh': (['-fexceptions'],),
'disable_catching': ([],), # Use defaults: DISABLE_EXCEPTION_CATCHING, NO_DISABLE_EXCEPTION_THROWING
'no_exceptions': (['-fno-exceptions', '-Wno-coroutine-missing-unhandled-exception'],),
})
def test_embind_val_coro_propagate_js_error(self, extra_flags):
create_file('pre.js', r'''Module.onRuntimeInitialized = () => {
Module.failingPromise().then(
console.log,
err => console.error(`rejected with: ${err.message}`)
);
}''')
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')
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', *extra_flags, '-sINCOMING_MODULE_JS_API=onRuntimeInitialized', '--no-entry']
self.do_runf('embind/test_val_coro_noexcept.cpp', 'rejected with: bang from JS promise!\n')

@parameterized({
'emscripten_eh': (['-fexceptions'],),
'wasm_eh': (['-fwasm-exceptions'],),
})
def test_embind_val_coro_catch_cpp_exception(self, extra_flags):
if self.is_wasm2js() and '-fwasm-exceptions' in extra_flags:
self.skipTest('wasm2js does not support WASM exceptions')
self.set_setting('EXCEPTION_STACK_TRACES') # For debugging
create_file('pre.js', r'''Module.onRuntimeInitialized = () => {
Module.catchCppExceptionPromise().then(console.log);
}''')
self.cflags += ['-std=c++20', '--bind', '--pre-js=pre.js', *extra_flags, '-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_non_val_coro(self):
create_file('pre.js', r'''Module.onRuntimeInitialized = () => {
Module.awaitInNonValCoro();
}''')
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')
Comment thread
stevenwdv marked this conversation as resolved.

def test_embind_dynamic_initialization(self):
self.cflags += ['-lembind']
Expand Down
Loading