diff options
author | Mikolaj Boc <mikolaj.boc@qt.io> | 2022-06-21 17:50:04 +0200 |
---|---|---|
committer | Mikolaj Boc <mikolaj.boc@qt.io> | 2022-06-30 01:20:28 +0200 |
commit | d0eba2449adcee6f1daa33426f9c9b8ebf7c28f5 (patch) | |
tree | c651a3f7759a0a46f3f792a79697d91e374994f7 /src | |
parent | 97665c9615ff399e9a074b94926ab06e0c9619e5 (diff) |
Create a promise wrapper for C++ and port existing uses
Currently, to use a promise from C++ we either have to use an ASM block
(which does not work well with dynamic linking) or declare exports in
the EMSCRIPTEN_BINDINGS block, which is cumbersome and cannot be chained.
This solution makes it easy to use js promises by introducing the
WebPromiseManager which dispatches callbacks to appropriate callers when
available.
This is a preliminary patch for FileSystem support, which will heavily
use async APIs.
Task-number: QTBUG-99611
Change-Id: I368a8f173027eaa883a9ca18d0ea6a3e99b86071
Reviewed-by: Morten Johan Sørvig <morten.sorvig@qt.io>
Diffstat (limited to 'src')
-rw-r--r-- | src/corelib/CMakeLists.txt | 15 | ||||
-rw-r--r-- | src/corelib/platform/wasm/qstdweb.cpp | 242 | ||||
-rw-r--r-- | src/corelib/platform/wasm/qstdweb_p.h | 32 | ||||
-rw-r--r-- | src/corelib/platform/wasm/qtcontextfulpromise_injection.js | 32 | ||||
-rw-r--r-- | src/plugins/platforms/wasm/qwasmclipboard.cpp | 109 |
5 files changed, 331 insertions, 99 deletions
diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index f89aaae573..5394ee642d 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -1359,3 +1359,18 @@ 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 a915c031fe..d13c374dc4 100644 --- a/src/corelib/platform/wasm/qstdweb.cpp +++ b/src/corelib/platform/wasm/qstdweb.cpp @@ -3,7 +3,10 @@ #include "qstdweb_p.h" +#include <QtCore/qcoreapplication.h> +#include <QtCore/qfile.h> #include <emscripten/bind.h> +#include <emscripten/emscripten.h> #include <cstdint> #include <iostream> @@ -11,7 +14,187 @@ QT_BEGIN_NAMESPACE namespace qstdweb { +const char makeContextfulPromiseFunctionName[] = "makePromise"; + typedef double uint53_t; // see Number.MAX_SAFE_INTEGER +namespace { +enum class CallbackType { + Then, + Catch, + Finally, +}; + +void validateCallbacks(const PromiseCallbacks& callbacks) { + Q_ASSERT(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc); +} + +void injectScript(const std::string& source, const std::string& injectionName) +{ + using namespace emscripten; + + auto script = val::global("document").call<val>("createElement", val("script")); + auto head = val::global("document").call<val>("getElementsByTagName", val("head")); + + script.call<void>("setAttribute", val("qtinjection"), val(injectionName)); + script.set("innerText", val(source)); + + head[0].call<void>("appendChild", std::move(script)); +} + +using PromiseContext = int; + +class WebPromiseManager +{ +public: + static const char contextfulPromiseSupportObjectName[]; + + static const char webPromiseManagerCallbackThunkExportName[]; + + 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(); + + static void callbackThunk(emscripten::val callbackType, emscripten::val context, emscripten::val result); + +private: + static std::optional<CallbackType> parseCallbackType(emscripten::val callbackType); + + void subscribeToJsPromiseCallbacks(const PromiseCallbacks& callbacks, emscripten::val jsContextfulPromise); + void callback(CallbackType type, emscripten::val context, emscripten::val result); + + void registerPromise(PromiseContext context, PromiseCallbacks promise); + void unregisterPromise(PromiseContext context); + + QHash<PromiseContext, PromiseCallbacks> m_promiseRegistry; + int m_nextContextId = 0; +}; + +static void qStdWebCleanup() +{ + auto window = emscripten::val::global("window"); + auto contextfulPromiseSupport = window[WebPromiseManager::contextfulPromiseSupportObjectName]; + if (contextfulPromiseSupport.isUndefined()) + return; + + contextfulPromiseSupport.call<void>("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); +} + +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::callbackThunk(emscripten::val callbackType, + emscripten::val context, + 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<val>("then"); + if (callbacks.catchFunc) + jsContextfulPromiseObject = jsContextfulPromiseObject.call<val>("catch"); + if (callbacks.finallyFunc) + jsContextfulPromiseObject = jsContextfulPromiseObject.call<val>("finally"); +} + +void WebPromiseManager::callback(CallbackType type, emscripten::val context, emscripten::val result) +{ + auto found = m_promiseRegistry.find(context.as<PromiseContext>()); + if (found == m_promiseRegistry.end()) { + return; + } + + bool expectingOtherCallbacks; + switch (type) { + case CallbackType::Then: + found->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; + break; + case CallbackType::Catch: + found->catchFunc(result); + expectingOtherCallbacks = !!found->finallyFunc; + break; + case CallbackType::Finally: + found->finallyFunc(); + expectingOtherCallbacks = false; + break; + } + + if (!expectingOtherCallbacks) + unregisterPromise(context.as<int>()); +} + +void WebPromiseManager::registerPromise(PromiseContext context, PromiseCallbacks callbacks) +{ + m_promiseRegistry.emplace(context, std::move(callbacks)); +} + +void WebPromiseManager::unregisterPromise(PromiseContext context) +{ + m_promiseRegistry.remove(context); +} + +void WebPromiseManager::adoptPromise(emscripten::val target, PromiseCallbacks callbacks) { + emscripten::val context(m_nextContextId++); + + auto jsContextfulPromise = emscripten::val::global("window") + [contextfulPromiseSupportObjectName].call<emscripten::val>( + makeContextfulPromiseFunctionName, target, context, + emscripten::val::module_property(webPromiseManagerCallbackThunkExportName)); + subscribeToJsPromiseCallbacks(callbacks, jsContextfulPromise); + registerPromise(context.as<int>(), std::move(callbacks)); +} +} // namespace ArrayBuffer::ArrayBuffer(uint32_t size) { @@ -167,6 +350,10 @@ File FileList::operator[](int index) const return item(index); } +emscripten::val FileList::val() { + return m_fileList; +} + ArrayBuffer FileReader::result() const { return ArrayBuffer(m_fileReader["result"]); @@ -322,6 +509,61 @@ std::string EventCallback::contextPropertyName(const std::string &eventName) EMSCRIPTEN_BINDINGS(qtStdwebCalback) { emscripten::function("qtStdWebEventCallbackActivate", &EventCallback::activate); + emscripten::function(WebPromiseManager::webPromiseManagerCallbackThunkExportName, &WebPromiseManager::callbackThunk); +} + +namespace Promise { + void adoptPromise(emscripten::val promiseObject, PromiseCallbacks callbacks) { + validateCallbacks(callbacks); + + WebPromiseManager::get()->adoptPromise( + std::move(promiseObject), std::move(callbacks)); + } + + void all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks) { + struct State { + std::map<int, emscripten::val> results; + int remainingThenCallbacks; + int remainingFinallyCallbacks; + }; + + validateCallbacks(callbacks); + + auto state = std::make_shared<State>(); + state->remainingThenCallbacks = state->remainingFinallyCallbacks = promises.size(); + + 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(); + } + }; + + adoptPromise(std::move(promises.at(i)), std::move(individualPromiseCallback)); + } + } } } // namespace qstdweb diff --git a/src/corelib/platform/wasm/qstdweb_p.h b/src/corelib/platform/wasm/qstdweb_p.h index dc8f643507..b4b8948b3a 100644 --- a/src/corelib/platform/wasm/qstdweb_p.h +++ b/src/corelib/platform/wasm/qstdweb_p.h @@ -19,12 +19,14 @@ #include <emscripten/val.h> #include <cstdint> #include <functional> +#include "initializer_list" #include <QtCore/qglobal.h> - +#include "QtCore/qhash.h" QT_BEGIN_NAMESPACE namespace qstdweb { + extern const char makeContextfulPromiseFunctionName[]; // DOM API in C++, implemented using emscripten val.h and bind.h. // This is private API and can be extended and changed as needed. @@ -153,6 +155,34 @@ namespace qstdweb { std::string m_eventName; std::function<void(emscripten::val)> m_fn; }; + + struct PromiseCallbacks + { + std::function<void(emscripten::val)> thenFunc; + std::function<void(emscripten::val)> catchFunc; + std::function<void()> finallyFunc; + }; + + namespace Promise { + void adoptPromise(emscripten::val promise, PromiseCallbacks callbacks); + + template<typename... Args> + void make(emscripten::val target, + QString methodName, + PromiseCallbacks callbacks, + Args... args) + { + emscripten::val promiseObject = target.call<emscripten::val>( + methodName.toStdString().c_str(), std::forward<Args>(args)...); + if (promiseObject.isUndefined() || promiseObject["constructor"]["name"].as<std::string>() != "Promise") { + qFatal("This function did not return a promise"); + } + + adoptPromise(std::move(promiseObject), std::move(callbacks)); + } + + void all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks); + }; } QT_END_NAMESPACE diff --git a/src/corelib/platform/wasm/qtcontextfulpromise_injection.js b/src/corelib/platform/wasm/qtcontextfulpromise_injection.js new file mode 100644 index 0000000000..ce5623171c --- /dev/null +++ b/src/corelib/platform/wasm/qtcontextfulpromise_injection.js @@ -0,0 +1,32 @@ +`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/src/plugins/platforms/wasm/qwasmclipboard.cpp b/src/plugins/platforms/wasm/qwasmclipboard.cpp index 88043ba092..99f3e61155 100644 --- a/src/plugins/platforms/wasm/qwasmclipboard.cpp +++ b/src/plugins/platforms/wasm/qwasmclipboard.cpp @@ -19,83 +19,6 @@ QT_BEGIN_NAMESPACE using namespace emscripten; -static void pasteClipboardData(emscripten::val format, emscripten::val dataPtr) -{ - QString formatString = QWasmString::toQString(format); - QByteArray dataArray = QByteArray::fromStdString(dataPtr.as<std::string>()); - - QMimeData *mMimeData = new QMimeData; - mMimeData->setData(formatString, dataArray); - - QWasmClipboard::qWasmClipboardPaste(mMimeData); -// QWasmIntegration::get()->getWasmClipboard()->isPaste = false; -} - -static void qClipboardPasteResolve(emscripten::val blob) -{ - // read Blob here - - auto fileReader = std::make_shared<qstdweb::FileReader>(); - auto _blob = qstdweb::Blob(blob); - QString formatString = QString::fromStdString(_blob.type()); - - fileReader->readAsArrayBuffer(_blob); - char *chunkBuffer = nullptr; - qstdweb::ArrayBuffer result = fileReader->result(); - qstdweb::Uint8Array(result).copyTo(chunkBuffer); - QMimeData *mMimeData = new QMimeData; - mMimeData->setData(formatString, chunkBuffer); - QWasmClipboard::qWasmClipboardPaste(mMimeData); -} - -static void qClipboardPromiseResolve(emscripten::val clipboardItems) -{ - int itemsCount = clipboardItems["length"].as<int>(); - - for (int i = 0; i < itemsCount; i++) { - int typesCount = clipboardItems[i]["types"]["length"].as<int>(); // ClipboardItem - - std::string mimeFormat = clipboardItems[i]["types"][0].as<std::string>(); - - if (mimeFormat.find(std::string("text")) != std::string::npos) { - // simple val object, no further processing - - val navigator = val::global("navigator"); - val textPromise = navigator["clipboard"].call<val>("readText"); - val readTextResolve = val::global("Module")["qtClipboardTextPromiseResolve"]; - textPromise.call<val>("then", readTextResolve); - - } else { - // binary types require additional processing - for (int j = 0; j < typesCount; j++) { - val pasteResolve = emscripten::val::module_property("qtClipboardPasteResolve"); - val pasteException = emscripten::val::module_property("qtClipboardPromiseException"); - - // get the blob - clipboardItems[i] - .call<val>("getType", clipboardItems[i]["types"][j]) - .call<val>("then", pasteResolve) - .call<val>("catch", pasteException); - } - } - } -} - -static void qClipboardCopyPromiseResolve(emscripten::val something) -{ - Q_UNUSED(something) - qWarning() << "copy succeeeded"; -} - - -static emscripten::val qClipboardPromiseException(emscripten::val something) -{ - qWarning() << "clipboard error" - << QString::fromStdString(something["name"].as<std::string>()) - << QString::fromStdString(something["message"].as<std::string>()); - return something; -} - static void commonCopyEvent(val event) { QMimeData *_mimes = QWasmIntegration::get()->getWasmClipboard()->mimeData(QClipboard::Clipboard); @@ -215,24 +138,10 @@ static void qClipboardPasteTo(val dataTransfer) QWasmIntegration::get()->getWasmClipboard()->m_isListener = false; } -static void qClipboardTextPromiseResolve(emscripten::val clipdata) -{ - pasteClipboardData(emscripten::val("text/plain"), clipdata); -} - EMSCRIPTEN_BINDINGS(qtClipboardModule) { - function("qtPasteClipboardData", &pasteClipboardData); - - function("qtClipboardTextPromiseResolve", &qClipboardTextPromiseResolve); - function("qtClipboardPromiseResolve", &qClipboardPromiseResolve); - - function("qtClipboardCopyPromiseResolve", &qClipboardCopyPromiseResolve); - function("qtClipboardPromiseException", &qClipboardPromiseException); - function("qtClipboardCutTo", &qClipboardCutTo); function("qtClipboardCopyTo", &qClipboardCopyTo); function("qtClipboardPasteTo", &qClipboardPasteTo); - function("qtClipboardPasteResolve", &qClipboardPasteResolve); } QWasmClipboard::QWasmClipboard() : @@ -419,14 +328,18 @@ void QWasmClipboard::writeToClipboardApi() // break; } - val copyResolve = emscripten::val::module_property("qtClipboardCopyPromiseResolve"); - val copyException = emscripten::val::module_property("qtClipboardPromiseException"); - val navigator = val::global("navigator"); - navigator["clipboard"] - .call<val>("write", clipboardWriteArray) - .call<val>("then", copyResolve) - .call<val>("catch", copyException); + + qstdweb::Promise::make( + navigator["clipboard"], "write", + { + .catchFunc = [](emscripten::val error) { + qWarning() << "clipboard error" + << QString::fromStdString(error["name"].as<std::string>()) + << QString::fromStdString(error["message"].as<std::string>()); + } + }, + clipboardWriteArray); } void QWasmClipboard::writeToClipboard(const QMimeData *data) |