summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMikolaj Boc <mikolaj.boc@qt.io>2022-06-21 17:50:04 +0200
committerMikolaj Boc <mikolaj.boc@qt.io>2022-06-30 01:20:28 +0200
commitd0eba2449adcee6f1daa33426f9c9b8ebf7c28f5 (patch)
treec651a3f7759a0a46f3f792a79697d91e374994f7 /src
parent97665c9615ff399e9a074b94926ab06e0c9619e5 (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.txt15
-rw-r--r--src/corelib/platform/wasm/qstdweb.cpp242
-rw-r--r--src/corelib/platform/wasm/qstdweb_p.h32
-rw-r--r--src/corelib/platform/wasm/qtcontextfulpromise_injection.js32
-rw-r--r--src/plugins/platforms/wasm/qwasmclipboard.cpp109
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)