diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp index 406243a33e9..d518029c7e9 100644 --- a/src/corelib/platform/wasm/qstdweb.cpp +++ b/src/corelib/platform/wasm/qstdweb.cpp @@ -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( - "then", - emscripten::val::module_property(thunkName(CallbackType::Then, id()).data())); - } - if (callbacks.catchFunc) { - target = target.call( - "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( - "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 callback) { - m_callback = std::move(callback); - } - - void allocateThunk(std::function)> 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(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(thunkId, this)); - } - - std::function m_callback; - - std::vector m_free = std::vector(poolSize); - std::vector)>> 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 allocation; - }; - - static std::optional 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 allocation, PromiseCallbacks promise); - void unregisterPromise(ThunkId context); - - std::array 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 -WebPromiseManager::parseCallbackType(emscripten::val callbackType) -{ - if (!callbackType.isString()) - return std::nullopt; - - const std::string data = callbackType.as(); - 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 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 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 promises, PromiseCallbacks callbacks) { - struct State { - std::map 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 cb) -> std::optional{ + if (!cb) + return std::nullopt; + return std::optional{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 thenIndex = registerCallback(std::move(callbacks.thenFunc)); + std::optional catchIndex = registerCallback(std::move(callbacks.catchFunc)); + std::shared_ptr finallyIndex = std::make_shared();; + auto finallyFunc = callbacks.finallyFunc; - auto state = std::make_shared(); - 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 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("then", suspendResume->jsEventHandlerAt(*thenIndex)); + if (catchIndex) + promise.call("catch", suspendResume->jsEventHandlerAt(*catchIndex)); + promise.call("finally", suspendResume->jsEventHandlerAt(*finallyIndex)); +} + +void Promise::all(std::vector promises, PromiseCallbacks callbacks) +{ + auto arr = emscripten::val::array(promises); + auto all = val::global("Promise").call("all", arr); + return adoptPromise(all, callbacks); } // Asyncify and thread blocking: Normally, it's not possible to block the main diff --git a/tests/manual/wasm/qstdweb/promise_main.cpp b/tests/manual/wasm/qstdweb/promise_main.cpp index c5f6f7f4122..5f1e5102c93 100644 --- a/tests/manual/wasm/qstdweb/promise_main.cpp +++ b/tests/manual/wasm/qstdweb/promise_main.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -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()); - 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 *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"));