-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Description
Version of emscripten/emsdk:
$ emcc -v
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 4.0.15 (09f52557f0d48b65b8c724853ed8f4e8bf80e669
)
clang version 22.0.0git (https:/github.com/llvm/llvm-project 3388d40684742e950b3c5d1d2dafe5a40695cfc1)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: <redacted>/emsdk/upstream/bin
I've tried out coroutines with val.h and embind as described in the documentation for val.h, and wanted to check if error handling is supported.
I've made a toy program, and it looks like exceptions are basically not available when calling other coroutines.
coroutines-tests.cpp
#include <stdexcept>
#include "emscripten.h"
#include "emscripten/bind.h"
#include "emscripten/val.h"
emscripten::val get_promise_with_resolvers() {
return emscripten::val::global("Promise").call<emscripten::val>("withResolvers");
}
static const int RESOLVED = 0;
static const int CPP_THROW_EXCEPTION = 1;
static const int JS_REJECTED = 2;
static const int CPP_THROW_OTHER = 3;
template <typename F>
emscripten::val test_coroutine(F &f) {
try {
co_await f();
co_return RESOLVED;
} catch (const std::exception & e) {
// For CPP exceptions thrown from f, we should be able to catch them here.
co_return CPP_THROW_EXCEPTION;
} catch (const emscripten::val & e) {
// For exceptions/rejected promises from JS, we should be able to catch them here.
co_return JS_REJECTED;
} catch (...) {
// we should at least be able to catch everything in here, including exceptions not applicable above
co_return CPP_THROW_OTHER;
}
}
emscripten::val returns() {
co_return emscripten::val::undefined();
}
emscripten::val test_returns() {
auto res = co_await test_coroutine(returns);
co_return res.as<int>() == RESOLVED;
}
emscripten::val throws_exception_immediately() {
throw std::runtime_error("throws_exception_immediately");
co_return emscripten::val::undefined();
}
emscripten::val test_throws_exception_immediately() {
auto res = co_await test_coroutine(throws_exception_immediately);
co_return res.as<int>() == CPP_THROW_EXCEPTION;
}
emscripten::val throws_after_timeout_0() {
auto promise_with_resolvers = get_promise_with_resolvers();
emscripten::val::global("setTimeout")(promise_with_resolvers["resolve"], 0);
co_await promise_with_resolvers["promise"];
throw std::runtime_error("throws_in_return");
co_return emscripten::val::undefined();
}
emscripten::val test_throws_after_timeout_0() {
auto res = co_await test_coroutine(throws_after_timeout_0);
co_return res.as<int>() == CPP_THROW_EXCEPTION;
}
emscripten::val returns_after_timeout_0() {
auto promise_with_resolvers = get_promise_with_resolvers();
emscripten::val::global("setTimeout")(promise_with_resolvers["resolve"], 0);
co_await promise_with_resolvers["promise"];
co_return emscripten::val::undefined();
}
emscripten::val test_returns_after_timeout_0() {
auto res = co_await test_coroutine(returns_after_timeout_0);
co_return res.as<int>() == RESOLVED;
}
emscripten::val rejects_after_timeout_0() {
auto promise_with_resolvers = get_promise_with_resolvers();
emscripten::val::global("setTimeout")(promise_with_resolvers["reject"], 0);
co_await promise_with_resolvers["promise"];
co_return emscripten::val::undefined();
}
emscripten::val test_rejects_after_timeout_0() {
auto res = co_await test_coroutine(rejects_after_timeout_0);
co_return res.as<int>() == JS_REJECTED;
}
emscripten::val throws_non_exception_immediately() {
throw "throws_non_exception_immediately";
co_return emscripten::val::undefined();
}
emscripten::val test_throws_non_exception_immediately() {
auto res = co_await test_coroutine(throws_non_exception_immediately);
co_return res.as<int>() == CPP_THROW_OTHER;
}
EMSCRIPTEN_BINDINGS(module) {
function("test_returns", &test_returns);
function("test_throws_after_timeout_0", &test_throws_after_timeout_0);
function("test_returns_after_timeout_0", &test_returns_after_timeout_0);
function("test_rejects_after_timeout_0", &test_rejects_after_timeout_0);
function("test_throws_exception_immediately", &test_throws_exception_immediately);
function("test_throws_non_exception_immediately", &test_throws_non_exception_immediately);
}
With the following build command
em++ -std=c++23 -sMODULARIZE -sEXPORT_ES6 -O0 -g2 -fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0 -o coroutines-tests.js ./coroutines-tests.cpp -lembind
(I have also tried with -sWASM_LEGACY_EXCEPTIONS=1
or with -fexceptions
instead)
To test it out I've made the following HTML document:
<!DOCTYPE html>
<html>
<head>
<title>wasm</title>
<link rel="shortcut icon" href="#">
<style>
:root {
color-scheme: light dark;
}
div {
line-height: 2em;
}
</style>
</head>
<body>
<script type="module">
import CoroutinesTestsFactory from "./coroutines-tests.js";
const tests = [
"test_returns",
"test_throws_after_timeout_0",
"test_returns_after_timeout_0",
"test_rejects_after_timeout_0",
"test_throws_exception_immediately",
"test_throws_non_exception_immediately"
].map((testName) => {
const div = document.createElement("div");
div.innerHTML = `<b>${testName}......</b><span><span>`;
const resultSpan = div.querySelector("span");
document.body.appendChild(div);
return [testName, resultSpan];
});
const CoroutinesTests = window.CoroutinesTests = await CoroutinesTestsFactory({});
await Promise.all(tests.map(async ([testName, resultSpan]) => {
let result = "";
try {
result = await CoroutinesTests[testName]() ? "success" : "failed";
} catch(e) {
result = `Test threw ${String(e)}`;
};
resultSpan.innerText = String(result);
}));
</script>
</body>
</html>
And served it via a simple http server (I used npx serve .
), and inspected the results. I was surprised to find that all tests fail where I except to be able to catch an exception or promise rejection.
The results when running in both latest stable Firefox and Chromium are the same (written to the document's body):
test_returns......success
test_throws_after_timeout_0......Test threw [object WebAssembly.Exception]
test_returns_after_timeout_0......success
test_rejects_after_timeout_0......Test threw undefined
test_throws_exception_immediately......Test threw [object WebAssembly.Exception]
test_throws_non_exception_immediately......Test threw [object WebAssembly.Exception]
I suspect this is due to the implementation of val::awaiter::reject_with
, which always rejects the underlying promise and destroys the coroutine, but I think it should resume the coroutine. In fact hacking around that code I managed to catch all thrown exceptions as emscripten::val
s.
I did this by making reject_with
store the error and resume instead of destroy the coroutine, then throw in await_resume
if an error was stored, but I'm not familiar enough with the internals of emscripten to preserve C++-based exceptions.
This is not ideal for when an exception is thrown from CPP as it doesn't seem trivial to correlate it back to the original exception type thrown, but seems reasonable for exceptions or promise rejections from the javascript side - for example when a fetch
fails due to CORS or an invalid URL.