diff options
Diffstat (limited to 'src/corelib/platform/wasm/qstdweb.cpp')
-rw-r--r-- | src/corelib/platform/wasm/qstdweb.cpp | 437 |
1 files changed, 351 insertions, 86 deletions
diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp index 486bfaf485..75e76a6806 100644 --- a/src/corelib/platform/wasm/qstdweb.cpp +++ b/src/corelib/platform/wasm/qstdweb.cpp @@ -5,8 +5,13 @@ #include <QtCore/qcoreapplication.h> #include <QtCore/qfile.h> +#include <QtCore/qmimedata.h> + #include <emscripten/bind.h> #include <emscripten/emscripten.h> +#include <emscripten/html5.h> +#include <emscripten/threading.h> + #include <cstdint> #include <iostream> @@ -14,10 +19,74 @@ QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + namespace qstdweb { +static void usePotentialyUnusedSymbols() +{ + // Using this adds a reference on JSEvents and specialHTMLTargets which are always exported. + // This hack is needed as it is currently impossible to specify a dollar sign in + // target_link_options. The following is impossible: + // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=$JSEvents + // TODO(mikolajboc): QTBUG-108444, review this when cmake gets fixed. + // Volatile is to make this unoptimizable, so that the function is referenced, but is not + // called at runtime. + volatile bool doIt = false; + if (doIt) + emscripten_set_wheel_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, 0, NULL); +} + +Q_CONSTRUCTOR_FUNCTION(usePotentialyUnusedSymbols) typedef double uint53_t; // see Number.MAX_SAFE_INTEGER namespace { +// Reads file in chunks in order to avoid holding two copies in memory at the same time +struct ChunkedFileReader +{ +public: + static void read(File file, char *buffer, uint32_t offset, uint32_t end, + std::function<void()> onCompleted) + { + (new ChunkedFileReader(end, std::move(onCompleted), std::move(file))) + ->readNextChunk(offset, buffer); + } + +private: + ChunkedFileReader(uint32_t end, std::function<void()> onCompleted, File file) + : end(end), onCompleted(std::move(onCompleted)), file(std::move(file)) + { + } + + void readNextChunk(uint32_t chunkBegin, char *chunkBuffer) + { + // Copy current chunk from JS memory to Wasm memory + qstdweb::ArrayBuffer result = fileReader.result(); + qstdweb::Uint8Array(result).copyTo(chunkBuffer); + + // Read next chunk if not at buffer end + const uint32_t nextChunkBegin = std::min(chunkBegin + result.byteLength(), end); + if (nextChunkBegin == end) { + onCompleted(); + delete this; + return; + } + char *nextChunkBuffer = chunkBuffer + result.byteLength(); + fileReader.onLoad([this, nextChunkBegin, nextChunkBuffer](emscripten::val) { + readNextChunk(nextChunkBegin, nextChunkBuffer); + }); + const uint32_t nextChunkEnd = std::min(nextChunkBegin + chunkSize, end); + qstdweb::Blob blob = file.slice(nextChunkBegin, nextChunkEnd); + fileReader.readAsArrayBuffer(blob); + } + + static constexpr uint32_t chunkSize = 256 * 1024; + + qstdweb::FileReader fileReader; + uint32_t end; + std::function<void()> onCompleted; + File file; +}; + enum class CallbackType { Then, Catch, @@ -69,11 +138,12 @@ public: "catch", emscripten::val::module_property(thunkName(CallbackType::Catch, id()).data())); } - if (callbacks.finallyFunc) { - target = target.call<val>( - "finally", - emscripten::val::module_property(thunkName(CallbackType::Finally, 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: @@ -254,25 +324,21 @@ void WebPromiseManager::promiseThunkCallback(int context, CallbackType type, ems auto* promiseState = &m_promiseRegistry[context]; auto* callbacks = &promiseState->callbacks; - bool expectingOtherCallbacks; switch (type) { case CallbackType::Then: callbacks->thenFunc(result); - // At this point, if there is no finally function, we are sure that the Catch callback won't be issued. - expectingOtherCallbacks = !!callbacks->finallyFunc; break; case CallbackType::Catch: callbacks->catchFunc(result); - expectingOtherCallbacks = !!callbacks->finallyFunc; break; case CallbackType::Finally: - callbacks->finallyFunc(); - expectingOtherCallbacks = false; + // Final callback may be empty, used solely for promise unregistration + if (callbacks->finallyFunc) { + callbacks->finallyFunc(); + } + unregisterPromise(context); break; - } - - if (!expectingOtherCallbacks) - unregisterPromise(context); + } } void WebPromiseManager::registerPromise( @@ -295,7 +361,19 @@ void WebPromiseManager::adoptPromise(emscripten::val target, PromiseCallbacks ca registerPromise(std::move(allocation), std::move(callbacks)); }); } -} // namespace +#if defined(QT_STATIC) + +EM_JS(bool, jsHaveAsyncify, (), { return typeof Asyncify !== "undefined"; }); +EM_JS(bool, jsHaveJspi, (), + { return typeof Asyncify !== "undefined" && !!Asyncify.makeAsyncFunction && !!WebAssembly.Function; }); + +#else + +bool jsHaveAsyncify() { return false; } +bool jsHaveJspi() { return false; } + +#endif +} // namespace ArrayBuffer::ArrayBuffer(uint32_t size) { @@ -316,7 +394,12 @@ uint32_t ArrayBuffer::byteLength() const return m_arrayBuffer["byteLength"].as<uint32_t>(); } -emscripten::val ArrayBuffer::val() +ArrayBuffer ArrayBuffer::slice(uint32_t begin, uint32_t end) const +{ + return ArrayBuffer(m_arrayBuffer.call<emscripten::val>("slice", begin, end)); +} + +emscripten::val ArrayBuffer::val() const { return m_arrayBuffer; } @@ -327,24 +410,55 @@ Blob::Blob(const emscripten::val &blob) } +Blob Blob::fromArrayBuffer(const ArrayBuffer &arrayBuffer) +{ + auto array = emscripten::val::array(); + array.call<void>("push", arrayBuffer.val()); + return Blob(emscripten::val::global("Blob").new_(array)); +} + uint32_t Blob::size() const { return m_blob["size"].as<uint32_t>(); } -// Copies content from the given buffer into a Blob object -Blob Blob::copyFrom(const char *buffer, uint32_t size) +Blob Blob::copyFrom(const char *buffer, uint32_t size, std::string mimeType) { Uint8Array contentCopy = Uint8Array::copyFrom(buffer, size); emscripten::val contentArray = emscripten::val::array(); contentArray.call<void>("push", contentCopy.val()); emscripten::val type = emscripten::val::object(); - type.set("type","application/octet-stream"); + type.set("type", std::move(mimeType)); return Blob(emscripten::val::global("Blob").new_(contentArray, type)); } -emscripten::val Blob::val() +// Copies content from the given buffer into a Blob object +Blob Blob::copyFrom(const char *buffer, uint32_t size) +{ + return copyFrom(buffer, size, "application/octet-stream"); +} + +Blob Blob::slice(uint32_t begin, uint32_t end) const +{ + return Blob(m_blob.call<emscripten::val>("slice", begin, end)); +} + +ArrayBuffer Blob::arrayBuffer_sync() const +{ + QEventLoop loop; + emscripten::val buffer; + qstdweb::Promise::make(m_blob, QStringLiteral("arrayBuffer"), { + .thenFunc = [&loop, &buffer](emscripten::val arrayBuffer) { + buffer = arrayBuffer; + loop.quit(); + } + }); + loop.exec(); + return ArrayBuffer(buffer); +} + +emscripten::val Blob::val() const { return m_blob; } @@ -355,6 +469,17 @@ File::File(const emscripten::val &file) } +File::~File() = default; + +File::File(const File &other) = default; + +File::File(File &&other) = default; + +File &File::operator=(const File &other) = default; + +File &File::operator=(File &&other) = default; + + Blob File::slice(uint64_t begin, uint64_t end) const { return Blob(m_file.call<emscripten::val>("slice", uint53_t(begin), uint53_t(end))); @@ -377,47 +502,17 @@ std::string Blob::type() const // Streams partial file content into the given buffer asynchronously. The completed // callback is called on completion. -void File::stream(uint32_t offset, uint32_t length, char *buffer, const std::function<void ()> &completed) const +void File::stream(uint32_t offset, uint32_t length, char *buffer, + std::function<void()> completed) const { - // Read file in chunks in order to avoid holding two copies in memory at the same time - const uint32_t chunkSize = 256 * 1024; - const uint32_t end = offset + length; - // assert end < file.size - auto fileReader = std::make_shared<qstdweb::FileReader>(); - - // "this" is valid now, but may not be by the time the chunkCompleted callback - // below is made. Make a copy of the file handle. - const File fileHandle = *this; - auto chunkCompleted = std::make_shared<std::function<void (uint32_t, char *buffer)>>(); - *chunkCompleted = [=](uint32_t chunkBegin, char *chunkBuffer) mutable { - - // Copy current chunk from JS memory to Wasm memory - qstdweb::ArrayBuffer result = fileReader->result(); - qstdweb::Uint8Array(result).copyTo(chunkBuffer); - - // Read next chunk if not at buffer end - uint32_t nextChunkBegin = std::min(chunkBegin + result.byteLength(), end); - uint32_t nextChunkEnd = std::min(nextChunkBegin + chunkSize, end); - if (nextChunkBegin == end) { - completed(); - chunkCompleted.reset(); - return; - } - char *nextChunkBuffer = chunkBuffer + result.byteLength(); - fileReader->onLoad([=](emscripten::val) { (*chunkCompleted)(nextChunkBegin, nextChunkBuffer); }); - qstdweb::Blob blob = fileHandle.slice(nextChunkBegin, nextChunkEnd); - fileReader->readAsArrayBuffer(blob); - }; - - // Read first chunk. First iteration is a dummy iteration with no available data. - (*chunkCompleted)(offset, buffer); + ChunkedFileReader::read(*this, buffer, offset, offset + length, std::move(completed)); } // Streams file content into the given buffer asynchronously. The completed // callback is called on completion. -void File::stream(char *buffer, const std::function<void ()> &completed) const +void File::stream(char *buffer, std::function<void()> completed) const { - stream(0, size(), buffer, completed); + stream(0, size(), buffer, std::move(completed)); } std::string File::type() const @@ -425,11 +520,27 @@ std::string File::type() const return m_file["type"].as<std::string>(); } -emscripten::val File::val() +emscripten::val File::val() const { return m_file; } +FileUrlRegistration::FileUrlRegistration(File file) +{ + m_path = QString::fromStdString(emscripten::val::global("window")["URL"].call<std::string>( + "createObjectURL", file.file())); +} + +FileUrlRegistration::~FileUrlRegistration() +{ + emscripten::val::global("window")["URL"].call<void>("revokeObjectURL", + emscripten::val(m_path.toStdString())); +} + +FileUrlRegistration::FileUrlRegistration(FileUrlRegistration &&other) = default; + +FileUrlRegistration &FileUrlRegistration::operator=(FileUrlRegistration &&other) = default; + FileList::FileList(const emscripten::val &fileList) :m_fileList(fileList) { @@ -468,20 +579,23 @@ void FileReader::readAsArrayBuffer(const Blob &blob) const void FileReader::onLoad(const std::function<void(emscripten::val)> &onLoad) { - m_onLoad.reset(new EventCallback(m_fileReader, "load", onLoad)); + m_onLoad.reset(); + m_onLoad = std::make_unique<EventCallback>(m_fileReader, "load", onLoad); } void FileReader::onError(const std::function<void(emscripten::val)> &onError) { - m_onError.reset(new EventCallback(m_fileReader, "error", onError)); + m_onError.reset(); + m_onError = std::make_unique<EventCallback>(m_fileReader, "error", onError); } void FileReader::onAbort(const std::function<void(emscripten::val)> &onAbort) { - m_onAbort.reset(new EventCallback(m_fileReader, "abort", onAbort)); + m_onAbort.reset(); + m_onAbort = std::make_unique<EventCallback>(m_fileReader, "abort", onAbort); } -emscripten::val FileReader::val() +emscripten::val FileReader::val() const { return m_fileReader; } @@ -514,7 +628,7 @@ Uint8Array::Uint8Array(const ArrayBuffer &buffer, uint32_t offset, uint32_t leng // Constructs a Uint8Array which references an area on the heap. Uint8Array::Uint8Array(const char *buffer, uint32_t size) -:m_uint8Array(Uint8Array::constructor_().new_(Uint8Array::heap().buffer().m_arrayBuffer, uint32_t(buffer), size)) +:m_uint8Array(Uint8Array::constructor_().new_(Uint8Array::heap().buffer().m_arrayBuffer, uintptr_t(buffer), size)) { } @@ -541,12 +655,31 @@ void Uint8Array::set(const Uint8Array &source) m_uint8Array.call<void>("set", source.m_uint8Array); // copies source content } +Uint8Array Uint8Array::subarray(uint32_t begin, uint32_t end) +{ + // Note: using uint64_t here errors with "Cannot convert a BigInt value to a number" + // (see JS BigInt and Number types). Use uint32_t for now. + return Uint8Array(m_uint8Array.call<emscripten::val>("subarray", begin, end)); +} + // Copies the Uint8Array content to a destination on the heap void Uint8Array::copyTo(char *destination) const { Uint8Array(destination, length()).set(*this); } +// Copies the Uint8Array content to a destination QByteArray +QByteArray Uint8Array::copyToQByteArray() const +{ + if (length() > std::numeric_limits<qsizetype>::max()) + return QByteArray(); + + QByteArray destinationArray; + destinationArray.resize(length()); + copyTo(destinationArray.data()); + return destinationArray; +} + // Copies the Uint8Array content to a destination on the heap void Uint8Array::copy(char *destination, const Uint8Array &source) { @@ -561,7 +694,13 @@ Uint8Array Uint8Array::copyFrom(const char *buffer, uint32_t size) return contentCopy; } -emscripten::val Uint8Array::val() +// Copies content from a QByteArray to a new Uint8Array object +Uint8Array Uint8Array::copyFrom(const QByteArray &buffer) +{ + return copyFrom(buffer.constData(), buffer.size()); +} + +emscripten::val Uint8Array::val() const { return m_uint8Array; } @@ -576,41 +715,45 @@ emscripten::val Uint8Array::constructor_() return emscripten::val::global("Uint8Array"); } +class EventListener { +public: + EventListener(uintptr_t handler) + :m_handler(handler) + { + + } + + // Special function - addEventListender() allows adding an object with a + // handleEvent() function which eceives the event. + void handleEvent(emscripten::val event) { + auto handlerPtr = reinterpret_cast<std::function<void(emscripten::val)> *>(m_handler); + (*handlerPtr)(event); + } + + uintptr_t m_handler; +}; + // Registers a callback function for a named event on the given element. The event // name must be the name as returned by the Event.type property: e.g. "load", "error". EventCallback::~EventCallback() { - // Clean up if this instance's callback is still installed on the element - if (m_element[contextPropertyName(m_eventName).c_str()].as<intptr_t>() == intptr_t(this)) { - m_element.set(contextPropertyName(m_eventName).c_str(), emscripten::val::undefined()); - m_element.set((std::string("on") + m_eventName).c_str(), emscripten::val::undefined()); - } + m_element.call<void>("removeEventListener", m_eventName, m_eventListener); } -EventCallback::EventCallback(emscripten::val element, const std::string &name, const std::function<void(emscripten::val)> &fn) +EventCallback::EventCallback(emscripten::val element, const std::string &name, const std::function<void(emscripten::val)> &handler) :m_element(element) ,m_eventName(name) - ,m_fn(fn) + ,m_handler(std::make_unique<std::function<void(emscripten::val)>>(handler)) { - m_element.set(contextPropertyName(m_eventName).c_str(), emscripten::val(intptr_t(this))); - m_element.set((std::string("on") + m_eventName).c_str(), emscripten::val::module_property("qtStdWebEventCallbackActivate")); -} - -void EventCallback::activate(emscripten::val event) -{ - emscripten::val target = event["target"]; - std::string eventName = event["type"].as<std::string>(); - EventCallback *that = reinterpret_cast<EventCallback *>(target[contextPropertyName(eventName).c_str()].as<intptr_t>()); - that->m_fn(event); -} - -std::string EventCallback::contextPropertyName(const std::string &eventName) -{ - return std::string("data-qtEventCallbackContext") + eventName; + uintptr_t handlerUint = reinterpret_cast<uintptr_t>(m_handler.get()); // FIXME: pass pointer directly instead + m_eventListener = emscripten::val::module_property("QtEventListener").new_(handlerUint); + m_element.call<void>("addEventListener", m_eventName, m_eventListener); } EMSCRIPTEN_BINDINGS(qtStdwebCalback) { - emscripten::function("qtStdWebEventCallbackActivate", &EventCallback::activate); + emscripten::class_<EventListener>("QtEventListener") + .constructor<uintptr_t>() + .function("handleEvent", &EventListener::handleEvent); } namespace Promise { @@ -667,6 +810,128 @@ namespace Promise { } } +// Asyncify and thread blocking: Normally, it's not possible to block the main +// thread, except if asyncify is enabled. Secondary threads can always block. +// +// haveAsyncify(): returns true if the main thread can block on QEventLoop::exec(), +// if either asyncify 1 or 2 (JSPI) is available. +// +// haveJspi(): returns true if asyncify 2 (JSPI) is available. +// +// canBlockCallingThread(): returns true if the calling thread can block on +// QEventLoop::exec(), using either asyncify or as a seconarday thread. +bool haveJspi() +{ + static bool HaveJspi = jsHaveJspi(); + return HaveJspi; +} + +bool haveAsyncify() +{ + static bool HaveAsyncify = jsHaveAsyncify() || haveJspi(); + return HaveAsyncify; +} + +bool canBlockCallingThread() +{ + return haveAsyncify() || !emscripten_is_main_runtime_thread(); +} + +BlobIODevice::BlobIODevice(Blob blob) + : m_blob(blob) +{ + +} + +bool BlobIODevice::open(QIODevice::OpenMode mode) +{ + if (mode.testFlag(QIODevice::WriteOnly)) + return false; + return QIODevice::open(mode); +} + +bool BlobIODevice::isSequential() const +{ + return false; +} + +qint64 BlobIODevice::size() const +{ + return m_blob.size(); +} + +bool BlobIODevice::seek(qint64 pos) +{ + if (pos >= size()) + return false; + return QIODevice::seek(pos); +} + +qint64 BlobIODevice::readData(char *data, qint64 maxSize) +{ + uint64_t begin = QIODevice::pos(); + uint64_t end = std::min<uint64_t>(begin + maxSize, size()); + uint64_t size = end - begin; + if (size > 0) { + qstdweb::ArrayBuffer buffer = m_blob.slice(begin, end).arrayBuffer_sync(); + qstdweb::Uint8Array(buffer).copyTo(data); + } + return size; +} + +qint64 BlobIODevice::writeData(const char *, qint64) +{ + Q_UNREACHABLE(); +} + +Uint8ArrayIODevice::Uint8ArrayIODevice(Uint8Array array) + : m_array(array) +{ + +} + +bool Uint8ArrayIODevice::open(QIODevice::OpenMode mode) +{ + return QIODevice::open(mode); +} + +bool Uint8ArrayIODevice::isSequential() const +{ + return false; +} + +qint64 Uint8ArrayIODevice::size() const +{ + return m_array.length(); +} + +bool Uint8ArrayIODevice::seek(qint64 pos) +{ + if (pos >= size()) + return false; + return QIODevice::seek(pos); +} + +qint64 Uint8ArrayIODevice::readData(char *data, qint64 maxSize) +{ + uint64_t begin = QIODevice::pos(); + uint64_t end = std::min<uint64_t>(begin + maxSize, size()); + uint64_t size = end - begin; + if (size > 0) + m_array.subarray(begin, end).copyTo(data); + return size; +} + +qint64 Uint8ArrayIODevice::writeData(const char *data, qint64 maxSize) +{ + uint64_t begin = QIODevice::pos(); + uint64_t end = std::min<uint64_t>(begin + maxSize, size()); + uint64_t size = end - begin; + if (size > 0) + m_array.subarray(begin, end).set(Uint8Array(data, size)); + return size; +} + } // namespace qstdweb QT_END_NAMESPACE |