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