diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 5da5205f1cc..5d498b7f5d0 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -1360,18 +1360,3 @@ if(APPLE AND QT_FEATURE_framework AND QT_FEATURE_separate_debug_info) DESTINATION "${dsym_script_install_dir}" ) endif() - -if(WASM) - set(wasm_injections - "${CMAKE_CURRENT_SOURCE_DIR}/platform/wasm/qtcontextfulpromise_injection.js" - ) - - qt_internal_add_resource(Core "wasminjections" - PREFIX - "/injections" - BASE - "${CMAKE_CURRENT_SOURCE_DIR}/platform/wasm" - FILES - ${wasm_injections} - ) -endif() diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp index d13c374dc4a..43cd079e9db 100644 --- a/src/corelib/platform/wasm/qstdweb.cpp +++ b/src/corelib/platform/wasm/qstdweb.cpp @@ -10,12 +10,12 @@ #include #include +#include + QT_BEGIN_NAMESPACE namespace qstdweb { -const char makeContextfulPromiseFunctionName[] = "makePromise"; - typedef double uint53_t; // see Number.MAX_SAFE_INTEGER namespace { enum class CallbackType { @@ -28,28 +28,166 @@ void validateCallbacks(const PromiseCallbacks& callbacks) { Q_ASSERT(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc); } -void injectScript(const std::string& source, const std::string& injectionName) +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())); + } + if (callbacks.finallyFunc) { + 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() { - using namespace emscripten; - - auto script = val::global("document").call("createElement", val("script")); - auto head = val::global("document").call("getElementsByTagName", val("head")); - - script.call("setAttribute", val("qtinjection"), val(injectionName)); - script.set("innerText", val(source)); - - head[0].call("appendChild", std::move(script)); + return g_thunkPool; } -using PromiseContext = int; +#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: - static const char contextfulPromiseSupportObjectName[]; - - static const char webPromiseManagerCallbackThunkExportName[]; - WebPromiseManager(); ~WebPromiseManager(); @@ -62,43 +200,30 @@ public: static WebPromiseManager* get(); - static void callbackThunk(emscripten::val callbackType, emscripten::val context, emscripten::val result); - private: + struct RegistryEntry { + PromiseCallbacks callbacks; + std::unique_ptr allocation; + }; + static std::optional parseCallbackType(emscripten::val callbackType); - void subscribeToJsPromiseCallbacks(const PromiseCallbacks& callbacks, emscripten::val jsContextfulPromise); - void callback(CallbackType type, emscripten::val context, emscripten::val result); + void subscribeToJsPromiseCallbacks(int i, const PromiseCallbacks& callbacks, emscripten::val jsContextfulPromise); + void promiseThunkCallback(int i, CallbackType type, emscripten::val result); - void registerPromise(PromiseContext context, PromiseCallbacks promise); - void unregisterPromise(PromiseContext context); + void registerPromise(std::unique_ptr allocation, PromiseCallbacks promise); + void unregisterPromise(ThunkId context); - QHash m_promiseRegistry; - int m_nextContextId = 0; + std::array m_promiseRegistry; }; -static void qStdWebCleanup() -{ - auto window = emscripten::val::global("window"); - auto contextfulPromiseSupport = window[WebPromiseManager::contextfulPromiseSupportObjectName]; - if (contextfulPromiseSupport.isUndefined()) - return; - - contextfulPromiseSupport.call("removeRef"); -} - -const char WebPromiseManager::webPromiseManagerCallbackThunkExportName[] = "qtStdWebWebPromiseManagerCallbackThunk"; -const char WebPromiseManager::contextfulPromiseSupportObjectName[] = "qtContextfulPromiseSupport"; - Q_GLOBAL_STATIC(WebPromiseManager, webPromiseManager) WebPromiseManager::WebPromiseManager() { - QFile injection(QStringLiteral(":/injections/qtcontextfulpromise_injection.js")); - if (!injection.open(QIODevice::ReadOnly)) - qFatal("Missing resource"); - injectScript(injection.readAll().toStdString(), "contextfulpromise"); - qAddPostRoutine(&qStdWebCleanup); + ThunkPool::get()->setThunkCallback(std::bind( + &WebPromiseManager::promiseThunkCallback, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } std::optional @@ -124,75 +249,51 @@ WebPromiseManager *WebPromiseManager::get() return webPromiseManager(); } -void WebPromiseManager::callbackThunk(emscripten::val callbackType, - emscripten::val context, - emscripten::val result) +void WebPromiseManager::promiseThunkCallback(int context, CallbackType type, emscripten::val result) { - auto parsedCallbackType = parseCallbackType(callbackType); - if (!parsedCallbackType) { - qFatal("Bad callback type"); - } - WebPromiseManager::get()->callback(*parsedCallbackType, context, std::move(result)); -} - -void WebPromiseManager::subscribeToJsPromiseCallbacks(const PromiseCallbacks& callbacks, emscripten::val jsContextfulPromiseObject) { - using namespace emscripten; - - if (Q_LIKELY(callbacks.thenFunc)) - jsContextfulPromiseObject = jsContextfulPromiseObject.call("then"); - if (callbacks.catchFunc) - jsContextfulPromiseObject = jsContextfulPromiseObject.call("catch"); - if (callbacks.finallyFunc) - jsContextfulPromiseObject = jsContextfulPromiseObject.call("finally"); -} - -void WebPromiseManager::callback(CallbackType type, emscripten::val context, emscripten::val result) -{ - auto found = m_promiseRegistry.find(context.as()); - if (found == m_promiseRegistry.end()) { - return; - } + auto* promiseState = &m_promiseRegistry[context]; + auto* callbacks = &promiseState->callbacks; bool expectingOtherCallbacks; switch (type) { case CallbackType::Then: - found->thenFunc(result); + callbacks->thenFunc(result); // At this point, if there is no finally function, we are sure that the Catch callback won't be issued. - expectingOtherCallbacks = !!found->finallyFunc; + expectingOtherCallbacks = !!callbacks->finallyFunc; break; case CallbackType::Catch: - found->catchFunc(result); - expectingOtherCallbacks = !!found->finallyFunc; + callbacks->catchFunc(result); + expectingOtherCallbacks = !!callbacks->finallyFunc; break; case CallbackType::Finally: - found->finallyFunc(); + callbacks->finallyFunc(); expectingOtherCallbacks = false; break; } if (!expectingOtherCallbacks) - unregisterPromise(context.as()); + unregisterPromise(context); } -void WebPromiseManager::registerPromise(PromiseContext context, PromiseCallbacks callbacks) +void WebPromiseManager::registerPromise( + std::unique_ptr allocation, + PromiseCallbacks callbacks) { - m_promiseRegistry.emplace(context, std::move(callbacks)); + const ThunkId id = allocation->id(); + m_promiseRegistry[id] = + RegistryEntry {std::move(callbacks), std::move(allocation)}; } -void WebPromiseManager::unregisterPromise(PromiseContext context) +void WebPromiseManager::unregisterPromise(ThunkId context) { - m_promiseRegistry.remove(context); + m_promiseRegistry[context] = {}; } void WebPromiseManager::adoptPromise(emscripten::val target, PromiseCallbacks callbacks) { - emscripten::val context(m_nextContextId++); - - auto jsContextfulPromise = emscripten::val::global("window") - [contextfulPromiseSupportObjectName].call( - makeContextfulPromiseFunctionName, target, context, - emscripten::val::module_property(webPromiseManagerCallbackThunkExportName)); - subscribeToJsPromiseCallbacks(callbacks, jsContextfulPromise); - registerPromise(context.as(), std::move(callbacks)); + ThunkPool::get()->allocateThunk([=](std::unique_ptr allocation) { + allocation->bindToPromise(std::move(target), callbacks); + registerPromise(std::move(allocation), std::move(callbacks)); + }); } } // namespace @@ -509,7 +610,6 @@ std::string EventCallback::contextPropertyName(const std::string &eventName) EMSCRIPTEN_BINDINGS(qtStdwebCalback) { emscripten::function("qtStdWebEventCallbackActivate", &EventCallback::activate); - emscripten::function(WebPromiseManager::webPromiseManagerCallbackThunkExportName, &WebPromiseManager::callbackThunk); } namespace Promise { diff --git a/src/corelib/platform/wasm/qtcontextfulpromise_injection.js b/src/corelib/platform/wasm/qtcontextfulpromise_injection.js deleted file mode 100644 index ce5623171c6..00000000000 --- a/src/corelib/platform/wasm/qtcontextfulpromise_injection.js +++ /dev/null @@ -1,32 +0,0 @@ -`Copyright (C) 2022 The Qt Company Ltd. -Copyright (C) 2016 Intel Corporation. -SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0`; - -if (window.qtContextfulPromiseSupport) { - ++window.qtContextfulPromiseSupport.refs; -} else { - window.qtContextfulPromiseSupport = { - refs: 1, - removeRef: () => { - --window.qtContextfulPromiseSupport.refs, 0 === window.qtContextfulPromiseSupport.refs && delete window.qtContextfulPromiseSupport; - }, - makePromise: (a, b, c) => new window.qtContextfulPromiseSupport.ContextfulPromise(a, b, c), - }; - - window.qtContextfulPromiseSupport.ContextfulPromise = class { - constructor(a, b, c) { - (this.wrappedPromise = a), (this.context = b), (this.callbackThunk = c); - } - then() { - return (this.wrappedPromise = this.wrappedPromise.then((a) => { this.callbackThunk("then", this.context, a); })), this; - } - catch() { - return (this.wrappedPromise = this.wrappedPromise.catch((a) => { this.callbackThunk("catch", this.context, a); })), this; - } - finally() { - return (this.wrappedPromise = this.wrappedPromise.finally(() => this.callbackThunk("finally", this.context, undefined))), this; - } - }; -} - -document.querySelector("[qtinjection=contextfulpromise]")?.remove(); diff --git a/tests/manual/wasm/qstdweb/promise_main.cpp b/tests/manual/wasm/qstdweb/promise_main.cpp index 456fb7eb329..351f06c91d4 100644 --- a/tests/manual/wasm/qstdweb/promise_main.cpp +++ b/tests/manual/wasm/qstdweb/promise_main.cpp @@ -17,14 +17,15 @@ class WasmPromiseTest : public QObject Q_OBJECT public: - WasmPromiseTest() : m_window(val::global("window")), m_testSupport(val::object()) { - m_window.set("testSupport", m_testSupport); - } + WasmPromiseTest() : m_window(val::global("window")), m_testSupport(val::object()) {} - ~WasmPromiseTest() noexcept {} + ~WasmPromiseTest() noexcept = default; private: void init() { + m_testSupport = val::object(); + m_window.set("testSupport", m_testSupport); + EM_ASM({ testSupport.resolve = {}; testSupport.reject = {}; @@ -108,52 +109,32 @@ void WasmPromiseTest::multipleResolve() { init(); - auto onThen = std::make_shared(3, []() { + static constexpr int promiseCount = 1000; + + auto onThen = std::make_shared(promiseCount, []() { QWASMSUCCESS(); }); - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [=](val result) { - QWASMVERIFY(result.isString()); - QWASMCOMPARE("Data 1", result.as()); + for (int i = 0; i < promiseCount; ++i) { + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [=](val result) { + QWASMVERIFY(result.isString()); + QWASMCOMPARE(QString::number(i).toStdString(), result.as()); - (*onThen)(); - }, - .catchFunc = [](val error) { - Q_UNUSED(error); - QWASMFAIL("Unexpected catch"); - } - }, std::string("1")); - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [=](val result) { - QWASMVERIFY(result.isString()); - QWASMCOMPARE("Data 2", result.as()); - - (*onThen)(); - }, - .catchFunc = [](val error) { - Q_UNUSED(error); - QWASMFAIL("Unexpected catch"); - } - }, std::string("2")); - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [=](val result) { - QWASMVERIFY(result.isString()); - QWASMCOMPARE("Data 3", result.as()); - - (*onThen)(); - }, - .catchFunc = [](val error) { - Q_UNUSED(error); - QWASMFAIL("Unexpected catch"); - } - }, std::string("3")); + (*onThen)(); + }, + .catchFunc = [](val error) { + Q_UNUSED(error); + QWASMFAIL("Unexpected catch"); + } + }, (QStringLiteral("test") + QString::number(i)).toStdString()); + } EM_ASM({ - testSupport.resolve["3"]("Data 3"); - testSupport.resolve["1"]("Data 1"); - testSupport.resolve["2"]("Data 2"); - }); + for (let i = $0 - 1; i >= 0; --i) { + testSupport.resolve['test' + i](`${i}`); + } + }, promiseCount); } void WasmPromiseTest::simpleReject() @@ -179,53 +160,32 @@ void WasmPromiseTest::simpleReject() void WasmPromiseTest::multipleReject() { - init(); - auto onThen = std::make_shared(3, []() { + static constexpr int promiseCount = 1000; + + auto onCatch = std::make_shared(promiseCount, []() { QWASMSUCCESS(); }); - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - Q_UNUSED(result); - QWASMFAIL("Unexpected then"); - }, - .catchFunc = [=](val error) { - QWASMVERIFY(error.isString()); - QWASMCOMPARE("Error 1", error.as()); + for (int i = 0; i < promiseCount; ++i) { + qstdweb::Promise::make(m_testSupport, "makeTestPromise", { + .thenFunc = [=](val result) { + QWASMVERIFY(result.isString()); + QWASMCOMPARE(QString::number(i).toStdString(), result.as()); - (*onThen)(); - } - }, std::string("1")); - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - Q_UNUSED(result); - QWASMFAIL("Unexpected then"); - }, - .catchFunc = [=](val error) { - QWASMVERIFY(error.isString()); - QWASMCOMPARE("Error 2", error.as()); - - (*onThen)(); - } - }, std::string("2")); - qstdweb::Promise::make(m_testSupport, "makeTestPromise", { - .thenFunc = [](val result) { - Q_UNUSED(result); - QWASMFAIL("Unexpected then"); - }, - .catchFunc = [=](val error) { - QWASMVERIFY(error.isString()); - QWASMCOMPARE("Error 3", error.as()); - - (*onThen)(); - } - }, std::string("3")); + (*onCatch)(); + }, + .catchFunc = [](val error) { + Q_UNUSED(error); + QWASMFAIL("Unexpected catch"); + } + }, (QStringLiteral("test") + QString::number(i)).toStdString()); + } EM_ASM({ - testSupport.reject["3"]("Error 3"); - testSupport.reject["1"]("Error 1"); - testSupport.reject["2"]("Error 2"); - }); + for (let i = $0 - 1; i >= 0; --i) { + testSupport.resolve['test' + i](`${i}`); + } + }, promiseCount); } void WasmPromiseTest::throwInThen() @@ -241,8 +201,7 @@ void WasmPromiseTest::throwInThen() }, .catchFunc = [](val error) { QWASMCOMPARE("Expected error", error.as()); - //QWASMSUCCESS(); - QWASMFAIL("Other nasty problem"); + QWASMSUCCESS(); } }, std::string("throwInThen")); @@ -386,39 +345,42 @@ void WasmPromiseTest::all() { init(); - val promise1 = m_testSupport.call("makeTestPromise", val("promise1")); - val promise2 = m_testSupport.call("makeTestPromise", val("promise2")); - val promise3 = m_testSupport.call("makeTestPromise", val("promise3")); - + static constexpr int promiseCount = 1000; auto thenCalledOnce = std::shared_ptr(); *thenCalledOnce = true; - qstdweb::Promise::all({promise1, promise2, promise3}, { - .thenFunc = [thenCalledOnce](val result) { + std::vector promises; + promises.reserve(promiseCount); + + for (int i = 0; i < promiseCount; ++i) { + promises.push_back(m_testSupport.call("makeTestPromise", val(("all" + QString::number(i)).toStdString()))); + } + + qstdweb::Promise::all(std::move(promises), { + .thenFunc = [=](val result) { QWASMVERIFY(*thenCalledOnce); *thenCalledOnce = false; QWASMVERIFY(result.isArray()); - QWASMCOMPARE(3, result["length"].as()); - QWASMCOMPARE("Data 1", result[0].as()); - QWASMCOMPARE("Data 2", result[1].as()); - QWASMCOMPARE("Data 3", result[2].as()); + QWASMCOMPARE(promiseCount, result["length"].as()); + for (int i = 0; i < promiseCount; ++i) { + QWASMCOMPARE(QStringLiteral("Data %1").arg(i).toStdString(), result[i].as()); + } QWASMSUCCESS(); }, - .catchFunc = [](val result) { - Q_UNUSED(result); - EM_ASM({ - throw new Error("Unexpected error"); - }); + .catchFunc = [](val error) { + Q_UNUSED(error); + QWASMFAIL("Unexpected catch"); } }); EM_ASM({ - testSupport.resolve["promise3"]("Data 3"); - testSupport.resolve["promise1"]("Data 1"); - testSupport.resolve["promise2"]("Data 2"); - }); + console.log('Resolving'); + for (let i = $0 - 1; i >= 0; --i) { + testSupport.resolve['all' + i](`Data ${i}`); + } + }, promiseCount); } void WasmPromiseTest::allWithThrow() @@ -503,8 +465,7 @@ void WasmPromiseTest::allWithFinallyAndThrow() .finallyFunc = [finallyCalledOnce]() { QWASMVERIFY(*finallyCalledOnce); *finallyCalledOnce = false; - // QWASMSUCCESS(); - QWASMFAIL("Some nasty problem"); + QWASMSUCCESS(); } });