diff --git a/mkspecs/wasm-emscripten/qmake.conf b/mkspecs/wasm-emscripten/qmake.conf index b5192928797..364edd7f163 100644 --- a/mkspecs/wasm-emscripten/qmake.conf +++ b/mkspecs/wasm-emscripten/qmake.conf @@ -22,6 +22,9 @@ load(emcc_ver) # (with "wasm validation error: too many locals" type errors) if optimizations # are omitted. Enable optimizations also for debug builds. QMAKE_LFLAGS_DEBUG += -Os + + # Declare all async functions + QMAKE_LFLAGS += -s \'ASYNCIFY_IMPORTS=[\"qt_asyncify_suspend_js\", \"qt_asyncify_resume_js\", \"emscripten_sleep\"]\' } } diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 055b191baa3..9665600f943 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -1289,6 +1289,7 @@ endif() qt_internal_extend_target(Core CONDITION WASM SOURCES platform/wasm/qstdweb.cpp platform/wasm/qstdweb_p.h + kernel/qeventdispatcher_wasm.cpp kernel/qeventdispatcher_wasm_p.h ) # special case begin diff --git a/src/corelib/kernel/qeventdispatcher_wasm.cpp b/src/corelib/kernel/qeventdispatcher_wasm.cpp new file mode 100644 index 00000000000..14e384b98df --- /dev/null +++ b/src/corelib/kernel/qeventdispatcher_wasm.cpp @@ -0,0 +1,545 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qeventdispatcher_wasm_p.h" + +#include +#include + +#include "emscripten.h" +#include +#include + +QT_BEGIN_NAMESPACE + +// using namespace emscripten; +extern int qGlobalPostedEventsCount(); // from qapplication.cpp + +Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher"); +Q_LOGGING_CATEGORY(lcEventDispatcherTimers, "qt.eventdispatcher.timers"); + +#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY + +// Enable/disable JavaScript-side debugging +#if 0 + #define QT_ASYNCIFY_DEBUG(X) out(X) +#else + #define QT_ASYNCIFY_DEBUG(X) +#endif + +// Emscripten asyncify currently supports one level of suspend - +// recursion is not permitted. We track the suspend state here +// on order to fail (more) gracefully, but we can of course only +// track Qts own usage of asyncify. +static bool g_is_asyncify_suspended = false; + +EM_JS(void, qt_asyncify_suspend_js, (), { + QT_ASYNCIFY_DEBUG("qt_asyncify_suspend_js"); + let sleepFn = (wakeUp) = > + { + QT_ASYNCIFY_DEBUG("setting Module.qtAsyncifyWakeUp") + Module.qtAsyncifyWakeUp = wakeUp; // ### not "Module" any more + }; + return Asyncify.handleSleep(sleepFn); +}); + +EM_JS(void, qt_asyncify_resume_js, (), { + QT_ASYNCIFY_DEBUG("qt_asyncify_resume_js"); + let wakeUp = Module.qtAsyncifyWakeUp; + if (wakeUp == = undefined) { + QT_ASYNCIFY_DEBUG("qt_asyncify_resume_js no wakeup fn set - did not wake"); + return; + } + Module.qtAsyncifyWakeUp = undefined; + + // Delayed wakeup with zero-timer. Workaround/fix for + // https://github.com/emscripten-core/emscripten/issues/10515 + setTimeout(wakeUp); + QT_ASYNCIFY_DEBUG("qt_asyncify_resume_js done"); +}); + +// Suspends the main thread until qt_asyncify_resume() is called. Returns +// false immediately if Qt has already suspended the main thread (recursive +// suspend is not supported by Emscripten). Returns true (after resuming), +// if the thread was suspended. +bool qt_asyncify_suspend() +{ + if (g_is_asyncify_suspended) + return false; + g_is_asyncify_suspended = true; + qt_asyncify_suspend_js(); + return true; +} + +// Wakes any currently suspended main thread. Returns true if the main +// thread was suspended, in which case it will now be asynchonously woken. +bool qt_asyncify_resume() +{ + if (!g_is_asyncify_suspended) + return false; + g_is_asyncify_suspended = false; + qt_asyncify_resume_js(); + return true; +} + +// Yields control to the browser, so that it can process events. Must +// be called on the main thread. Returns false immediately if Qt has +// already suspended the main thread. Returns true after yielding. +bool qt_asyncify_yield() +{ + if (g_is_asyncify_suspended) + return false; + emscripten_sleep(0); + return true; +} + +#endif // QT_HAVE_EMSCRIPTEN_ASYNCIFY + +QEventDispatcherWasm *QEventDispatcherWasm::g_mainThreadEventDispatcher = nullptr; +#if QT_CONFIG(thread) +QVector QEventDispatcherWasm::g_secondaryThreadEventDispatchers; +std::mutex QEventDispatcherWasm::g_secondaryThreadEventDispatchersMutex; +#endif + +QEventDispatcherWasm::QEventDispatcherWasm() + : QAbstractEventDispatcher() +{ + // QEventDispatcherWasm operates in two main modes: + // - On the main thread: + // The event dispatcher can process native events but can't + // block and wait for new events, unless asyncify is used. + // - On a secondary thread: + // The event dispatcher can't process native events but can + // block and wait for new events. + // + // Which mode is determined by the calling thread: construct + // the event dispatcher object on the thread where it will live. + + qCDebug(lcEventDispatcher) << "Creating QEventDispatcherWasm instance" << this + << "is main thread" << emscripten_is_main_runtime_thread(); + + if (emscripten_is_main_runtime_thread()) { + // There can be only one main thread event dispatcher at a time; in + // addition the main instance is used by the secondary thread event + // dispatchers so we set a global pointer to it. + Q_ASSERT(g_mainThreadEventDispatcher == nullptr); + g_mainThreadEventDispatcher = this; + } else { +#if QT_CONFIG(thread) + std::lock_guard lock(g_secondaryThreadEventDispatchersMutex); + g_secondaryThreadEventDispatchers.append(this); +#endif + } +} + +QEventDispatcherWasm::~QEventDispatcherWasm() +{ + qCDebug(lcEventDispatcher) << "Detroying QEventDispatcherWasm instance" << this; + + delete m_timerInfo; + +#if QT_CONFIG(thread) + if (isSecondaryThreadEventDispatcher()) { + std::lock_guard lock(g_secondaryThreadEventDispatchersMutex); + g_secondaryThreadEventDispatchers.remove(g_secondaryThreadEventDispatchers.indexOf(this)); + } else +#endif + { + if (m_timerId > 0) + emscripten_clear_timeout(m_timerId); + g_mainThreadEventDispatcher = nullptr; + } +} + +bool QEventDispatcherWasm::isMainThreadEventDispatcher() +{ + return this == g_mainThreadEventDispatcher; +} + +bool QEventDispatcherWasm::isSecondaryThreadEventDispatcher() +{ + return this != g_mainThreadEventDispatcher; +} + +bool QEventDispatcherWasm::processEvents(QEventLoop::ProcessEventsFlags flags) +{ + emit awake(); + + bool hasPendingEvents = qGlobalPostedEventsCount() > 0; + + qCDebug(lcEventDispatcher) << "QEventDispatcherWasm::processEvents flags" << flags + << "pending events" << hasPendingEvents; + + if (!(flags & QEventLoop::ExcludeUserInputEvents)) + pollForNativeEvents(); + + hasPendingEvents = qGlobalPostedEventsCount() > 0; + + if (!hasPendingEvents && (flags & QEventLoop::WaitForMoreEvents)) + waitForForEvents(); + + if (m_interrupted) { + m_interrupted = false; + return false; + } + + if (m_processTimers) { + m_processTimers = false; + processTimers(); + } + + hasPendingEvents = qGlobalPostedEventsCount() > 0; + QCoreApplication::sendPostedEvents(); + return hasPendingEvents; +} + +void QEventDispatcherWasm::registerSocketNotifier(QSocketNotifier *notifier) +{ + Q_UNUSED(notifier); + qWarning("QEventDispatcherWasm::registerSocketNotifier: socket notifiers are not supported"); +} + +void QEventDispatcherWasm::unregisterSocketNotifier(QSocketNotifier *notifier) +{ + Q_UNUSED(notifier); + qWarning("QEventDispatcherWasm::unregisterSocketNotifier: socket notifiers are not supported"); +} + +void QEventDispatcherWasm::registerTimer(int timerId, qint64 interval, Qt::TimerType timerType, QObject *object) +{ +#ifndef QT_NO_DEBUG + if (timerId < 1 || interval < 0 || !object) { + qWarning("QEventDispatcherWasm::registerTimer: invalid arguments"); + return; + } else if (object->thread() != thread() || thread() != QThread::currentThread()) { + qWarning("QEventDispatcherWasm::registerTimer: timers cannot be started from another " + "thread"); + return; + } +#endif + qCDebug(lcEventDispatcherTimers) << "registerTimer" << timerId << interval << timerType << object; + + m_timerInfo->registerTimer(timerId, interval, timerType, object); + updateNativeTimer(); +} + +bool QEventDispatcherWasm::unregisterTimer(int timerId) +{ +#ifndef QT_NO_DEBUG + if (timerId < 1) { + qWarning("QEventDispatcherWasm::unregisterTimer: invalid argument"); + return false; + } else if (thread() != QThread::currentThread()) { + qWarning("QEventDispatcherWasm::unregisterTimer: timers cannot be stopped from another " + "thread"); + return false; + } +#endif + + qCDebug(lcEventDispatcherTimers) << "unregisterTimer" << timerId; + + bool ans = m_timerInfo->unregisterTimer(timerId); + updateNativeTimer(); + return ans; +} + +bool QEventDispatcherWasm::unregisterTimers(QObject *object) +{ +#ifndef QT_NO_DEBUG + if (!object) { + qWarning("QEventDispatcherWasm::unregisterTimers: invalid argument"); + return false; + } else if (object->thread() != thread() || thread() != QThread::currentThread()) { + qWarning("QEventDispatcherWasm::unregisterTimers: timers cannot be stopped from another " + "thread"); + return false; + } +#endif + + qCDebug(lcEventDispatcherTimers) << "registerTimer" << object; + + bool ans = m_timerInfo->unregisterTimers(object); + updateNativeTimer(); + return ans; +} + +QList +QEventDispatcherWasm::registeredTimers(QObject *object) const +{ +#ifndef QT_NO_DEBUG + if (!object) { + qWarning("QEventDispatcherWasm:registeredTimers: invalid argument"); + return QList(); + } +#endif + + return m_timerInfo->registeredTimers(object); +} + +int QEventDispatcherWasm::remainingTime(int timerId) +{ + return m_timerInfo->timerRemainingTime(timerId); +} + +void QEventDispatcherWasm::interrupt() +{ + m_interrupted = true; + wakeUp(); +} + +void QEventDispatcherWasm::wakeUp() +{ +#if QT_CONFIG(thread) + if (isSecondaryThreadEventDispatcher()) { + std::lock_guard lock(m_mutex); + m_wakeUpCalled = true; + m_moreEvents.notify_one(); + return; + } +#endif + +#ifdef QT_HAVE_EMSCRIPTEN_ASYNCIFY + // The main thread may be asyncify-blocked in processEvents(). If so resume it. + if (qt_asyncify_resume()) // ### safe to call from secondary thread? + return; +#endif + + { +#if QT_CONFIG(thread) + // This function can be called from any thread (via wakeUp()), + // so we need to lock access to m_pendingProcessEvents. + std::lock_guard lock(m_mutex); +#endif + if (m_pendingProcessEvents) + return; + m_pendingProcessEvents = true; + } + +#if QT_CONFIG(thread) + if (!emscripten_is_main_runtime_thread()) { + runOnMainThread([this](){ + QEventDispatcherWasm::callProcessEvents(this); + }); + } else +#endif + emscripten_async_call(&QEventDispatcherWasm::callProcessEvents, this, 0); +} + +void QEventDispatcherWasm::pollForNativeEvents() +{ + // Secondary thread event dispatchers do not support native events + if (isSecondaryThreadEventDispatcher()) + return; + +#if HAVE_EMSCRIPTEN_ASYNCIFY + // Asyncify allows us to yield to the browser and have it process native events - + // but this will fail if we are recursing and are already in a yield. + bool didYield = qt_asyncify_yield(); + if (!didYield) + qWarning("QEventDispatcherWasm::processEvents() did not asyncify process native events"); +#endif +} + +// Waits for more events. This is possible in two cases: +// - On a secondary thread +// - On the main thread iff asyncify is used +// Returns true if waiting was possible (at which point it +// has already happened). +bool QEventDispatcherWasm::waitForForEvents() +{ +#if QT_CONFIG(thread) + if (isSecondaryThreadEventDispatcher()) { + std::unique_lock lock(m_mutex); + m_moreEvents.wait(lock, [=] { return m_wakeUpCalled; }); + m_wakeUpCalled = false; + return true; + } +#endif + + Q_ASSERT(emscripten_is_main_runtime_thread()); + +#if QT_HAVE_EMSCRIPTEN_ASYNCIFY + // We can block on the main thread using asyncify: + bool didSuspend = qt_asyncify_suspend(); + if (!didSuspend) + qWarning("QEventDispatcherWasm: current thread is already suspended; could not asyncify wait for events"); + return didSuspend; +#else + qWarning("QEventLoop::WaitForMoreEvents is not supported on the main thread without asyncify"); + return false; +#endif +} + +// Process event activation callbacks for the main thread event dispatcher. +// Must be called on the main thread. +void QEventDispatcherWasm::callProcessEvents(void *context) +{ + Q_ASSERT(emscripten_is_main_runtime_thread()); + + // Bail out if Qt has been shut down. + if (!g_mainThreadEventDispatcher) + return; + + // In the unlikely event that we get a callProcessEvents() call for + // a previous main thread event dispatcher (i.e. the QApplication + // object was deleted and crated again): just ignore it and return. + if (context != g_mainThreadEventDispatcher) + return; + + { +#if QT_CONFIG(thread) + std::lock_guard lock(g_mainThreadEventDispatcher->m_mutex); +#endif + g_mainThreadEventDispatcher->m_pendingProcessEvents = false; + } + g_mainThreadEventDispatcher->processEvents(QEventLoop::AllEvents); +} + +void QEventDispatcherWasm::processTimers() +{ + m_timerInfo->activateTimers(); + updateNativeTimer(); // schedule next native timer, if any +} + +// Updates the native timer based on currently registered Qt timers. +// Must be called on the event dispatcher thread. +void QEventDispatcherWasm::updateNativeTimer() +{ +#if QT_CONFIG(thread) + Q_ASSERT(QThread::currentThread() == thread()); +#endif + + // Multiplex Qt timers down to a single native timer, maintained + // to have a timeout corresponding to the shortest Qt timer. This + // is done in two steps: first determine the target wakeup time + // on the event dispatcher thread (since this thread has exclusive + // access to m_timerInfo), and then call native API to set the new + // wakeup time on the main thread. + + auto timespecToNanosec = [](timespec ts) -> uint64_t { + return ts.tv_sec * 1000 + ts.tv_nsec / (1000 * 1000); + }; + timespec toWait; + m_timerInfo->timerWait(toWait); + uint64_t currentTime = timespecToNanosec(m_timerInfo->currentTime); + uint64_t toWaitDuration = timespecToNanosec(toWait); + uint64_t newTargetTime = currentTime + toWaitDuration; + + auto maintainNativeTimer = [this, toWaitDuration, newTargetTime]() { + Q_ASSERT(emscripten_is_main_runtime_thread()); + + if (m_timerTargetTime != 0 && newTargetTime >= m_timerTargetTime) + return; // existing timer is good + + qCDebug(lcEventDispatcherTimers) + << "Created new native timer with wait" << toWaitDuration << "timeout" << newTargetTime; + emscripten_clear_timeout(m_timerId); + m_timerId = emscripten_set_timeout(&QEventDispatcherWasm::callProcessTimers, toWaitDuration, this); + m_timerTargetTime = newTargetTime; + }; + + // Update the native timer for this thread/dispatcher. This must be + // done on the main thread where we have access to native API. + +#if QT_CONFIG(thread) + if (isSecondaryThreadEventDispatcher()) { + runOnMainThread([this, maintainNativeTimer]() { + Q_ASSERT(emscripten_is_main_runtime_thread()); + + // "this" may have been deleted, or may be about to be deleted. + // Check if the pointer we have is still a valid event dispatcher, + // and keep the mutex locked while updating the native timer to + // prevent it from being deleted. + std::lock_guard lock(g_secondaryThreadEventDispatchersMutex); + if (g_secondaryThreadEventDispatchers.contains(this)) + maintainNativeTimer(); + }); + } else +#endif + maintainNativeTimer(); +} + +// Static timer activation callback. Must be called on the main thread +// and will then either process timers on the main thrad or wake and +// process timers on a secondary thread. +void QEventDispatcherWasm::callProcessTimers(void *context) +{ + Q_ASSERT(emscripten_is_main_runtime_thread()); + + // Bail out if Qt has been shut down + if (!g_mainThreadEventDispatcher) + return; + + // Note: "context" may be a stale pointer here, + // take care before casting and dereferencing! + + // Process timers on this thread if this is the main event dispatcher + if (reinterpret_cast(context) == g_mainThreadEventDispatcher) { + g_mainThreadEventDispatcher->m_timerTargetTime = 0; + g_mainThreadEventDispatcher->processTimers(); + return; + } + + // Wake and process timers on the secondary thread if this a secondary thread dispatcher +#if QT_CONFIG(thread) + std::lock_guard lock(g_secondaryThreadEventDispatchersMutex); + if (g_secondaryThreadEventDispatchers.contains(context)) { + QEventDispatcherWasm *eventDispatcher = reinterpret_cast(context); + eventDispatcher->m_timerTargetTime = 0; + eventDispatcher->m_processTimers = true; + eventDispatcher->wakeUp(); + } +#endif +} + +#if QT_CONFIG(thread) +// Runs a function on the main thread +void QEventDispatcherWasm::runOnMainThread(std::function fn) +{ + static auto trampoline = [](void *context) { + std::function *fn = reinterpret_cast *>(context); + (*fn)(); + delete fn; + }; + void *context = new std::function(fn); + emscripten_async_run_in_main_runtime_thread_(EM_FUNC_SIG_VI, reinterpret_cast(&trampoline), context); +} +#endif + +QT_END_NAMESPACE diff --git a/src/corelib/kernel/qeventdispatcher_wasm_p.h b/src/corelib/kernel/qeventdispatcher_wasm_p.h new file mode 100644 index 00000000000..2fcd512773a --- /dev/null +++ b/src/corelib/kernel/qeventdispatcher_wasm_p.h @@ -0,0 +1,125 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QEVENTDISPATCHER_WASM_P_H +#define QEVENTDISPATCHER_WASM_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qabstracteventdispatcher.h" +#include "private/qtimerinfo_unix_p.h" +#include +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcEventDispatcher); +Q_DECLARE_LOGGING_CATEGORY(lcEventDispatcherTimers) + +class Q_CORE_EXPORT QEventDispatcherWasm : public QAbstractEventDispatcher +{ + Q_OBJECT +public: + QEventDispatcherWasm(); + ~QEventDispatcherWasm(); + + bool processEvents(QEventLoop::ProcessEventsFlags flags) override; + + void registerSocketNotifier(QSocketNotifier *notifier) override; + void unregisterSocketNotifier(QSocketNotifier *notifier) override; + + void registerTimer(int timerId, qint64 interval, Qt::TimerType timerType, QObject *object) override; + bool unregisterTimer(int timerId) override; + bool unregisterTimers(QObject *object) override; + QList registeredTimers(QObject *object) const override; + int remainingTime(int timerId) override; + + void interrupt() override; + void wakeUp() override; + +private: + bool isMainThreadEventDispatcher(); + bool isSecondaryThreadEventDispatcher(); + + void pollForNativeEvents(); + bool waitForForEvents(); + static void callProcessEvents(void *eventDispatcher); + + void processTimers(); + void updateNativeTimer(); + static void callProcessTimers(void *eventDispatcher); + +#if QT_CONFIG(thread) + void runOnMainThread(std::function fn); +#endif + + static QEventDispatcherWasm *g_mainThreadEventDispatcher; + + bool m_interrupted = false; + bool m_processTimers = false; + bool m_pendingProcessEvents = false; + + QTimerInfoList *m_timerInfo = new QTimerInfoList(); + long m_timerId = 0; + uint64_t m_timerTargetTime = 0; + +#if QT_CONFIG(thread) + std::mutex m_mutex; + bool m_wakeUpCalled = false; + std::condition_variable m_moreEvents; + + static QVector g_secondaryThreadEventDispatchers; + static std::mutex g_secondaryThreadEventDispatchersMutex; +#endif +}; + +#endif // QEVENTDISPATCHER_WASM_P_H