Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qeventdispatcher_wasm.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
7#include <QtCore/qcoreapplication.h>
8#include <QtCore/qthread.h>
9#include <QtCore/qscopedvaluerollback.h>
10#include <QtCore/private/qobject_p.h>
11#include <QtCore/private/qwasmglobal_p.h>
12#include <QtCore/private/qstdweb_p.h>
13#include <QtCore/private/qwasmsocket_p.h>
14
15using namespace std::chrono;
16using namespace std::chrono_literals;
17
18QT_BEGIN_NAMESPACE
19
20using emscripten::val;
21
22Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher");
23Q_LOGGING_CATEGORY(lcEventDispatcherTimers, "qt.eventdispatcher.timers");
24
25#if QT_CONFIG(thread)
26#define LOCK_GUARD(M) std::lock_guard<std::mutex> lock(M)
27#else
28#define LOCK_GUARD(M)
29#endif
30
31#if defined(QT_STATIC)
32
33static bool useAsyncify()
34{
35 return qstdweb::haveAsyncify();
36}
37
38#else
39
40// EM_JS is not supported for side modules; disable asyncify
41
42static bool useAsyncify()
43{
44 return false;
45}
46
47#endif // defined(QT_STATIC)
48
49Q_CONSTINIT QEventDispatcherWasm *QEventDispatcherWasm::g_mainThreadEventDispatcher = nullptr;
50Q_CONSTINIT std::shared_ptr<QWasmSuspendResumeControl> QEventDispatcherWasm::g_customMainThreadSuspendResumeControl;
51Q_CONSTINIT QWasmSuspendResumeControl *QEventDispatcherWasm::g_mainThreadSuspendResumeControl = nullptr;
52
53#if QT_CONFIG(thread)
54Q_CONSTINIT QVector<QEventDispatcherWasm *> QEventDispatcherWasm::g_secondaryThreadEventDispatchers;
55Q_CONSTINIT std::mutex QEventDispatcherWasm::g_staticDataMutex;
56#endif
57
58QEventDispatcherWasm::QEventDispatcherWasm(std::shared_ptr<QWasmSuspendResumeControl> suspendResumeControl)
59{
60 // QEventDispatcherWasm operates in two main modes:
61 // - On the main thread:
62 // The event dispatcher can process native events but can't
63 // block and wait for new events, unless asyncify is used.
64 // - On a secondary thread:
65 // The event dispatcher can't process native events but can
66 // block and wait for new events.
67 //
68 // Which mode is determined by the calling thread: construct
69 // the event dispatcher object on the thread where it will live.
70
71 qCDebug(lcEventDispatcher) << "Creating QEventDispatcherWasm instance" << this
72 << "is main thread" << emscripten_is_main_runtime_thread();
73
74 if (emscripten_is_main_runtime_thread()) {
75 // There can be only one main thread event dispatcher at a time; in
76 // addition the main instance is used by the secondary thread event
77 // dispatchers so we set a global pointer to it.
78 Q_ASSERT(g_mainThreadEventDispatcher == nullptr);
79 g_mainThreadEventDispatcher = this;
80
81 if (suspendResumeControl) {
82 g_customMainThreadSuspendResumeControl = suspendResumeControl;
83 g_mainThreadSuspendResumeControl = g_customMainThreadSuspendResumeControl.get();
84 } else {
85 g_mainThreadSuspendResumeControl = QWasmSuspendResumeControl::get();
86 }
87
88 // Zero-timer used on wake() calls
89 m_wakeupTimer = std::make_unique<QWasmTimer>(g_mainThreadSuspendResumeControl, [](){ onWakeup(); });
90
91 // Timer set to fire at the next Qt timer timeout
92 m_nativeTimer = std::make_unique<QWasmTimer>(g_mainThreadSuspendResumeControl, []() { onTimer(); });
93
94 // Timer used when suspending to process native events
95 m_suspendTimer = std::make_unique<QWasmTimer>(g_mainThreadSuspendResumeControl, []() { onProcessNativeEventsResume(); });
96 } else {
97#if QT_CONFIG(thread)
98 std::lock_guard<std::mutex> lock(g_staticDataMutex);
99 g_secondaryThreadEventDispatchers.append(this);
100#endif
101 }
102
103 m_timerInfo = std::make_unique<QTimerInfoList>();
104}
105
106QEventDispatcherWasm::~QEventDispatcherWasm()
107{
108 qCDebug(lcEventDispatcher) << "Destroying QEventDispatcherWasm instance" << this;
109
110 // Reset to ensure destruction before g_mainThreadSuspendResumeControl
111 m_wakeupTimer.reset();
112 m_nativeTimer.reset();
113 m_suspendTimer.reset();
114
115#if QT_CONFIG(thread)
116 if (isSecondaryThreadEventDispatcher()) {
117 std::lock_guard<std::mutex> lock(g_staticDataMutex);
118 g_secondaryThreadEventDispatchers.remove(g_secondaryThreadEventDispatchers.indexOf(this));
119 } else
120#endif
121 {
122 QWasmSocket::clearSocketNotifiers();
123 g_mainThreadEventDispatcher = nullptr;
124 g_customMainThreadSuspendResumeControl.reset();
125 g_mainThreadSuspendResumeControl = nullptr;
126 }
127}
128
129bool QEventDispatcherWasm::isMainThreadEventDispatcher()
130{
131 return this == g_mainThreadEventDispatcher;
132}
133
134bool QEventDispatcherWasm::isSecondaryThreadEventDispatcher()
135{
136 return this != g_mainThreadEventDispatcher;
137}
138
139bool QEventDispatcherWasm::isValidEventDispatcher()
140{
141 return isValidEventDispatcherPointer(this);
142}
143
144bool QEventDispatcherWasm::isValidEventDispatcherPointer(QEventDispatcherWasm *eventDispatcher)
145{
146 if (eventDispatcher == g_mainThreadEventDispatcher)
147 return true;
148#if QT_CONFIG(thread)
149 if (g_secondaryThreadEventDispatchers.contains(eventDispatcher))
150 return true;
151#endif
152 return false;
153}
154
155bool QEventDispatcherWasm::processEvents(QEventLoop::ProcessEventsFlags flags)
156{
157 // Accept the current event if event handling recurses / event loop is reentered,
158 // to prevent the browser from propagating it to other event handlers.
159 if (useAsyncify() && isMainThreadEventDispatcher()) {
160 auto control = QWasmSuspendResumeControl::get();
161 auto currentEvent = control->currentEvent();
162 if (!currentEvent.isUndefined() &&
163 !currentEvent["isInstanceOfEvent"].isUndefined() &&
164 !currentEvent["isInstanceOfEvent"].isNull() &&
165 currentEvent["isInstanceOfEvent"].as<bool>()) {
166 currentEvent.call<void>("preventDefault");
167 currentEvent.call<void>("stopPropagation");
168 control->setCurrentEvent(emscripten::val::undefined());
169 }
170 }
171
172 emit awake();
173
174 if (!useAsyncify() && isMainThreadEventDispatcher())
175 handleNonAsyncifyErrorCases(flags);
176
177 bool didSendEvents = sendAllEvents(flags);
178
179 if (!isValidEventDispatcher())
180 return false;
181
182 if (m_interrupted) {
183 m_interrupted = false;
184 return false;
185 }
186
187 bool shouldWait = flags.testFlag(QEventLoop::WaitForMoreEvents);
188 if (!shouldWait || didSendEvents)
189 return didSendEvents;
190
191 processEventsWait();
192
193 return sendAllEvents(flags);
194}
195
196bool QEventDispatcherWasm::sendAllEvents(QEventLoop::ProcessEventsFlags flags)
197{
198 bool didSendEvents = false;
199
200 didSendEvents |= sendPostedEvents();
201 if (!isValidEventDispatcher())
202 return false;
203
204 didSendEvents |= sendNativeEvents(flags);
205 if (!isValidEventDispatcher())
206 return false;
207
208 didSendEvents |= sendTimerEvents();
209 if (!isValidEventDispatcher())
210 return false;
211
212 return didSendEvents;
213}
214
215bool QEventDispatcherWasm::sendNativeEvents(QEventLoop::ProcessEventsFlags flags)
216{
217 // TODO: support ExcludeUserInputEvents and ExcludeSocketNotifiers
218
219 // Secondary threads do not support native events
220 if (!isMainThreadEventDispatcher())
221 return false;
222
223 // Can't suspend without asyncify
224 if (!useAsyncify())
225 return false;
226
227 // Send any pending events, and
228 int sentEventCount = 0;
229 sentEventCount += g_mainThreadSuspendResumeControl->sendPendingEvents();
230
231 // if the processEvents() call is made from an exec() call then we assume
232 // that the main thread has just resumed, and that it will suspend again
233 // at the end of processEvents(). This makes the suspend loop below superfluous.
234 if (flags.testFlag(QEventLoop::EventLoopExec))
235 return sentEventCount > 0;
236
237 // Run a suspend-resume loop until all pending native events have
238 // been processed. Suspending returns control to the browsers'event
239 // loop and makes it process events. If any event was for us then
240 // the wasm instance will resume (via event handling code in QWasmSuspendResumeControl
241 // and process the event.
242 //
243 // Set a zero-timer to exit the loop via the m_wakeFromSuspendTimer flag.
244 // This timer will be added to the end of the native event queue and
245 // ensures that all pending (at the time of this sendNativeEvents() call)
246 // native events are processed.
247 m_wakeFromSuspendTimer = false;
248 do {
249 m_suspendTimer->setTimeout(0ms);
250 g_mainThreadSuspendResumeControl->suspend();
251 QScopedValueRollback scoped(m_isSendingNativeEvents, true);
252 sentEventCount += g_mainThreadSuspendResumeControl->sendPendingEvents();
253 } while (!m_wakeFromSuspendTimer);
254
255 return sentEventCount > 1; // Don't count m_suspendTimer
256}
257
258bool QEventDispatcherWasm::sendPostedEvents()
259{
260 QCoreApplication::sendPostedEvents();
261
262 // QCoreApplication::sendPostedEvents() returns void and does not tell us
263 // if it actually did send events. Use the wakeUp() state instead:
264 // QCoreApplication::postEvent() calls wakeUp(), so if wakeUp() was
265 // called there is a chance there was a posted event. This should never
266 // return false if a posted event was sent, but might return true also
267 // if there was no event sent.
268 bool didWakeup = m_wakeup;
269 m_wakeup = false;
270 return didWakeup;
271}
272
273bool QEventDispatcherWasm::sendTimerEvents()
274{
275 int activatedTimers = m_timerInfo->activateTimers();
276 if (activatedTimers > 0)
277 updateNativeTimer();
278 return activatedTimers > 0;
279}
280
281void QEventDispatcherWasm::registerTimer(Qt::TimerId timerId, Duration interval, Qt::TimerType timerType, QObject *object)
282{
283#ifndef QT_NO_DEBUG
284 if (qToUnderlying(timerId) < 1 || interval < 0ns || !object) {
285 qWarning("QEventDispatcherWasm::registerTimer: invalid arguments");
286 return;
287 } else if (object->thread() != thread() || thread() != QThread::currentThread()) {
288 qWarning("QEventDispatcherWasm::registerTimer: timers cannot be started from another "
289 "thread");
290 return;
291 }
292#endif
293 qCDebug(lcEventDispatcherTimers) << "registerTimer" << int(timerId) << interval << timerType << object;
294
295 m_timerInfo->registerTimer(timerId, interval, timerType, object);
296 updateNativeTimer();
297}
298
299bool QEventDispatcherWasm::unregisterTimer(Qt::TimerId timerId)
300{
301#ifndef QT_NO_DEBUG
302 if (qToUnderlying(timerId) < 1) {
303 qWarning("QEventDispatcherWasm::unregisterTimer: invalid argument");
304 return false;
305 } else if (thread() != QThread::currentThread()) {
306 qWarning("QEventDispatcherWasm::unregisterTimer: timers cannot be stopped from another "
307 "thread");
308 return false;
309 }
310#endif
311
312 qCDebug(lcEventDispatcherTimers) << "unregisterTimer" << int(timerId);
313
314 bool ans = m_timerInfo->unregisterTimer(timerId);
315 updateNativeTimer();
316 return ans;
317}
318
319bool QEventDispatcherWasm::unregisterTimers(QObject *object)
320{
321#ifndef QT_NO_DEBUG
322 if (!object) {
323 qWarning("QEventDispatcherWasm::unregisterTimers: invalid argument");
324 return false;
325 } else if (object->thread() != thread() || thread() != QThread::currentThread()) {
326 qWarning("QEventDispatcherWasm::unregisterTimers: timers cannot be stopped from another "
327 "thread");
328 return false;
329 }
330#endif
331
332 qCDebug(lcEventDispatcherTimers) << "registerTimer" << object;
333
334 bool ans = m_timerInfo->unregisterTimers(object);
335 updateNativeTimer();
336 return ans;
337}
338
339QList<QAbstractEventDispatcher::TimerInfoV2>
340QEventDispatcherWasm::timersForObject(QObject *object) const
341{
342#ifndef QT_NO_DEBUG
343 if (!object) {
344 qWarning("QEventDispatcherWasm:registeredTimers: invalid argument");
345 return {};
346 }
347#endif
348
349 return m_timerInfo->registeredTimers(object);
350}
351
352QEventDispatcherWasm::Duration QEventDispatcherWasm::remainingTime(Qt::TimerId timerId) const
353{
354 return m_timerInfo->remainingDuration(timerId);
355}
356
357void QEventDispatcherWasm::interrupt()
358{
359 m_interrupted = true;
360 wakeUp();
361}
362
363void QEventDispatcherWasm::wakeUp()
364{
365 m_wakeup = true;
366#if QT_CONFIG(thread)
367 if (isSecondaryThreadEventDispatcher()) {
368 std::lock_guard<std::mutex> lock(m_mutex);
369 m_wakeUpCalled = true;
370 m_moreEvents.notify_one();
371 } else
372#endif
373 {
374 QEventDispatcherWasm *eventDispatcher = this;
375 qwasmglobal::runOnMainThreadAsync([eventDispatcher]() {
376 if (isValidEventDispatcherPointer(eventDispatcher)) {
377 if (!eventDispatcher->m_wakeupTimer->hasTimeout())
378 eventDispatcher->m_wakeupTimer->setTimeout(0ms);
379 }
380 });
381 }
382}
383
384void QEventDispatcherWasm::handleNonAsyncifyErrorCases(QEventLoop::ProcessEventsFlags flags)
385{
386 Q_ASSERT(!useAsyncify());
387
388 if (flags & QEventLoop::ApplicationExec) {
389 // Start the main loop, and then stop it on the first callback. This
390 // is done for the "simulateInfiniteLoop" functionality where
391 // emscripten_set_main_loop() throws a JS exception which returns
392 // control to the browser while preserving the C++ stack.
393 const bool simulateInfiniteLoop = true;
394 emscripten_set_main_loop([](){
395 emscripten_pause_main_loop();
396 }, 0, simulateInfiniteLoop);
397 } else if (flags & QEventLoop::DialogExec) {
398 qFatal() << "Calling exec() is not supported on Qt for WebAssembly in this configuration. Please build"
399 << "with asyncify support, or use an asynchronous API like QDialog::open()";
400 } else if (flags & QEventLoop::WaitForMoreEvents) {
401 qFatal("QEventLoop::WaitForMoreEvents is not supported on the main thread without asyncify");
402 }
403}
404
405// Blocks or suspends the current thread for the given amount of time.
406// The event dispatcher does not process events while blocked. TODO:
407// make it not process events while blocked.
408bool QEventDispatcherWasm::wait(int timeout)
409{
410 auto tim = timeout > 0 ? std::optional<std::chrono::milliseconds>(timeout) : std::nullopt;
411 if (isSecondaryThreadEventDispatcher())
412 return secondaryThreadWait(tim);
413 if (useAsyncify())
414 asyncifyWait(tim);
415 return true;
416}
417
418// Waits for more events by blocking or suspending the current thread. Should be called from
419// processEvents() only.
420void QEventDispatcherWasm::processEventsWait()
421{
422 if (isMainThreadEventDispatcher()) {
423 asyncifyWait(std::nullopt);
424 } else {
425 auto nanoWait = m_timerInfo->timerWait();
426 std::optional<std::chrono::milliseconds> milliWait;
427 if (nanoWait.has_value())
428 milliWait = std::chrono::duration_cast<std::chrono::milliseconds>(*nanoWait);
429 secondaryThreadWait(milliWait);
430 }
431}
432
433void QEventDispatcherWasm::asyncifyWait(std::optional<std::chrono::milliseconds> timeout)
434{
435 Q_ASSERT(emscripten_is_main_runtime_thread());
436 Q_ASSERT(isMainThreadEventDispatcher());
437 Q_ASSERT(useAsyncify());
438 if (timeout.has_value())
439 m_suspendTimer->setTimeout(timeout.value());
440 g_mainThreadSuspendResumeControl->suspend();
441}
442
443bool QEventDispatcherWasm::secondaryThreadWait(std::optional<std::chrono::milliseconds> timeout)
444{
445#if QT_CONFIG(thread)
446 Q_ASSERT(QThread::currentThread() == thread());
447 using namespace std::chrono_literals;
448 std::unique_lock<std::mutex> lock(m_mutex);
449
450 // If wakeUp() was called there might be pending events in the event
451 // queue which should be processed. Don't block, instead return
452 // so that the event loop can spin and call processEvents() again.
453 if (m_wakeUpCalled) {
454 m_wakeUpCalled = false;
455 return true;
456 }
457
458 auto waitTime = timeout.value_or(std::chrono::milliseconds::max());
459 bool wakeUpCalled = m_moreEvents.wait_for(lock, waitTime, [this] { return m_wakeUpCalled; });
460 m_wakeUpCalled = false;
461 return wakeUpCalled;
462#else
463 Q_UNREACHABLE();
464 return false;
465#endif
466}
467
468void QEventDispatcherWasm::onTimer()
469{
470 Q_ASSERT(emscripten_is_main_runtime_thread());
471 if (!g_mainThreadEventDispatcher)
472 return;
473
474 // If asyncify is in use then instance will resume and process timers
475 // in processEvents()
476 if (useAsyncify())
477 return;
478
479 g_mainThreadEventDispatcher->sendTimerEvents();
480}
481
482void QEventDispatcherWasm::onWakeup()
483{
484 Q_ASSERT(emscripten_is_main_runtime_thread());
485 if (!g_mainThreadEventDispatcher)
486 return;
487
488 // In the case where we are suspending from sendNativeEvents() we don't want
489 // to call processEvents() again, since we are then already in processEvents()
490 // and are already awake.
491 if (g_mainThreadEventDispatcher->m_isSendingNativeEvents)
492 return;
493
494 g_mainThreadEventDispatcher->processEvents(QEventLoop::AllEvents);
495}
496
497void QEventDispatcherWasm::onProcessNativeEventsResume()
498{
499 Q_ASSERT(emscripten_is_main_runtime_thread());
500 if (!g_mainThreadEventDispatcher)
501 return;
502 g_mainThreadEventDispatcher->m_wakeFromSuspendTimer = true;
503}
504
505// Updates the native timer based on currently registered Qt timers,
506// by setting a timeout equivalent to the shortest timer.
507// Must be called on the event dispatcher thread.
508void QEventDispatcherWasm::updateNativeTimer()
509{
510#if QT_CONFIG(thread)
511 Q_ASSERT(QThread::currentThread() == thread());
512#endif
513
514 // On secondary threads, the timeout is managed by setting the WaitForMoreEvents
515 // timeout in processEventsWait().
516 if (!isMainThreadEventDispatcher())
517 return;
518
519 // Clear any timer if there are no active timers
520 const std::optional<std::chrono::nanoseconds> nanoWait = m_timerInfo->timerWait();
521 if (!nanoWait.has_value()) {
522 m_nativeTimer->clearTimeout();
523 return;
524 }
525
526 auto milliWait = std::chrono::duration_cast<std::chrono::milliseconds>(*nanoWait);
527 const auto newTargetTime = m_timerInfo->currentTime + milliWait;
528
529 // Keep existing timer if the timeout has not changed.
530 if (m_nativeTimer->hasTimeout() && newTargetTime == m_timerTargetTime)
531 return;
532
533 // Clear current and set new timer
534 qCDebug(lcEventDispatcherTimers)
535 << "Created new native timer timeout" << milliWait.count() << "ms"
536 << "previous target time" << m_timerTargetTime.time_since_epoch()
537 << "new target time" << newTargetTime.time_since_epoch();
538 m_nativeTimer->clearTimeout();
539 m_nativeTimer->setTimeout(milliWait);
540 m_timerTargetTime = newTargetTime;
541}
542
543namespace {
544 int g_startupTasks = 0;
545}
546
547// The following functions manages sending the "qtLoaded" event/callback
548// from qtloader.js on startup, once Qt initialization has been completed
549// and the application is ready to display the first frame. This can be
550// either as soon as the event loop is running, or later, if additional
551// startup tasks (e.g. local font loading) have been registered.
552
553void QEventDispatcherWasm::registerStartupTask()
554{
555 ++g_startupTasks;
556}
557
558void QEventDispatcherWasm::completeStarupTask()
559{
560 --g_startupTasks;
561 callOnLoadedIfRequired();
562}
563
564void QEventDispatcherWasm::callOnLoadedIfRequired()
565{
566 if (g_startupTasks > 0)
567 return;
568
569 static bool qtLoadedCalled = false;
570 if (qtLoadedCalled)
571 return;
572 qtLoadedCalled = true;
573}
574
575void QEventDispatcherWasm::onLoaded()
576{
577 // TODO: call qtloader.js onLoaded from here, in order to delay
578 // hiding the "Loading..." message until the app is ready to paint
579 // the first frame. Currently onLoaded must be called early before
580 // main() in order to ensure that the screen/container elements
581 // have valid geometry at startup.
582}
583
584void QEventDispatcherWasm::registerSocketNotifier(QSocketNotifier *notifier)
585{
586 QWasmSocket::registerSocketNotifier(notifier);
587}
588
589void QEventDispatcherWasm::unregisterSocketNotifier(QSocketNotifier *notifier)
590{
591 QWasmSocket::unregisterSocketNotifier(notifier);
592}
593
594void QEventDispatcherWasm::socketSelect(int timeout, int socket, bool waitForRead, bool waitForWrite,
595 bool *selectForRead, bool *selectForWrite, bool *socketDisconnect)
596{
597 QEventDispatcherWasm *eventDispatcher = static_cast<QEventDispatcherWasm *>(
598 QAbstractEventDispatcher::instance(QThread::currentThread()));
599
600 if (!eventDispatcher) {
601 qWarning("QEventDispatcherWasm::socketSelect called without eventdispatcher instance");
602 return;
603 }
604
605 QWasmSocket::waitForSocketState(eventDispatcher, timeout, socket, waitForRead, waitForWrite,
606 selectForRead, selectForWrite, socketDisconnect);
607}
608
609QT_END_NAMESPACE
610
611#include "moc_qeventdispatcher_wasm_p.cpp"
static bool useAsyncify()
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")