summaryrefslogtreecommitdiffstats
path: root/src/corelib/kernel/qeventdispatcher_wasm.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/corelib/kernel/qeventdispatcher_wasm.cpp')
-rw-r--r--src/corelib/kernel/qeventdispatcher_wasm.cpp880
1 files changed, 646 insertions, 234 deletions
diff --git a/src/corelib/kernel/qeventdispatcher_wasm.cpp b/src/corelib/kernel/qeventdispatcher_wasm.cpp
index 5004d0079c..f4fcdbb8b2 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
@@ -109,36 +173,27 @@ 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 asynchronously woken.
-bool qt_asyncify_resume()
+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
}
@@ -176,14 +240,26 @@ QEventDispatcherWasm::~QEventDispatcherWasm()
#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,61 +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 (isMainThreadEventDispatcher()) {
if (flags & QEventLoop::DialogExec)
handleDialogExec();
- else if (flags & QEventLoop::EventLoopExec)
- handleEventLoopExec();
+ 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()) {
@@ -260,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()) {
@@ -279,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();
@@ -306,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()
@@ -332,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
@@ -378,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 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 (isSecondaryThreadEventDispatcher()) {
+ 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)
+ 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());
@@ -454,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 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()
@@ -490,47 +619,48 @@ 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
@@ -540,48 +670,330 @@ 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)
+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()
+{
+ emscripten::val qt = emscripten::val::module_property("qt");
+ if (qt.isUndefined())
+ return;
+ qt.call<void>("onLoaded");
+}
namespace {
void trampoline(void *context) {
- std::function<void(void)> *fn = reinterpret_cast<std::function<void(void)> *>(context);
- (*fn)();
- delete fn;
+
+ 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 on the main thread
+// Runs a function right away
+void QEventDispatcherWasm::run(std::function<void(void)> fn)
+{
+ fn();
+}
+
void QEventDispatcherWasm::runOnMainThread(std::function<void(void)> 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"