wasm: add new event dispatcher implementation

Add QEventDispatcherWasm to QtCore. The event dispatcher
supports managing event queue wakeups and timers, both
for the main thread or for secondary threads.

Blocking in processEvents() (using QEventLoop::WaitForMoreEvents)
is supported when running on a secondary thread, or
on the main thread when Qt is built with Emscripten’s
asyncify support.

Code is shared for all both modes as far as possible,
with breakout functions which handle main and secondary
thread as well as asyncify specifics,. Some functions
like wakeUp() can be called from any thread, and needs
to take the calling thread into consideration as well.

The current asyncify implementation in Emscripten is
restricted to one level of suspend, and this restriction
carries over to Qt as well. In practice this means we
support one level of exec()-like API.

Note that this commit does not _enable_ use of the
new event dispatcher. This will be done in separate
commits.

Task-number: QTBUG-76007
Task-number: QTBUG-64020
Change-Id: I77dc9ba34bcff59ef05dd23a46dbf1873cbe6780
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Morten Johan Sørvig 2019-09-09 13:04:27 +02:00
parent 82f14a95b5
commit ecb92aacab
4 changed files with 674 additions and 0 deletions

View File

@ -22,6 +22,9 @@ load(emcc_ver)
# (with "wasm validation error: too many locals" type errors) if optimizations
# are omitted. Enable optimizations also for debug builds.
QMAKE_LFLAGS_DEBUG += -Os
# Declare all async functions
QMAKE_LFLAGS += -s \'ASYNCIFY_IMPORTS=[\"qt_asyncify_suspend_js\", \"qt_asyncify_resume_js\", \"emscripten_sleep\"]\'
}
}

View File

@ -1289,6 +1289,7 @@ endif()
qt_internal_extend_target(Core CONDITION WASM
SOURCES
platform/wasm/qstdweb.cpp platform/wasm/qstdweb_p.h
kernel/qeventdispatcher_wasm.cpp kernel/qeventdispatcher_wasm_p.h
)
# special case begin

View File

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

View File

@ -0,0 +1,125 @@
/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef QEVENTDISPATCHER_WASM_P_H
#define QEVENTDISPATCHER_WASM_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qabstracteventdispatcher.h"
#include "private/qtimerinfo_unix_p.h"
#include <QtCore/qloggingcategory.h>
#include <QtCore/qwaitcondition.h>
#include <mutex>
#include <optional>
#include <tuple>
QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(lcEventDispatcher);
Q_DECLARE_LOGGING_CATEGORY(lcEventDispatcherTimers)
class Q_CORE_EXPORT QEventDispatcherWasm : public QAbstractEventDispatcher
{
Q_OBJECT
public:
QEventDispatcherWasm();
~QEventDispatcherWasm();
bool processEvents(QEventLoop::ProcessEventsFlags flags) override;
void registerSocketNotifier(QSocketNotifier *notifier) override;
void unregisterSocketNotifier(QSocketNotifier *notifier) override;
void registerTimer(int timerId, qint64 interval, Qt::TimerType timerType, QObject *object) override;
bool unregisterTimer(int timerId) override;
bool unregisterTimers(QObject *object) override;
QList<QAbstractEventDispatcher::TimerInfo> registeredTimers(QObject *object) const override;
int remainingTime(int timerId) override;
void interrupt() override;
void wakeUp() override;
private:
bool isMainThreadEventDispatcher();
bool isSecondaryThreadEventDispatcher();
void pollForNativeEvents();
bool waitForForEvents();
static void callProcessEvents(void *eventDispatcher);
void processTimers();
void updateNativeTimer();
static void callProcessTimers(void *eventDispatcher);
#if QT_CONFIG(thread)
void runOnMainThread(std::function<void(void)> fn);
#endif
static QEventDispatcherWasm *g_mainThreadEventDispatcher;
bool m_interrupted = false;
bool m_processTimers = false;
bool m_pendingProcessEvents = false;
QTimerInfoList *m_timerInfo = new QTimerInfoList();
long m_timerId = 0;
uint64_t m_timerTargetTime = 0;
#if QT_CONFIG(thread)
std::mutex m_mutex;
bool m_wakeUpCalled = false;
std::condition_variable m_moreEvents;
static QVector<QEventDispatcherWasm *> g_secondaryThreadEventDispatchers;
static std::mutex g_secondaryThreadEventDispatchersMutex;
#endif
};
#endif // QEVENTDISPATCHER_WASM_P_H