wasm: implement promise handler using suspendresumecontrol

This is required to make sure we resume the wasm instance
when a promise resolves. As a bonus QWasmSuspendResumeControl
already implements the JS -> C++ callback mapping, and
we can removed the fixed-4 ThunkPool which the current
implementation is using.

The implementation is straightforward, where the only
snag is that cleanup must be done in the finally callback.

Implement Promise::all by calling JS Promise.all(). This
function returns a new Promise, which we can adopt.

Make two changes to the test:

- remove throwInThen(): We no longer support propagating
  JS exceptions from the then() handler to the catch function.
  (catching a rejected promise still works). As far as
  I can see this functionality is not used in qtbase.

- In finallyWithThen(), change shared_ptr<bool> to plain
  pointer. This works around a (mysterious) issue where we
  were not getting the correct value when reading from the
  shared_ptr.

Change-Id: I8fb11b66ecba74f80708bd39eeeac59bb62f3786
Reviewed-by: Lorn Potter <lorn.potter@qt.io>
This commit is contained in:
Morten Sørvig 2025-04-09 12:11:16 +02:00
parent 03699cc0ff
commit f5231e2dbb
2 changed files with 53 additions and 344 deletions

View File

@ -91,280 +91,6 @@ private:
File file;
};
enum class CallbackType {
Then,
Catch,
Finally,
};
void validateCallbacks(const PromiseCallbacks& callbacks) {
Q_ASSERT(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc);
}
using ThunkId = int;
#define THUNK_NAME(type, i) callbackThunk##type##i
// A resource pool for exported promise thunk functions. ThunkPool::poolSize sets of
// 3 promise thunks (then, catch, finally) are exported and can be used by promises
// in C++. To allocate a thunk, call allocateThunk. When a thunk is ready for use,
// a callback with allocation RAII object ThunkAllocation will be returned. Deleting
// the object frees the thunk and automatically makes any pending allocateThunk call
// run its callback with a free thunk slot.
class ThunkPool {
public:
static constexpr size_t poolSize = 4;
// An allocation for a thunk function set. Following the RAII pattern, destruction of
// this objects frees a corresponding thunk pool entry.
// To actually make the thunks react to a js promise's callbacks, call bindToPromise.
class ThunkAllocation {
public:
ThunkAllocation(int thunkId, ThunkPool* pool) : m_thunkId(thunkId), m_pool(pool) {}
~ThunkAllocation() {
m_pool->free(m_thunkId);
}
// The id of the underlaying thunk set
int id() const { return m_thunkId; }
// Binds the corresponding thunk set to the js promise 'target'.
void bindToPromise(emscripten::val target, const PromiseCallbacks& callbacks) {
using namespace emscripten;
if (Q_LIKELY(callbacks.thenFunc)) {
target = target.call<val>(
"then",
emscripten::val::module_property(thunkName(CallbackType::Then, id()).data()));
}
if (callbacks.catchFunc) {
target = target.call<val>(
"catch",
emscripten::val::module_property(thunkName(CallbackType::Catch, id()).data()));
}
// Guarantee the invocation of at least one callback by always
// registering 'finally'. This is required by WebPromiseManager
// design
target = target.call<val>(
"finally", emscripten::val::module_property(
thunkName(CallbackType::Finally, id()).data()));
}
private:
int m_thunkId;
ThunkPool* m_pool;
};
ThunkPool() {
std::iota(m_free.begin(), m_free.end(), 0);
}
void setThunkCallback(std::function<void(int, CallbackType, emscripten::val)> callback) {
m_callback = std::move(callback);
}
void allocateThunk(std::function<void(std::unique_ptr<ThunkAllocation>)> onAllocated) {
if (m_free.empty()) {
m_pendingAllocations.push_back(std::move(onAllocated));
return;
}
const int thunkId = m_free.back();
m_free.pop_back();
onAllocated(std::make_unique<ThunkAllocation>(thunkId, this));
}
static QByteArray thunkName(CallbackType type, size_t i) {
return QStringLiteral("promiseCallback%1%2").arg([type]() -> QString {
switch (type) {
case CallbackType::Then:
return QStringLiteral("Then");
case CallbackType::Catch:
return QStringLiteral("Catch");
case CallbackType::Finally:
return QStringLiteral("Finally");
}
}()).arg(i).toLatin1();
}
static ThunkPool* get();
#define THUNK(i) \
static void THUNK_NAME(Then, i)(emscripten::val result) \
{ \
get()->onThunkCalled(i, CallbackType::Then, std::move(result)); \
} \
static void THUNK_NAME(Catch, i)(emscripten::val result) \
{ \
get()->onThunkCalled(i, CallbackType::Catch, std::move(result)); \
} \
static void THUNK_NAME(Finally, i)() \
{ \
get()->onThunkCalled(i, CallbackType::Finally, emscripten::val::undefined()); \
}
THUNK(0);
THUNK(1);
THUNK(2);
THUNK(3);
#undef THUNK
private:
void onThunkCalled(int index, CallbackType type, emscripten::val result) {
m_callback(index, type, std::move(result));
}
void free(int thunkId) {
if (m_pendingAllocations.empty()) {
// Return the thunk to the free pool
m_free.push_back(thunkId);
return;
}
// Take the next enqueued allocation and reuse the thunk
auto allocation = m_pendingAllocations.back();
m_pendingAllocations.pop_back();
allocation(std::make_unique<ThunkAllocation>(thunkId, this));
}
std::function<void(int, CallbackType, emscripten::val)> m_callback;
std::vector<int> m_free = std::vector<int>(poolSize);
std::vector<std::function<void(std::unique_ptr<ThunkAllocation>)>> m_pendingAllocations;
};
Q_GLOBAL_STATIC(ThunkPool, g_thunkPool)
ThunkPool* ThunkPool::get()
{
return g_thunkPool;
}
#define CALLBACK_BINDING(i) \
emscripten::function(ThunkPool::thunkName(CallbackType::Then, i).data(), \
&ThunkPool::THUNK_NAME(Then, i)); \
emscripten::function(ThunkPool::thunkName(CallbackType::Catch, i).data(), \
&ThunkPool::THUNK_NAME(Catch, i)); \
emscripten::function(ThunkPool::thunkName(CallbackType::Finally, i).data(), \
&ThunkPool::THUNK_NAME(Finally, i));
EMSCRIPTEN_BINDINGS(qtThunkPool) {
CALLBACK_BINDING(0)
CALLBACK_BINDING(1)
CALLBACK_BINDING(2)
CALLBACK_BINDING(3)
}
#undef CALLBACK_BINDING
#undef THUNK_NAME
class WebPromiseManager
{
public:
WebPromiseManager();
~WebPromiseManager();
WebPromiseManager(const WebPromiseManager& other) = delete;
WebPromiseManager(WebPromiseManager&& other) = delete;
WebPromiseManager& operator=(const WebPromiseManager& other) = delete;
WebPromiseManager& operator=(WebPromiseManager&& other) = delete;
void adoptPromise(emscripten::val target, PromiseCallbacks callbacks);
static WebPromiseManager* get();
private:
struct RegistryEntry {
PromiseCallbacks callbacks;
std::unique_ptr<ThunkPool::ThunkAllocation> allocation;
};
static std::optional<CallbackType> parseCallbackType(emscripten::val callbackType);
void subscribeToJsPromiseCallbacks(int i, const PromiseCallbacks& callbacks, emscripten::val jsContextfulPromise);
void promiseThunkCallback(int i, CallbackType type, emscripten::val result);
void registerPromise(std::unique_ptr<ThunkPool::ThunkAllocation> allocation, PromiseCallbacks promise);
void unregisterPromise(ThunkId context);
std::array<RegistryEntry, ThunkPool::poolSize> m_promiseRegistry;
};
Q_GLOBAL_STATIC(WebPromiseManager, webPromiseManager)
WebPromiseManager::WebPromiseManager()
{
ThunkPool::get()->setThunkCallback(std::bind(
&WebPromiseManager::promiseThunkCallback, this,
std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
}
std::optional<CallbackType>
WebPromiseManager::parseCallbackType(emscripten::val callbackType)
{
if (!callbackType.isString())
return std::nullopt;
const std::string data = callbackType.as<std::string>();
if (data == "then")
return CallbackType::Then;
if (data == "catch")
return CallbackType::Catch;
if (data == "finally")
return CallbackType::Finally;
return std::nullopt;
}
WebPromiseManager::~WebPromiseManager() = default;
WebPromiseManager *WebPromiseManager::get()
{
return webPromiseManager();
}
void WebPromiseManager::promiseThunkCallback(int context, CallbackType type, emscripten::val result)
{
auto* promiseState = &m_promiseRegistry[context];
auto* callbacks = &promiseState->callbacks;
switch (type) {
case CallbackType::Then:
callbacks->thenFunc(result);
break;
case CallbackType::Catch:
callbacks->catchFunc(result);
break;
case CallbackType::Finally:
// Final callback may be empty, used solely for promise unregistration
if (callbacks->finallyFunc) {
callbacks->finallyFunc();
}
unregisterPromise(context);
break;
}
}
void WebPromiseManager::registerPromise(
std::unique_ptr<ThunkPool::ThunkAllocation> allocation,
PromiseCallbacks callbacks)
{
const ThunkId id = allocation->id();
m_promiseRegistry[id] =
RegistryEntry {std::move(callbacks), std::move(allocation)};
}
void WebPromiseManager::unregisterPromise(ThunkId context)
{
m_promiseRegistry[context] = {};
}
void WebPromiseManager::adoptPromise(emscripten::val target, PromiseCallbacks callbacks) {
ThunkPool::get()->allocateThunk([=](std::unique_ptr<ThunkPool::ThunkAllocation> allocation) {
allocation->bindToPromise(std::move(target), callbacks);
registerPromise(std::move(allocation), std::move(callbacks));
});
}
#if defined(QT_STATIC)
EM_JS(bool, jsHaveAsyncify, (), { return typeof Asyncify !== "undefined"; });
@ -726,58 +452,62 @@ EventCallback::EventCallback(emscripten::val element, const std::string &name,
}
namespace Promise {
void adoptPromise(emscripten::val promiseObject, PromiseCallbacks callbacks) {
validateCallbacks(callbacks);
void Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks)
{
Q_ASSERT_X(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc,
"Promise::adoptPromise", "must provide at least one callback function");
WebPromiseManager::get()->adoptPromise(
std::move(promiseObject), std::move(callbacks));
}
QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
Q_ASSERT(suspendResume);
void all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks) {
struct State {
std::map<int, emscripten::val> results;
int remainingThenCallbacks;
int remainingFinallyCallbacks;
};
// Registers a possibly-empty callback with suspendresumecontrol. Returns
// the the handler index if there was a valid callback, or nullopt.
auto registerCallback = [suspendResume](std::function<void(emscripten::val)> cb) -> std::optional<uint32_t>{
if (!cb)
return std::nullopt;
return std::optional<uint32_t>{suspendResume->registerEventHandler(std::move(cb))};
};
validateCallbacks(callbacks);
// Register callbacks with suspendresumecontrol, so that it can
// resume the wasm instance when the promise resolves. The finally
// callback is sepecial, since we remove the event handlers there
// as cleanup, including the event handler for the cleanup function
// itself.
std::optional<uint32_t> thenIndex = registerCallback(std::move(callbacks.thenFunc));
std::optional<uint32_t> catchIndex = registerCallback(std::move(callbacks.catchFunc));
std::shared_ptr<uint32_t> finallyIndex = std::make_shared<uint32_t>();;
auto finallyFunc = callbacks.finallyFunc;
auto state = std::make_shared<State>();
state->remainingThenCallbacks = state->remainingFinallyCallbacks = promises.size();
// 'Finally' callback which performs clean-up and calls the user-provided finally.
auto finally = [suspendResume, thenIndex, catchIndex, finallyIndex, finallyFunc](emscripten::val){
for (size_t i = 0; i < promises.size(); ++i) {
PromiseCallbacks individualPromiseCallback;
if (callbacks.thenFunc) {
individualPromiseCallback.thenFunc = [i, state, callbacks](emscripten::val partialResult) mutable {
state->results.emplace(i, std::move(partialResult));
if (!--(state->remainingThenCallbacks)) {
std::vector<emscripten::val> transformed;
for (auto& data : state->results) {
transformed.push_back(std::move(data.second));
}
callbacks.thenFunc(emscripten::val::array(std::move(transformed)));
}
};
}
if (callbacks.catchFunc) {
individualPromiseCallback.catchFunc = [state, callbacks](emscripten::val error) mutable {
callbacks.catchFunc(error);
};
}
individualPromiseCallback.finallyFunc = [state, callbacks]() mutable {
if (!--(state->remainingFinallyCallbacks)) {
if (callbacks.finallyFunc)
callbacks.finallyFunc();
// Explicitly reset here for verbosity, this would have been done automatically with the
// destruction of the adopted promise in WebPromiseManager.
state.reset();
}
};
// Clean up event handlers
if (thenIndex)
suspendResume->removeEventHandler(*thenIndex);
if (catchIndex)
suspendResume->removeEventHandler(*catchIndex);
suspendResume->removeEventHandler(*finallyIndex);
adoptPromise(std::move(promises.at(i)), std::move(individualPromiseCallback));
}
}
// Call user finally
if (finallyFunc)
finallyFunc();
};
*finallyIndex = suspendResume->registerEventHandler(std::move(finally));
// Set handlers on the promise
if (thenIndex)
promise.call<emscripten::val>("then", suspendResume->jsEventHandlerAt(*thenIndex));
if (catchIndex)
promise.call<emscripten::val>("catch", suspendResume->jsEventHandlerAt(*catchIndex));
promise.call<emscripten::val>("finally", suspendResume->jsEventHandlerAt(*finallyIndex));
}
void Promise::all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks)
{
auto arr = emscripten::val::array(promises);
auto all = val::global("Promise").call<emscripten::val>("all", arr);
return adoptPromise(all, callbacks);
}
// Asyncify and thread blocking: Normally, it's not possible to block the main

