diff options
Diffstat (limited to 'src/corelib/kernel/qeventdispatcher_wasm.cpp')
-rw-r--r-- | src/corelib/kernel/qeventdispatcher_wasm.cpp | 903 |
1 files changed, 661 insertions, 242 deletions
diff --git a/src/corelib/kernel/qeventdispatcher_wasm.cpp b/src/corelib/kernel/qeventdispatcher_wasm.cpp index 7632b2ca55..4aa435b64b 100644 --- a/src/corelib/kernel/qeventdispatcher_wasm.cpp +++ b/src/corelib/kernel/qeventdispatcher_wasm.cpp @@ -1,66 +1,33 @@ -/**************************************************************************** -** -** 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$ -** -****************************************************************************/ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qeventdispatcher_wasm_p.h" +#include <QtCore/private/qabstracteventdispatcher_p.h> // for qGlobalPostedEventsCount() #include <QtCore/qcoreapplication.h> #include <QtCore/qthread.h> +#include <QtCore/qsocketnotifier.h> +#include <QtCore/private/qstdweb_p.h> #include "emscripten.h" #include <emscripten/html5.h> #include <emscripten/threading.h> +#include <emscripten/val.h> + +using namespace std::chrono; +using namespace std::chrono_literals; 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) +#if QT_CONFIG(thread) +#define LOCK_GUARD(M) std::lock_guard<std::mutex> lock(M) #else - #define QT_ASYNCIFY_DEBUG(X) +#define LOCK_GUARD(M) #endif // Emscripten asyncify currently supports one level of suspend - @@ -69,30 +36,127 @@ Q_LOGGING_CATEGORY(lcEventDispatcherTimers, "qt.eventdispatcher.timers"); // track Qts own usage of asyncify. static bool g_is_asyncify_suspended = false; +#if defined(QT_STATIC) + +static bool useAsyncify() +{ + return qstdweb::haveAsyncify(); +} + +static bool useJspi() +{ + return qstdweb::haveJspi(); +} + +// clang-format off +EM_ASYNC_JS(void, qt_jspi_suspend_js, (), { + ++Module.qtJspiSuspensionCounter; + + await new Promise(resolve => { + Module.qtAsyncifyWakeUp.push(resolve); + }); +}); + +EM_JS(bool, qt_jspi_resume_js, (), { + if (!Module.qtJspiSuspensionCounter) + return false; + + --Module.qtJspiSuspensionCounter; + + setTimeout(() => { + const wakeUp = (Module.qtAsyncifyWakeUp ?? []).pop(); + if (wakeUp) wakeUp(); + }); + return true; +}); + +EM_JS(bool, qt_jspi_can_resume_js, (), { + return Module.qtJspiSuspensionCounter > 0; +}); + +EM_JS(void, init_jspi_support_js, (), { + Module.qtAsyncifyWakeUp = []; + Module.qtJspiSuspensionCounter = 0; +}); +// clang-format on + +void initJspiSupport() { + init_jspi_support_js(); +} + +Q_CONSTRUCTOR_FUNCTION(initJspiSupport); + +// clang-format off 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 + if (Module.qtSuspendId === undefined) + Module.qtSuspendId = 0; + let sleepFn = (wakeUp) => { + Module.qtAsyncifyWakeUp = wakeUp; }; + ++Module.qtSuspendId; 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"); + if (wakeUp == undefined) return; - } Module.qtAsyncifyWakeUp = undefined; + const suspendId = Module.qtSuspendId; // 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"); + setTimeout(() => { + // Another suspend occurred while the timeout was in queue. + if (Module.qtSuspendId !== suspendId) + return; + wakeUp(); + }); }); +// clang-format on + +#else + +// EM_JS is not supported for side modules; disable asyncify + +static bool useAsyncify() +{ + return false; +} + +static bool useJspi() +{ + return false; +} + +void qt_jspi_suspend_js() +{ + Q_UNREACHABLE(); +} + +bool qt_jspi_resume_js() +{ + Q_UNREACHABLE(); + return false; +} + +bool qt_jspi_can_resume_js() +{ + Q_UNREACHABLE(); + return false; +} + +void qt_asyncify_suspend_js() +{ + Q_UNREACHABLE(); +} + +void qt_asyncify_resume_js() +{ + Q_UNREACHABLE(); +} + +#endif // defined(QT_STATIC) // Suspends the main thread until qt_asyncify_resume() is called. Returns // false immediately if Qt has already suspended the main thread (recursive @@ -108,37 +172,28 @@ bool qt_asyncify_suspend() } // 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() +// thread was suspended, in which case it will now be asynchronously woken. +void qt_asyncify_resume() { if (!g_is_asyncify_suspended) - return false; + return; 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; +Q_CONSTINIT QEventDispatcherWasm *QEventDispatcherWasm::g_mainThreadEventDispatcher = nullptr; #if QT_CONFIG(thread) -QVector<QEventDispatcherWasm *> QEventDispatcherWasm::g_secondaryThreadEventDispatchers; -std::mutex QEventDispatcherWasm::g_secondaryThreadEventDispatchersMutex; +Q_CONSTINIT QVector<QEventDispatcherWasm *> QEventDispatcherWasm::g_secondaryThreadEventDispatchers; +Q_CONSTINIT std::mutex QEventDispatcherWasm::g_staticDataMutex; +emscripten::ProxyingQueue QEventDispatcherWasm::g_proxyingQueue; +pthread_t QEventDispatcherWasm::g_mainThread; #endif +// ### dynamic initialization: +std::multimap<int, QSocketNotifier *> QEventDispatcherWasm::g_socketNotifiers; +std::map<int, QEventDispatcherWasm::SocketReadyState> QEventDispatcherWasm::g_socketState; QEventDispatcherWasm::QEventDispatcherWasm() - : QAbstractEventDispatcher() { // QEventDispatcherWasm operates in two main modes: // - On the main thread: @@ -160,9 +215,18 @@ QEventDispatcherWasm::QEventDispatcherWasm() // dispatchers so we set a global pointer to it. Q_ASSERT(g_mainThreadEventDispatcher == nullptr); g_mainThreadEventDispatcher = this; +#if QT_CONFIG(thread) + g_mainThread = pthread_self(); +#endif + + // Call the "onLoaded" JavaScript callback, unless startup tasks + // have been registered which should complete first. Run async + // to make sure event dispatcher construction (in particular any + // subclass construction) has completed first. + runAsync(callOnLoadedIfRequired); } else { #if QT_CONFIG(thread) - std::lock_guard<std::mutex> lock(g_secondaryThreadEventDispatchersMutex); + std::lock_guard<std::mutex> lock(g_staticDataMutex); g_secondaryThreadEventDispatchers.append(this); #endif } @@ -170,20 +234,32 @@ QEventDispatcherWasm::QEventDispatcherWasm() QEventDispatcherWasm::~QEventDispatcherWasm() { - qCDebug(lcEventDispatcher) << "Detroying QEventDispatcherWasm instance" << this; + qCDebug(lcEventDispatcher) << "Destroying QEventDispatcherWasm instance" << this; delete m_timerInfo; #if QT_CONFIG(thread) if (isSecondaryThreadEventDispatcher()) { - std::lock_guard<std::mutex> lock(g_secondaryThreadEventDispatchersMutex); + std::lock_guard<std::mutex> lock(g_staticDataMutex); g_secondaryThreadEventDispatchers.remove(g_secondaryThreadEventDispatchers.indexOf(this)); } else #endif { if (m_timerId > 0) emscripten_clear_timeout(m_timerId); + if (!g_socketNotifiers.empty()) { + qWarning("QEventDispatcherWasm: main thread event dispatcher deleted with active socket notifiers"); + clearEmscriptenSocketCallbacks(); + g_socketNotifiers.clear(); + } g_mainThreadEventDispatcher = nullptr; + if (!g_socketNotifiers.empty()) { + qWarning("QEventDispatcherWasm: main thread event dispatcher deleted with active socket notifiers"); + clearEmscriptenSocketCallbacks(); + g_socketNotifiers.clear(); + } + + g_socketState.clear(); } } @@ -197,59 +273,92 @@ bool QEventDispatcherWasm::isSecondaryThreadEventDispatcher() return this != g_mainThreadEventDispatcher; } -bool QEventDispatcherWasm::processEvents(QEventLoop::ProcessEventsFlags flags) +bool QEventDispatcherWasm::isValidEventDispatcherPointer(QEventDispatcherWasm *eventDispatcher) { - emit awake(); + if (eventDispatcher == g_mainThreadEventDispatcher) + return true; +#if QT_CONFIG(thread) + if (g_secondaryThreadEventDispatchers.contains(eventDispatcher)) + return true; +#endif + return false; +} - bool hasPendingEvents = qGlobalPostedEventsCount() > 0; +bool QEventDispatcherWasm::processEvents(QEventLoop::ProcessEventsFlags flags) +{ + qCDebug(lcEventDispatcher) << "QEventDispatcherWasm::processEvents flags" << flags; - qCDebug(lcEventDispatcher) << "QEventDispatcherWasm::processEvents flags" << flags - << "pending events" << hasPendingEvents; + emit awake(); - if (flags & QEventLoop::DialogExec) - handleDialogExec(); - else if (flags & QEventLoop::EventLoopExec) - handleEventLoopExec(); + if (isMainThreadEventDispatcher()) { + if (flags & QEventLoop::DialogExec) + handleDialogExec(); + else if (flags & QEventLoop::ApplicationExec) + handleApplicationExec(); + } - if (!(flags & QEventLoop::ExcludeUserInputEvents)) - pollForNativeEvents(); +#if QT_CONFIG(thread) + { + // Reset wakeUp state: if wakeUp() was called at some point before + // this then processPostedEvents() below will service that call. + std::unique_lock<std::mutex> lock(m_mutex); + m_wakeUpCalled = false; + } +#endif - hasPendingEvents = qGlobalPostedEventsCount() > 0; + processPostedEvents(); - if (!hasPendingEvents && (flags & QEventLoop::WaitForMoreEvents)) - waitForForEvents(); + // The processPostedEvents() call above may process an event which deletes the + // application object and the event dispatcher; stop event processing in that case. + if (!isValidEventDispatcherPointer(this)) + return false; if (m_interrupted) { m_interrupted = false; return false; } + if (flags & QEventLoop::WaitForMoreEvents) + wait(); + if (m_processTimers) { m_processTimers = false; processTimers(); } - hasPendingEvents = qGlobalPostedEventsCount() > 0; - QCoreApplication::sendPostedEvents(); - return hasPendingEvents; + return false; } void QEventDispatcherWasm::registerSocketNotifier(QSocketNotifier *notifier) { - Q_UNUSED(notifier); - qWarning("QEventDispatcherWasm::registerSocketNotifier: socket notifiers are not supported"); + LOCK_GUARD(g_staticDataMutex); + + bool wasEmpty = g_socketNotifiers.empty(); + g_socketNotifiers.insert({notifier->socket(), notifier}); + if (wasEmpty) + runOnMainThread([] { setEmscriptenSocketCallbacks(); }); } void QEventDispatcherWasm::unregisterSocketNotifier(QSocketNotifier *notifier) { - Q_UNUSED(notifier); - qWarning("QEventDispatcherWasm::unregisterSocketNotifier: socket notifiers are not supported"); + LOCK_GUARD(g_staticDataMutex); + + auto notifiers = g_socketNotifiers.equal_range(notifier->socket()); + for (auto it = notifiers.first; it != notifiers.second; ++it) { + if (it->second == notifier) { + g_socketNotifiers.erase(it); + break; + } + } + + if (g_socketNotifiers.empty()) + runOnMainThread([] { clearEmscriptenSocketCallbacks(); }); } -void QEventDispatcherWasm::registerTimer(int timerId, qint64 interval, Qt::TimerType timerType, QObject *object) +void QEventDispatcherWasm::registerTimer(Qt::TimerId timerId, Duration interval, Qt::TimerType timerType, QObject *object) { #ifndef QT_NO_DEBUG - if (timerId < 1 || interval < 0 || !object) { + if (qToUnderlying(timerId) < 1 || interval < 0ns || !object) { qWarning("QEventDispatcherWasm::registerTimer: invalid arguments"); return; } else if (object->thread() != thread() || thread() != QThread::currentThread()) { @@ -258,16 +367,16 @@ void QEventDispatcherWasm::registerTimer(int timerId, qint64 interval, Qt::Timer return; } #endif - qCDebug(lcEventDispatcherTimers) << "registerTimer" << timerId << interval << timerType << object; + qCDebug(lcEventDispatcherTimers) << "registerTimer" << int(timerId) << interval << timerType << object; m_timerInfo->registerTimer(timerId, interval, timerType, object); updateNativeTimer(); } -bool QEventDispatcherWasm::unregisterTimer(int timerId) +bool QEventDispatcherWasm::unregisterTimer(Qt::TimerId timerId) { #ifndef QT_NO_DEBUG - if (timerId < 1) { + if (qToUnderlying(timerId) < 1) { qWarning("QEventDispatcherWasm::unregisterTimer: invalid argument"); return false; } else if (thread() != QThread::currentThread()) { @@ -277,7 +386,7 @@ bool QEventDispatcherWasm::unregisterTimer(int timerId) } #endif - qCDebug(lcEventDispatcherTimers) << "unregisterTimer" << timerId; + qCDebug(lcEventDispatcherTimers) << "unregisterTimer" << int(timerId); bool ans = m_timerInfo->unregisterTimer(timerId); updateNativeTimer(); @@ -304,22 +413,22 @@ bool QEventDispatcherWasm::unregisterTimers(QObject *object) return ans; } -QList<QAbstractEventDispatcher::TimerInfo> -QEventDispatcherWasm::registeredTimers(QObject *object) const +QList<QAbstractEventDispatcher::TimerInfoV2> +QEventDispatcherWasm::timersForObject(QObject *object) const { #ifndef QT_NO_DEBUG if (!object) { qWarning("QEventDispatcherWasm:registeredTimers: invalid argument"); - return QList<TimerInfo>(); + return {}; } #endif return m_timerInfo->registeredTimers(object); } -int QEventDispatcherWasm::remainingTime(int timerId) +QEventDispatcherWasm::Duration QEventDispatcherWasm::remainingTime(Qt::TimerId timerId) const { - return m_timerInfo->timerRemainingTime(timerId); + return m_timerInfo->remainingDuration(timerId); } void QEventDispatcherWasm::interrupt() @@ -330,43 +439,27 @@ void QEventDispatcherWasm::interrupt() void QEventDispatcherWasm::wakeUp() { -#if QT_CONFIG(thread) - if (isSecondaryThreadEventDispatcher()) { - std::lock_guard<std::mutex> 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<std::mutex> 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); + // The event dispatcher thread may be blocked or suspended by + // wait(), or control may have been returned to the browser's + // event loop. Make sure the thread is unblocked or make it + // process events. + bool wasBlocked = wakeEventDispatcherThread(); + // JSPI does not need a scheduled call to processPostedEvents, as the stack is not unwound + // at startup. + if (!qstdweb::haveJspi() && !wasBlocked && isMainThreadEventDispatcher()) { + { + LOCK_GUARD(m_mutex); + if (m_pendingProcessEvents) + return; + m_pendingProcessEvents = true; + } + runOnMainThreadAsync([this](){ + QEventDispatcherWasm::callProcessPostedEvents(this); }); - } else -#endif - emscripten_async_call(&QEventDispatcherWasm::callProcessEvents, this, 0); + } } -void QEventDispatcherWasm::handleEventLoopExec() +void QEventDispatcherWasm::handleApplicationExec() { // Start the main loop, and then stop it on the first callback. This // is done for the "simulateInfiniteLoop" functionality where @@ -376,75 +469,108 @@ void QEventDispatcherWasm::handleEventLoopExec() // Note that we don't use asyncify here: Emscripten supports one level of // asyncify only and we want to reserve that for dialog exec() instead of // using it for the one qApp exec(). - const bool simulateInfiniteLoop = true; - emscripten_set_main_loop([](){ - emscripten_pause_main_loop(); - }, 0, simulateInfiniteLoop); + // When JSPI is used, awaited async calls are allowed to be nested, so we + // proceed normally. + if (!qstdweb::haveJspi()) { + const bool simulateInfiniteLoop = true; + emscripten_set_main_loop([](){ + emscripten_pause_main_loop(); + }, 0, simulateInfiniteLoop); + } } void QEventDispatcherWasm::handleDialogExec() { -#if !QT_HAVE_EMSCRIPTEN_ASYNCIFY - qWarning() << "Warning: dialog exec() is not supported on Qt for WebAssembly in this" - << "configuration. Please use show() instead, or enable experimental support" - << "for asyncify.\n" - << "When using exec() (without asyncify) the dialog will show, the user can interact" - << "with it and the appropriate signals will be emitted on close. However, the" - << "exec() call never returns, stack content at the time of the exec() call" - << "is leaked, and the exec() call may interfere with input event processing"; - emscripten_sleep(1); // This call never returns -#endif - // For the asyncify case we do nothing here and wait for events in waitForForEvents() + if (!useAsyncify()) { + qWarning() << "Warning: exec() is not supported on Qt for WebAssembly in this configuration. Please build" + << "with asyncify support, or use an asynchronous API like QDialog::open()"; + emscripten_sleep(1); // This call never returns + } + // For the asyncify case we do nothing here and wait for events in wait() } -void QEventDispatcherWasm::pollForNativeEvents() +// Blocks/suspends the calling thread. This is possible in two cases: +// - Caller is a secondary thread: block on m_moreEvents +// - Caller is the main thread and asyncify is enabled: suspend using qt_asyncify_suspend() +// Returns false if the wait timed out. +bool QEventDispatcherWasm::wait(int timeout) { - // Secondary thread event dispatchers do not support native events - if (isSecondaryThreadEventDispatcher()) - return; +#if QT_CONFIG(thread) + using namespace std::chrono_literals; + Q_ASSERT(QThread::currentThread() == thread()); + + if (isSecondaryThreadEventDispatcher()) { + std::unique_lock<std::mutex> lock(m_mutex); -#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"); + // If wakeUp() was called there might be pending events in the event + // queue which should be processed. Don't block, instead return + // so that the event loop can spin and call processEvents() again. + if (m_wakeUpCalled) + return true; + + auto wait_time = timeout > 0 ? timeout * 1ms : std::chrono::duration<int, std::micro>::max(); + bool wakeUpCalled = m_moreEvents.wait_for(lock, wait_time, [=] { return m_wakeUpCalled; }); + return wakeUpCalled; + } #endif + Q_ASSERT(emscripten_is_main_runtime_thread()); + Q_ASSERT(isMainThreadEventDispatcher()); + if (useAsyncify()) { + if (timeout > 0) + qWarning() << "QEventDispatcherWasm asyncify wait with timeout is not supported; timeout will be ignored"; // FIXME + + if (useJspi()) { + qt_jspi_suspend_js(); + } else { + bool didSuspend = qt_asyncify_suspend(); + if (!didSuspend) { + qWarning("QEventDispatcherWasm: current thread is already suspended; could not asyncify wait for events"); + return false; + } + } + return true; + } else { + qWarning("QEventLoop::WaitForMoreEvents is not supported on the main thread without asyncify"); + Q_UNUSED(timeout); + } + return false; } -// 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() +// Wakes a blocked/suspended event dispatcher thread. Returns true if the +// thread is unblocked or was resumed, false if the thread state could not +// be determined. +bool QEventDispatcherWasm::wakeEventDispatcherThread() { #if QT_CONFIG(thread) if (isSecondaryThreadEventDispatcher()) { - std::unique_lock<std::mutex> lock(m_mutex); - m_moreEvents.wait(lock, [=] { return m_wakeUpCalled; }); - m_wakeUpCalled = false; + std::lock_guard<std::mutex> lock(m_mutex); + m_wakeUpCalled = true; + m_moreEvents.notify_one(); return true; } #endif + Q_ASSERT(isMainThreadEventDispatcher()); + if (useJspi()) { - 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; +#if QT_CONFIG(thread) + return qstdweb::runTaskOnMainThread<bool>( + []() { return qt_jspi_can_resume_js() && qt_jspi_resume_js(); }, &g_proxyingQueue); #else - qWarning("QEventLoop::WaitForMoreEvents is not supported on the main thread without asyncify"); - return false; + return qstdweb::runTaskOnMainThread<bool>( + []() { return qt_jspi_can_resume_js() && qt_jspi_resume_js(); }); #endif + + } else { + if (!g_is_asyncify_suspended) + return false; + runOnMainThread([]() { qt_asyncify_resume(); }); + } + return true; } // Process event activation callbacks for the main thread event dispatcher. // Must be called on the main thread. -void QEventDispatcherWasm::callProcessEvents(void *context) +void QEventDispatcherWasm::callProcessPostedEvents(void *context) { Q_ASSERT(emscripten_is_main_runtime_thread()); @@ -452,19 +578,24 @@ void QEventDispatcherWasm::callProcessEvents(void *context) if (!g_mainThreadEventDispatcher) return; - // In the unlikely event that we get a callProcessEvents() call for + // In the unlikely event that we get a callProcessPostedEvents() call for // a previous main thread event dispatcher (i.e. the QApplication - // object was deleted and crated again): just ignore it and return. + // object was deleted and created again): just ignore it and return. if (context != g_mainThreadEventDispatcher) return; { -#if QT_CONFIG(thread) - std::lock_guard<std::mutex> lock(g_mainThreadEventDispatcher->m_mutex); -#endif + LOCK_GUARD(g_mainThreadEventDispatcher->m_mutex); g_mainThreadEventDispatcher->m_pendingProcessEvents = false; } - g_mainThreadEventDispatcher->processEvents(QEventLoop::AllEvents); + + g_mainThreadEventDispatcher->processPostedEvents(); +} + +bool QEventDispatcherWasm::processPostedEvents() +{ + QCoreApplication::sendPostedEvents(); + return false; } void QEventDispatcherWasm::processTimers() @@ -488,94 +619,382 @@ void QEventDispatcherWasm::updateNativeTimer() // 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]() { + const std::optional<std::chrono::nanoseconds> wait = m_timerInfo->timerWait(); + const auto toWaitDuration = duration_cast<milliseconds>(wait.value_or(0ms)); + const auto newTargetTimePoint = m_timerInfo->currentTime + toWaitDuration; + auto epochNsecs = newTargetTimePoint.time_since_epoch(); + auto newTargetTime = std::chrono::duration_cast<std::chrono::milliseconds>(epochNsecs); + auto maintainNativeTimer = [this, wait, toWaitDuration, newTargetTime]() { Q_ASSERT(emscripten_is_main_runtime_thread()); - if (m_timerTargetTime != 0 && newTargetTime >= m_timerTargetTime) + if (!wait) { + if (m_timerId > 0) { + emscripten_clear_timeout(m_timerId); + m_timerId = 0; + m_timerTargetTime = 0ms; + } + return; + } + + if (m_timerTargetTime != 0ms && newTargetTime >= m_timerTargetTime) return; // existing timer is good qCDebug(lcEventDispatcherTimers) - << "Created new native timer with wait" << toWaitDuration << "timeout" << newTargetTime; + << "Created new native timer with wait" << toWaitDuration.count() << "ms" + << "timeout" << newTargetTime.count() << "ms"; emscripten_clear_timeout(m_timerId); - m_timerId = emscripten_set_timeout(&QEventDispatcherWasm::callProcessTimers, toWaitDuration, this); + m_timerId = emscripten_set_timeout(&QEventDispatcherWasm::callProcessTimers, + toWaitDuration.count(), 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. + runOnMainThread([this, maintainNativeTimer]() { + Q_ASSERT(emscripten_is_main_runtime_thread()); -#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<std::mutex> lock(g_secondaryThreadEventDispatchersMutex); - if (g_secondaryThreadEventDispatchers.contains(this)) - maintainNativeTimer(); - }); - } else -#endif - maintainNativeTimer(); + // "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. + LOCK_GUARD(g_staticDataMutex); + if (isValidEventDispatcherPointer(this)) + 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 +// and will then either process timers on the main thread 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<QEventDispatcherWasm *>(context) == g_mainThreadEventDispatcher) { - g_mainThreadEventDispatcher->m_timerTargetTime = 0; + g_mainThreadEventDispatcher->m_timerTargetTime = 0ms; 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<std::mutex> lock(g_secondaryThreadEventDispatchersMutex); + std::lock_guard<std::mutex> lock(g_staticDataMutex); if (g_secondaryThreadEventDispatchers.contains(context)) { QEventDispatcherWasm *eventDispatcher = reinterpret_cast<QEventDispatcherWasm *>(context); - eventDispatcher->m_timerTargetTime = 0; + eventDispatcher->m_timerTargetTime = 0ms; eventDispatcher->m_processTimers = true; eventDispatcher->wakeUp(); } #endif } -#if QT_CONFIG(thread) -// Runs a function on the main thread +void QEventDispatcherWasm::setEmscriptenSocketCallbacks() +{ + qCDebug(lcEventDispatcher) << "setEmscriptenSocketCallbacks"; + + emscripten_set_socket_error_callback(nullptr, QEventDispatcherWasm::socketError); + emscripten_set_socket_open_callback(nullptr, QEventDispatcherWasm::socketOpen); + emscripten_set_socket_listen_callback(nullptr, QEventDispatcherWasm::socketListen); + emscripten_set_socket_connection_callback(nullptr, QEventDispatcherWasm::socketConnection); + emscripten_set_socket_message_callback(nullptr, QEventDispatcherWasm::socketMessage); + emscripten_set_socket_close_callback(nullptr, QEventDispatcherWasm::socketClose); +} + +void QEventDispatcherWasm::clearEmscriptenSocketCallbacks() +{ + qCDebug(lcEventDispatcher) << "clearEmscriptenSocketCallbacks"; + + emscripten_set_socket_error_callback(nullptr, nullptr); + emscripten_set_socket_open_callback(nullptr, nullptr); + emscripten_set_socket_listen_callback(nullptr, nullptr); + emscripten_set_socket_connection_callback(nullptr, nullptr); + emscripten_set_socket_message_callback(nullptr, nullptr); + emscripten_set_socket_close_callback(nullptr, nullptr); +} + +void QEventDispatcherWasm::socketError(int socket, int err, const char* msg, void *context) +{ + Q_UNUSED(err); + Q_UNUSED(msg); + Q_UNUSED(context); + + // Emscripten makes socket callbacks while the main thread is busy-waiting for a mutex, + // which can cause deadlocks if the callback code also tries to lock the same mutex. + // This is most easily reproducible by adding print statements, where each print requires + // taking a mutex lock. Work around this by running the callback asynchronously, i.e. by using + // a native zero-timer, to make sure the main thread stack is completely unwond before calling + // the Qt handler. + // It is currently unclear if this problem is caused by code in Qt or in Emscripten, or + // if this completely fixes the problem. + runAsync([socket](){ + auto notifiersRange = g_socketNotifiers.equal_range(socket); + std::vector<std::pair<int, QSocketNotifier *>> notifiers(notifiersRange.first, notifiersRange.second); + for (auto [_, notifier]: notifiers) { + QCoreApplication::postEvent(notifier, new QEvent(QEvent::SockAct)); + } + setSocketState(socket, true, true); + }); +} + +void QEventDispatcherWasm::socketOpen(int socket, void *context) +{ + Q_UNUSED(context); + + runAsync([socket](){ + auto notifiersRange = g_socketNotifiers.equal_range(socket); + std::vector<std::pair<int, QSocketNotifier *>> notifiers(notifiersRange.first, notifiersRange.second); + for (auto [_, notifier]: notifiers) { + if (notifier->type() == QSocketNotifier::Write) { + QCoreApplication::postEvent(notifier, new QEvent(QEvent::SockAct)); + } + } + setSocketState(socket, false, true); + }); +} + +void QEventDispatcherWasm::socketListen(int socket, void *context) +{ + Q_UNUSED(socket); + Q_UNUSED(context); +} + +void QEventDispatcherWasm::socketConnection(int socket, void *context) +{ + Q_UNUSED(socket); + Q_UNUSED(context); +} + +void QEventDispatcherWasm::socketMessage(int socket, void *context) +{ + Q_UNUSED(context); + + runAsync([socket](){ + auto notifiersRange = g_socketNotifiers.equal_range(socket); + std::vector<std::pair<int, QSocketNotifier *>> notifiers(notifiersRange.first, notifiersRange.second); + for (auto [_, notifier]: notifiers) { + if (notifier->type() == QSocketNotifier::Read) { + QCoreApplication::postEvent(notifier, new QEvent(QEvent::SockAct)); + } + } + setSocketState(socket, true, false); + }); +} + +void QEventDispatcherWasm::socketClose(int socket, void *context) +{ + Q_UNUSED(context); + + // Emscripten makes emscripten_set_socket_close_callback() calls to socket 0, + // which is not a valid socket. see https://github.com/emscripten-core/emscripten/issues/6596 + if (socket == 0) + return; + + runAsync([socket](){ + auto notifiersRange = g_socketNotifiers.equal_range(socket); + std::vector<std::pair<int, QSocketNotifier *>> notifiers(notifiersRange.first, notifiersRange.second); + for (auto [_, notifier]: notifiers) + QCoreApplication::postEvent(notifier, new QEvent(QEvent::SockClose)); + + setSocketState(socket, true, true); + clearSocketState(socket); + }); +} + +void QEventDispatcherWasm::setSocketState(int socket, bool setReadyRead, bool setReadyWrite) +{ + LOCK_GUARD(g_staticDataMutex); + SocketReadyState &state = g_socketState[socket]; + + // Additively update socket ready state, e.g. if it + // was already ready read then it stays ready read. + state.readyRead |= setReadyRead; + state.readyWrite |= setReadyWrite; + + // Wake any waiters for the given readiness. The waiter consumes + // the ready state, returning the socket to not-ready. + if (QEventDispatcherWasm *waiter = state.waiter) + if ((state.readyRead && state.waitForReadyRead) || (state.readyWrite && state.waitForReadyWrite)) + waiter->wakeEventDispatcherThread(); +} + +void QEventDispatcherWasm::clearSocketState(int socket) +{ + LOCK_GUARD(g_staticDataMutex); + g_socketState.erase(socket); +} + +void QEventDispatcherWasm::waitForSocketState(int timeout, int socket, bool checkRead, bool checkWrite, + bool *selectForRead, bool *selectForWrite, bool *socketDisconnect) +{ + // Loop until the socket becomes readyRead or readyWrite. Wait for + // socket activity if it currently is neither. + while (true) { + *selectForRead = false; + *selectForWrite = false; + + { + LOCK_GUARD(g_staticDataMutex); + + // Access or create socket state: we want to register that a thread is waitng + // even if we have not received any socket callbacks yet. + SocketReadyState &state = g_socketState[socket]; + if (state.waiter) { + qWarning() << "QEventDispatcherWasm::waitForSocketState: a thread is already waiting"; + break; + } + + bool shouldWait = true; + if (checkRead && state.readyRead) { + shouldWait = false; + state.readyRead = false; + *selectForRead = true; + } + if (checkWrite && state.readyWrite) { + shouldWait = false; + state.readyWrite = false; + *selectForRead = true; + } + if (!shouldWait) + break; + + state.waiter = this; + state.waitForReadyRead = checkRead; + state.waitForReadyWrite = checkWrite; + } + + bool didTimeOut = !wait(timeout); + { + LOCK_GUARD(g_staticDataMutex); + + // Missing socket state after a wakeup means that the socket has been closed. + auto it = g_socketState.find(socket); + if (it == g_socketState.end()) { + *socketDisconnect = true; + break; + } + it->second.waiter = nullptr; + it->second.waitForReadyRead = false; + it->second.waitForReadyWrite = false; + } + + if (didTimeOut) + break; + } +} + +void QEventDispatcherWasm::socketSelect(int timeout, int socket, bool waitForRead, bool waitForWrite, + bool *selectForRead, bool *selectForWrite, bool *socketDisconnect) +{ + QEventDispatcherWasm *eventDispatcher = static_cast<QEventDispatcherWasm *>( + QAbstractEventDispatcher::instance(QThread::currentThread())); + + if (!eventDispatcher) { + qWarning("QEventDispatcherWasm::socketSelect called without eventdispatcher instance"); + return; + } + + eventDispatcher->waitForSocketState(timeout, socket, waitForRead, waitForWrite, + selectForRead, selectForWrite, socketDisconnect); +} + +namespace { + int g_startupTasks = 0; +} + +// The following functions manages sending the "qtLoaded" event/callback +// from qtloader.js on startup, once Qt initialization has been completed +// and the application is ready to display the first frame. This can be +// either as soon as the event loop is running, or later, if additional +// startup tasks (e.g. local font loading) have been registered. + +void QEventDispatcherWasm::registerStartupTask() +{ + ++g_startupTasks; +} + +void QEventDispatcherWasm::completeStarupTask() +{ + --g_startupTasks; + callOnLoadedIfRequired(); +} + +void QEventDispatcherWasm::callOnLoadedIfRequired() +{ + if (g_startupTasks > 0) + return; + + static bool qtLoadedCalled = false; + if (qtLoadedCalled) + return; + qtLoadedCalled = true; + + Q_ASSERT(g_mainThreadEventDispatcher); + g_mainThreadEventDispatcher->onLoaded(); +} + +void QEventDispatcherWasm::onLoaded() +{ + // TODO: call qtloader.js onLoaded from here, in order to delay + // hiding the "Loading..." message until the app is ready to paint + // the first frame. Currently onLoaded must be called early before + // main() in order to ensure that the screen/container elements + // have valid geometry at startup. +} + +namespace { + void trampoline(void *context) { + + auto async_fn = [](void *context){ + std::function<void(void)> *fn = reinterpret_cast<std::function<void(void)> *>(context); + (*fn)(); + delete fn; + }; + + emscripten_async_call(async_fn, context, 0); + } +} + +// Runs a function right away +void QEventDispatcherWasm::run(std::function<void(void)> fn) +{ + fn(); +} + void QEventDispatcherWasm::runOnMainThread(std::function<void(void)> fn) { - static auto trampoline = [](void *context) { - std::function<void(void)> *fn = reinterpret_cast<std::function<void(void)> *>(context); - (*fn)(); - delete fn; - }; - void *context = new std::function<void(void)>(fn); - emscripten_async_run_in_main_runtime_thread_(EM_FUNC_SIG_VI, reinterpret_cast<void *>(&trampoline), context); +#if QT_CONFIG(thread) + qstdweb::runTaskOnMainThread<void>(fn, &g_proxyingQueue); +#else + qstdweb::runTaskOnMainThread<void>(fn); +#endif +} + +// Runs a function asynchronously. Main thread only. +void QEventDispatcherWasm::runAsync(std::function<void(void)> fn) +{ + trampoline(new std::function<void(void)>(fn)); } + +// Runs a function on the main thread. The function always runs asynchronously, +// also if the calling thread is the main thread. +void QEventDispatcherWasm::runOnMainThreadAsync(std::function<void(void)> fn) +{ + void *context = new std::function<void(void)>(fn); +#if QT_CONFIG(thread) + if (!emscripten_is_main_runtime_thread()) { + g_proxyingQueue.proxyAsync(g_mainThread, [context]{ + trampoline(context); + }); + return; + } #endif + trampoline(context); +} QT_END_NAMESPACE + +#include "moc_qeventdispatcher_wasm_p.cpp" |