Revision control
++ // 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
++ // Qt-Security score:significant reason:default
++
++ #include "qeventdispatcher_wasm_p.h"
++
++ #include <QtCore/qcoreapplication.h>
++ #include <QtCore/qthread.h>
++ #include <QtCore/qscopedvaluerollback.h>
++ #include <QtCore/private/qobject_p.h>
++ #include <QtCore/private/qwasmglobal_p.h>
++ #include <QtCore/private/qstdweb_p.h>
++ #include <QtCore/private/qwasmsocket_p.h>
++
++ using namespace std::chrono;
++ using namespace std::chrono_literals;
++
++ QT_BEGIN_NAMESPACE
++
++ using emscripten::val;
++
++ Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher");
++ Q_LOGGING_CATEGORY(lcEventDispatcherTimers, "qt.eventdispatcher.timers");
++
++ #if QT_CONFIG(thread)
++ #define LOCK_GUARD(M) std::lock_guard<std::mutex> lock(M)
++ #else
++ #define LOCK_GUARD(M)
++ #endif
++
++ static bool useAsyncify()
++ {
++ return qstdweb::haveAsyncify();
++ }
++
++ Q_CONSTINIT QEventDispatcherWasm *QEventDispatcherWasm::g_mainThreadEventDispatcher = nullptr;
++ Q_CONSTINIT std::shared_ptr<QWasmSuspendResumeControl> QEventDispatcherWasm::g_customMainThreadSuspendResumeControl;
++ Q_CONSTINIT QWasmSuspendResumeControl *QEventDispatcherWasm::g_mainThreadSuspendResumeControl = nullptr;
++
++ #if QT_CONFIG(thread)
++ Q_CONSTINIT QVector<QEventDispatcherWasm *> QEventDispatcherWasm::g_secondaryThreadEventDispatchers;
++ Q_CONSTINIT std::mutex QEventDispatcherWasm::g_staticDataMutex;
++ #endif
++
++ QEventDispatcherWasm::QEventDispatcherWasm(std::shared_ptr<QWasmSuspendResumeControl> suspendResumeControl)
++ {
++ // 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;
++
++ if (suspendResumeControl) {
++ g_customMainThreadSuspendResumeControl = suspendResumeControl;
++ g_mainThreadSuspendResumeControl = g_customMainThreadSuspendResumeControl.get();
++ } else {
++ g_mainThreadSuspendResumeControl = QWasmSuspendResumeControl::get();
++ }
++
++ // Zero-timer used on wake() calls
++ m_wakeupTimer = std::make_unique<QWasmTimer>(g_mainThreadSuspendResumeControl, [](){ onWakeup(); });
++
++ // Timer set to fire at the next Qt timer timeout
++ m_nativeTimer = std::make_unique<QWasmTimer>(g_mainThreadSuspendResumeControl, []() { onTimer(); });
++
++ // Timer used when suspending to process native events
++ m_suspendTimer = std::make_unique<QWasmTimer>(g_mainThreadSuspendResumeControl, []() { onProcessNativeEventsResume(); });
++ } else {
++ #if QT_CONFIG(thread)
++ std::lock_guard<std::mutex> lock(g_staticDataMutex);
++ g_secondaryThreadEventDispatchers.append(this);
++ #endif
++ }
++
++ m_timerInfo = std::make_unique<QTimerInfoList>();
++ }
++
++ QEventDispatcherWasm::~QEventDispatcherWasm()
++ {
++ qCDebug(lcEventDispatcher) << "Destroying QEventDispatcherWasm instance" << this;
++
++ // Reset to ensure destruction before g_mainThreadSuspendResumeControl
++ m_wakeupTimer.reset();
++ m_nativeTimer.reset();
++ m_suspendTimer.reset();
++
++ #if QT_CONFIG(thread)
++ if (isSecondaryThreadEventDispatcher()) {
++ std::lock_guard<std::mutex> lock(g_staticDataMutex);
++ g_secondaryThreadEventDispatchers.remove(g_secondaryThreadEventDispatchers.indexOf(this));
++ } else
++ #endif
++ {
++ QWasmSocket::clearSocketNotifiers();
++ g_mainThreadEventDispatcher = nullptr;
++ g_customMainThreadSuspendResumeControl.reset();
++ g_mainThreadSuspendResumeControl = nullptr;
++ }
++ }
++
++ bool QEventDispatcherWasm::isMainThreadEventDispatcher()
++ {
++ return this == g_mainThreadEventDispatcher;
++ }
++
++ bool QEventDispatcherWasm::isSecondaryThreadEventDispatcher()
++ {
++ return this != g_mainThreadEventDispatcher;
++ }
++
++ bool QEventDispatcherWasm::isValidEventDispatcher()
++ {
++ return isValidEventDispatcherPointer(this);
++ }
++
++ bool QEventDispatcherWasm::isValidEventDispatcherPointer(QEventDispatcherWasm *eventDispatcher)
++ {
++ if (eventDispatcher == g_mainThreadEventDispatcher)
++ return true;
++ #if QT_CONFIG(thread)
++ if (g_secondaryThreadEventDispatchers.contains(eventDispatcher))
++ return true;
++ #endif
++ return false;
++ }
++
++ bool QEventDispatcherWasm::processEvents(QEventLoop::ProcessEventsFlags flags)
++ {
++ // Accept the current event if event handling recurses / event loop is reentered,
++ // to prevent the browser from propagating it to other event handlers.
++ if (useAsyncify() && isMainThreadEventDispatcher()) {
++ auto control = QWasmSuspendResumeControl::get();
++ auto currentEvent = control->currentEvent();
++ if (!currentEvent.isUndefined() &&
++ !currentEvent["isInstanceOfEvent"].isUndefined() &&
++ !currentEvent["isInstanceOfEvent"].isNull() &&
++ currentEvent["isInstanceOfEvent"].as<bool>()) {
++ currentEvent.call<void>("preventDefault");
++ currentEvent.call<void>("stopPropagation");
++ control->setCurrentEvent(emscripten::val::undefined());
++ }
++ }
++
++ emit awake();
++
++ if (!useAsyncify() && isMainThreadEventDispatcher())
++ handleNonAsyncifyErrorCases(flags);
++
++ bool didSendEvents = sendAllEvents(flags);
++
++ if (!isValidEventDispatcher())
++ return false;
++
++ if (m_interrupted) {
++ m_interrupted = false;
++ return false;
++ }
++
++ bool shouldWait = flags.testFlag(QEventLoop::WaitForMoreEvents);
++ if (!shouldWait || didSendEvents)
++ return didSendEvents;
++
++ processEventsWait();
++
++ return sendAllEvents(flags);
++ }
++
++ bool QEventDispatcherWasm::sendAllEvents(QEventLoop::ProcessEventsFlags flags)
++ {
++ bool didSendEvents = false;
++
++ didSendEvents |= sendPostedEvents();
++ if (!isValidEventDispatcher())
++ return false;
++
++ didSendEvents |= sendNativeEvents(flags);
++ if (!isValidEventDispatcher())
++ return false;
++
++ didSendEvents |= sendTimerEvents();
++ if (!isValidEventDispatcher())
++ return false;
++
++ return didSendEvents;
++ }
++
++ bool QEventDispatcherWasm::sendNativeEvents(QEventLoop::ProcessEventsFlags flags)
++ {
++ // TODO: support ExcludeUserInputEvents and ExcludeSocketNotifiers
++
++ // Secondary threads do not support native events
++ if (!isMainThreadEventDispatcher())
++ return false;
++
++ // Can't suspend without asyncify
++ if (!useAsyncify())
++ return false;
++
++ // Send any pending events, and
++ int sentEventCount = 0;
++ sentEventCount += g_mainThreadSuspendResumeControl->sendPendingEvents();
++
++ // if the processEvents() call is made from an exec() call then we assume
++ // that the main thread has just resumed, and that it will suspend again
++ // at the end of processEvents(). This makes the suspend loop below superfluous.
++ if (flags.testFlag(QEventLoop::EventLoopExec))
++ return sentEventCount > 0;
++
++ // Run a suspend-resume loop until all pending native events have
++ // been processed. Suspending returns control to the browsers'event
++ // loop and makes it process events. If any event was for us then
++ // the wasm instance will resume (via event handling code in QWasmSuspendResumeControl
++ // and process the event.
++ //
++ // Set a zero-timer to exit the loop via the m_wakeFromSuspendTimer flag.
++ // This timer will be added to the end of the native event queue and
++ // ensures that all pending (at the time of this sendNativeEvents() call)
++ // native events are processed.
++ m_wakeFromSuspendTimer = false;
++ do {
++ m_suspendTimer->setTimeout(0ms);
++ g_mainThreadSuspendResumeControl->suspend();
++ QScopedValueRollback scoped(m_isSendingNativeEvents, true);
++ sentEventCount += g_mainThreadSuspendResumeControl->sendPendingEvents();
++ } while (!m_wakeFromSuspendTimer);
++
++ return sentEventCount > 1; // Don't count m_suspendTimer
++ }
++
++ bool QEventDispatcherWasm::sendPostedEvents()
++ {
++ QCoreApplication::sendPostedEvents();
++
++ // QCoreApplication::sendPostedEvents() returns void and does not tell us
++ // if it actually did send events. Use the wakeUp() state instead:
++ // QCoreApplication::postEvent() calls wakeUp(), so if wakeUp() was
++ // called there is a chance there was a posted event. This should never
++ // return false if a posted event was sent, but might return true also
++ // if there was no event sent.
++ bool didWakeup = m_wakeup;
++ m_wakeup = false;
++ return didWakeup;
++ }
++
++ bool QEventDispatcherWasm::sendTimerEvents()
++ {
++ int activatedTimers = m_timerInfo->activateTimers();
++ if (activatedTimers > 0)
++ updateNativeTimer();
++ return activatedTimers > 0;
++ }
++
++ void QEventDispatcherWasm::registerTimer(Qt::TimerId timerId, Duration interval, Qt::TimerType timerType, QObject *object)
++ {
++ #ifndef QT_NO_DEBUG
++ if (qToUnderlying(timerId) < 1 || interval < 0ns || !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" << int(timerId) << interval << timerType << object;
++
++ m_timerInfo->registerTimer(timerId, interval, timerType, object);
++ updateNativeTimer();
++ }
++
++ bool QEventDispatcherWasm::unregisterTimer(Qt::TimerId timerId)
++ {
++ #ifndef QT_NO_DEBUG
++ if (qToUnderlying(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" << int(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<QAbstractEventDispatcher::TimerInfoV2>
++ QEventDispatcherWasm::timersForObject(QObject *object) const
++ {
++ #ifndef QT_NO_DEBUG
++ if (!object) {
++ qWarning("QEventDispatcherWasm:registeredTimers: invalid argument");
++ return {};
++ }
++ #endif
++
++ return m_timerInfo->registeredTimers(object);
++ }
++
++ QEventDispatcherWasm::Duration QEventDispatcherWasm::remainingTime(Qt::TimerId timerId) const
++ {
++ return m_timerInfo->remainingDuration(timerId);
++ }
++
++ void QEventDispatcherWasm::interrupt()
++ {
++ m_interrupted = true;
++ wakeUp();
++ }
++
++ void QEventDispatcherWasm::wakeUp()
++ {
++ m_wakeup = true;
++ #if QT_CONFIG(thread)
++ if (isSecondaryThreadEventDispatcher()) {
++ std::lock_guard<std::mutex> lock(m_mutex);
++ m_wakeUpCalled = true;
++ m_moreEvents.notify_one();
++ } else
++ #endif
++ {
++ QEventDispatcherWasm *eventDispatcher = this;
++ qwasmglobal::runOnMainThreadAsync([eventDispatcher]() {
++ if (isValidEventDispatcherPointer(eventDispatcher)) {
++ if (!eventDispatcher->m_wakeupTimer->hasTimeout())
++ eventDispatcher->m_wakeupTimer->setTimeout(0ms);
++ }
++ });
++ }
++ }
++
++ void QEventDispatcherWasm::handleNonAsyncifyErrorCases(QEventLoop::ProcessEventsFlags flags)
++ {
++ Q_ASSERT(!useAsyncify());
++
++ if (flags & QEventLoop::ApplicationExec) {
++ // Start the main loop, and then stop it on the first callback. This
++ // is done for the "simulateInfiniteLoop" functionality where
++ // emscripten_set_main_loop() throws a JS exception which returns
++ // control to the browser while preserving the C++ stack.
++ const bool simulateInfiniteLoop = true;
++ emscripten_set_main_loop([](){
++ emscripten_pause_main_loop();
++ }, 0, simulateInfiniteLoop);
++ } else if (flags & QEventLoop::DialogExec) {
++ qFatal() << "Calling exec() is not supported on Qt for WebAssembly in this configuration. Please build"
++ << "with asyncify support, or use an asynchronous API like QDialog::open()";
++ } else if (flags & QEventLoop::WaitForMoreEvents) {
++ qFatal("QEventLoop::WaitForMoreEvents is not supported on the main thread without asyncify");
++ }
++ }
++
++ // Blocks or suspends the current thread for the given amount of time.
++ // The event dispatcher does not process events while blocked. TODO:
++ // make it not process events while blocked.
++ bool QEventDispatcherWasm::wait(int timeout)
++ {
++ auto tim = timeout > 0 ? std::optional<std::chrono::milliseconds>(timeout) : std::nullopt;
++ if (isSecondaryThreadEventDispatcher())
++ return secondaryThreadWait(tim);
++ if (useAsyncify())
++ asyncifyWait(tim);
++ return true;
++ }
++
++ // Waits for more events by blocking or suspending the current thread. Should be called from
++ // processEvents() only.
++ void QEventDispatcherWasm::processEventsWait()
++ {
++ if (isMainThreadEventDispatcher()) {
++ asyncifyWait(std::nullopt);
++ } else {
++ auto nanoWait = m_timerInfo->timerWait();
++ std::optional<std::chrono::milliseconds> milliWait;
++ if (nanoWait.has_value())
++ milliWait = std::chrono::duration_cast<std::chrono::milliseconds>(*nanoWait);
++ secondaryThreadWait(milliWait);
++ }
++ }
++
++ void QEventDispatcherWasm::asyncifyWait(std::optional<std::chrono::milliseconds> timeout)
++ {
++ Q_ASSERT(emscripten_is_main_runtime_thread());
++ Q_ASSERT(isMainThreadEventDispatcher());
++ Q_ASSERT(useAsyncify());
++ if (timeout.has_value())
++ m_suspendTimer->setTimeout(timeout.value());
++ g_mainThreadSuspendResumeControl->suspend();
++ }
++
++ bool QEventDispatcherWasm::secondaryThreadWait(std::optional<std::chrono::milliseconds> timeout)
++ {
++ #if QT_CONFIG(thread)
++ Q_ASSERT(QThread::currentThread() == thread());
++ using namespace std::chrono_literals;
++ std::unique_lock<std::mutex> lock(m_mutex);
++
++ // 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) {
++ m_wakeUpCalled = false;
++ return true;
++ }
++
++ auto waitTime = timeout.value_or(std::chrono::milliseconds::max());
++ bool wakeUpCalled = m_moreEvents.wait_for(lock, waitTime, [this] { return m_wakeUpCalled; });
++ m_wakeUpCalled = false;
++ return wakeUpCalled;
++ #else
++ Q_UNREACHABLE();
++ return false;
++ #endif
++ }
++
++ void QEventDispatcherWasm::onTimer()
++ {
++ Q_ASSERT(emscripten_is_main_runtime_thread());
++ if (!g_mainThreadEventDispatcher)
++ return;
++
++ // If asyncify is in use then instance will resume and process timers
++ // in processEvents()
++ if (useAsyncify())
++ return;
++
++ g_mainThreadEventDispatcher->sendTimerEvents();
++ }
++
++ void QEventDispatcherWasm::onWakeup()
++ {
++ Q_ASSERT(emscripten_is_main_runtime_thread());
++ if (!g_mainThreadEventDispatcher)
++ return;
++
++ // In the case where we are suspending from sendNativeEvents() we don't want
++ // to call processEvents() again, since we are then already in processEvents()
++ // and are already awake.
++ if (g_mainThreadEventDispatcher->m_isSendingNativeEvents)
++ return;
++
++ g_mainThreadEventDispatcher->processEvents(QEventLoop::AllEvents);
++ }
++
++ void QEventDispatcherWasm::onProcessNativeEventsResume()
++ {
++ Q_ASSERT(emscripten_is_main_runtime_thread());
++ if (!g_mainThreadEventDispatcher)
++ return;
++ g_mainThreadEventDispatcher->m_wakeFromSuspendTimer = true;
++ }
++
++ // Updates the native timer based on currently registered Qt timers,
++ // by setting a timeout equivalent to the shortest timer.
++ // Must be called on the event dispatcher thread.
++ void QEventDispatcherWasm::updateNativeTimer()
++ {
++ #if QT_CONFIG(thread)
++ Q_ASSERT(QThread::currentThread() == thread());
++ #endif
++
++ // On secondary threads, the timeout is managed by setting the WaitForMoreEvents
++ // timeout in processEventsWait().
++ if (!isMainThreadEventDispatcher())
++ return;
++
++ // Clear any timer if there are no active timers
++ const std::optional<std::chrono::nanoseconds> nanoWait = m_timerInfo->timerWait();
++ if (!nanoWait.has_value()) {
++ m_nativeTimer->clearTimeout();
++ return;
++ }
++
++ auto milliWait = std::chrono::duration_cast<std::chrono::milliseconds>(*nanoWait);
++ const auto newTargetTime = m_timerInfo->currentTime + milliWait;
++
++ // Keep existing timer if the timeout has not changed.
++ if (m_nativeTimer->hasTimeout() && newTargetTime == m_timerTargetTime)
++ return;
++
++ // Clear current and set new timer
++ qCDebug(lcEventDispatcherTimers)
++ << "Created new native timer timeout" << milliWait.count() << "ms"
++ << "previous target time" << m_timerTargetTime.time_since_epoch()
++ << "new target time" << newTargetTime.time_since_epoch();
++ m_nativeTimer->clearTimeout();
++ m_nativeTimer->setTimeout(milliWait);
++ m_timerTargetTime = newTargetTime;
++ }
++
++ namespace {
++ int g_startupTasks = 0;
++ }
++
++ void QEventDispatcherWasm::registerSocketNotifier(QSocketNotifier *notifier)
++ {
++ QWasmSocket::registerSocketNotifier(notifier);
++ }
++
++ void QEventDispatcherWasm::unregisterSocketNotifier(QSocketNotifier *notifier)
++ {
++ QWasmSocket::unregisterSocketNotifier(notifier);
++ }
++
++ 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;
++ }
++
++ QWasmSocket::waitForSocketState(eventDispatcher, timeout, socket, waitForRead, waitForWrite,
++ selectForRead, selectForWrite, socketDisconnect);
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_qeventdispatcher_wasm_p.cpp"