View File

@ -5,6 +5,7 @@
#include <QtCore/QEvent>
#include <QtCore/QMutex>
#include <QtCore/QObject>
#include <QtCore/QDebug>
#include <QtCore/private/qstdweb_p.h>
#include <qtwasmtestlib.h>
@ -53,7 +54,6 @@ private slots:
void multipleResolve();
void simpleReject();
void multipleReject();
void throwInThen();
void bareFinally();
void finallyWithThen();
void finallyWithThrow();
@ -188,28 +188,6 @@ void WasmPromiseTest::multipleReject()
}, promiseCount);
}
void WasmPromiseTest::throwInThen()
{
init();
qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
.thenFunc = [](val result) {
Q_UNUSED(result);
EM_ASM({
throw "Expected error";
});
},
.catchFunc = [](val error) {
QWASMCOMPARE("Expected error", error.as<std::string>());
QWASMSUCCESS();
}
}, std::string("throwInThen"));
EM_ASM({
testSupport.resolve["throwInThen"]();
});
}
void WasmPromiseTest::bareFinally()
{
init();
@ -229,7 +207,7 @@ void WasmPromiseTest::finallyWithThen()
{
init();
auto thenCalled = std::make_shared<bool>();
bool *thenCalled = new bool(false);
qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
.thenFunc = [thenCalled] (val result) {
Q_UNUSED(result);
@ -237,6 +215,7 @@ void WasmPromiseTest::finallyWithThen()
},
.finallyFunc = [thenCalled]() {
QWASMVERIFY(*thenCalled);
delete thenCalled;
QWASMSUCCESS();
}
}, std::string("finallyWithThen"));