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 // Accept the current event if event handling recurses / event loop is reentered,
154 // to prevent the browser from propagating it to other event handlers.
155 if (useAsyncify() && isMainThreadEventDispatcher()) {
156 auto control = QWasmSuspendResumeControl::get();
157 auto currentEvent = control->currentEvent();
158 if (!currentEvent.isUndefined() &&
159 !currentEvent["isInstanceOfEvent"].isUndefined() &&
160 !currentEvent["isInstanceOfEvent"].isNull() &&
161 currentEvent["isInstanceOfEvent"].as<bool>()) {
162 currentEvent.call<void>("preventDefault");
163 currentEvent.call<void>("stopPropagation");
164 control->setCurrentEvent(emscripten::val::undefined());
165 }
166 }
167
168 emit awake();
169
170 if (!useAsyncify() && isMainThreadEventDispatcher())
171 handleNonAsyncifyErrorCases(flags);
172
173 bool didSendEvents = sendAllEvents(flags);
174
175 if (!isValidEventDispatcher())
176 return false;
177
178 if (m_interrupted) {
179 m_interrupted = false;
180 return false;
181 }
182
183 bool shouldWait = flags.testFlag(QEventLoop::WaitForMoreEvents);
184 if (!shouldWait || didSendEvents)
185 return didSendEvents;
186
187 processEventsWait();
188
189 return sendAllEvents(flags);
190}
191
192bool QEventDispatcherWasm::sendAllEvents(QEventLoop::ProcessEventsFlags flags)
193{
194 bool didSendEvents = false;
195
196 didSendEvents |= sendPostedEvents();
197 if (!isValidEventDispatcher())
198 return false;
199
200 didSendEvents |= sendNativeEvents(flags);
201 if (!isValidEventDispatcher())
202 return false;
203
204 didSendEvents |= sendTimerEvents();
205 if (!isValidEventDispatcher())
206 return false;
207
208 return didSendEvents;
209}
210
211bool QEventDispatcherWasm::sendNativeEvents(QEventLoop::ProcessEventsFlags flags)
212{
213 // TODO: support ExcludeUserInputEvents and ExcludeSocketNotifiers
214
215 // Secondary threads do not support native events
216 if (!isMainThreadEventDispatcher())
217 return false;
218
219 // Can't suspend without asyncify
220 if (!useAsyncify())
221 return false;
222
223 // Send any pending events, and
224 int sentEventCount = 0;
225 sentEventCount += g_mainThreadSuspendResumeControl->sendPendingEvents();
226
227 // if the processEvents() call is made from an exec() call then we assume
228 // that the main thread has just resumed, and that it will suspend again
229 // at the end of processEvents(). This makes the suspend loop below superfluous.
230 if (flags.testFlag(QEventLoop::EventLoopExec))
231 return sentEventCount > 0;
232
233 // Run a suspend-resume loop until all pending native events have
234 // been processed. Suspending returns control to the browsers'event
235 // loop and makes it process events. If any event was for us then
236 // the wasm instance will resume (via event handling code in QWasmSuspendResumeControl
237 // and process the event.
238 //
239 // Set a zero-timer to exit the loop via the m_wakeFromSuspendTimer flag.
240 // This timer will be added to the end of the native event queue and
241 // ensures that all pending (at the time of this sendNativeEvents() call)
242 // native events are processed.
243 m_wakeFromSuspendTimer = false;
244 do {
245 m_suspendTimer->setTimeout(0ms);
246 g_mainThreadSuspendResumeControl->suspend();
247 QScopedValueRollback scoped(m_isSendingNativeEvents, true);
248 sentEventCount += g_mainThreadSuspendResumeControl->sendPendingEvents();
249 } while (!m_wakeFromSuspendTimer);
250
251 return sentEventCount > 1; // Don't count m_suspendTimer
252}
253
254bool QEventDispatcherWasm::sendPostedEvents()
255{
256 QCoreApplication::sendPostedEvents();
257
258 // QCoreApplication::sendPostedEvents() returns void and does not tell us
259 // if it actually did send events. Use the wakeUp() state instead:
260 // QCoreApplication::postEvent() calls wakeUp(), so if wakeUp() was
261 // called there is a chance there was a posted event. This should never
262 // return false if a posted event was sent, but might return true also
263 // if there was no event sent.
264 bool didWakeup = m_wakeup;
265 m_wakeup = false;
266 return didWakeup;
267}
268
269bool QEventDispatcherWasm::sendTimerEvents()
270{
271 int activatedTimers = m_timerInfo->activateTimers();
272 if (activatedTimers > 0)
273 updateNativeTimer();
274 return activatedTimers > 0;
275}
276
277void QEventDispatcherWasm::registerTimer(Qt::TimerId timerId, Duration interval, Qt::TimerType timerType, QObject *object)
278{
279#ifndef QT_NO_DEBUG
280 if (qToUnderlying(timerId) < 1 || interval < 0ns || !object) {
281 qWarning("QEventDispatcherWasm::registerTimer: invalid arguments");
282 return;
283 } else if (object->thread() != thread() || thread() != QThread::currentThread()) {
284 qWarning("QEventDispatcherWasm::registerTimer: timers cannot be started from another "
285 "thread");
286 return;
287 }
288#endif
289 qCDebug(lcEventDispatcherTimers) << "registerTimer" << int(timerId) << interval << timerType << object;
290
291 m_timerInfo->registerTimer(timerId, interval, timerType, object);
292 updateNativeTimer();
293}
294
295bool QEventDispatcherWasm::unregisterTimer(Qt::TimerId timerId)
296{
297#ifndef QT_NO_DEBUG
298 if (qToUnderlying(timerId) < 1) {
299 qWarning("QEventDispatcherWasm::unregisterTimer: invalid argument");
300 return false;
301 } else if (thread() != QThread::currentThread()) {
302 qWarning("QEventDispatcherWasm::unregisterTimer: timers cannot be stopped from another "
303 "thread");
304 return false;
305 }
306#endif
307
308 qCDebug(lcEventDispatcherTimers) << "unregisterTimer" << int(timerId);
309
310 bool ans = m_timerInfo->unregisterTimer(timerId);
311 updateNativeTimer();
312 return ans;
313}
314
315bool QEventDispatcherWasm::unregisterTimers(QObject *object)
316{
317#ifndef QT_NO_DEBUG
318 if (!object) {
319 qWarning("QEventDispatcherWasm::unregisterTimers: invalid argument");
320 return false;
321 } else if (object->thread() != thread() || thread() != QThread::currentThread()) {
322 qWarning("QEventDispatcherWasm::unregisterTimers: timers cannot be stopped from another "
323 "thread");
324 return false;
325 }
326#endif
327
328 qCDebug(lcEventDispatcherTimers) << "registerTimer" << object;
329
330 bool ans = m_timerInfo->unregisterTimers(object);
331 updateNativeTimer();
332 return ans;
333}
334
335QList<QAbstractEventDispatcher::TimerInfoV2>
336QEventDispatcherWasm::timersForObject(QObject *object) const
337{
338#ifndef QT_NO_DEBUG
339 if (!object) {
340 qWarning("QEventDispatcherWasm:registeredTimers: invalid argument");
341 return {};
342 }
343#endif
344
345 return m_timerInfo->registeredTimers(object);
346}
347
348QEventDispatcherWasm::Duration QEventDispatcherWasm::remainingTime(Qt::TimerId timerId) const
349{
350 return m_timerInfo->remainingDuration(timerId);
351}
352
353void QEventDispatcherWasm::interrupt()
354{
355 m_interrupted = true;
356 wakeUp();
357}
358
359void QEventDispatcherWasm::wakeUp()
360{
361 m_wakeup = true;
362#if QT_CONFIG(thread)
363 if (isSecondaryThreadEventDispatcher()) {
364 std::lock_guard<std::mutex> lock(m_mutex);
365 m_wakeUpCalled = true;
366 m_moreEvents.notify_one();
367 } else
368#endif
369 {
370 QEventDispatcherWasm *eventDispatcher = this;
371 qwasmglobal::runOnMainThreadAsync([eventDispatcher]() {
372 if (isValidEventDispatcherPointer(eventDispatcher)) {
373 if (!eventDispatcher->m_wakeupTimer->hasTimeout())
374 eventDispatcher->m_wakeupTimer->setTimeout(0ms);
375 }
376 });
377 }
378}
379
380void QEventDispatcherWasm::handleNonAsyncifyErrorCases(QEventLoop::ProcessEventsFlags flags)
381{
382 Q_ASSERT(!useAsyncify());
383
384 if (flags & QEventLoop::ApplicationExec) {
385 // Start the main loop, and then stop it on the first callback. This
386 // is done for the "simulateInfiniteLoop" functionality where
387 // emscripten_set_main_loop() throws a JS exception which returns
388 // control to the browser while preserving the C++ stack.
389 const bool simulateInfiniteLoop = true;
390 emscripten_set_main_loop([](){
391 emscripten_pause_main_loop();
392 }, 0, simulateInfiniteLoop);
393 } else if (flags & QEventLoop::DialogExec) {
394 qFatal() << "Calling exec() is not supported on Qt for WebAssembly in this configuration. Please build"
395 << "with asyncify support, or use an asynchronous API like QDialog::open()";
396 } else if (flags & QEventLoop::WaitForMoreEvents) {
397 qFatal("QEventLoop::WaitForMoreEvents is not supported on the main thread without asyncify");
398 }
399}
400
401// Blocks or suspends the current thread for the given amount of time.
402// The event dispatcher does not process events while blocked. TODO:
403// make it not process events while blocked.
404bool QEventDispatcherWasm::wait(int timeout)
405{
406 auto tim = timeout > 0 ? std::optional<std::chrono::milliseconds>(timeout) : std::nullopt;
407 if (isSecondaryThreadEventDispatcher())
408 return secondaryThreadWait(tim);
409 if (useAsyncify())
410 asyncifyWait(tim);
411 return true;
412}
413
414// Waits for more events by blocking or suspending the current thread. Should be called from
415// processEvents() only.
416void QEventDispatcherWasm::processEventsWait()
417{
418 if (isMainThreadEventDispatcher()) {
419 asyncifyWait(std::nullopt);
420 } else {
421 auto nanoWait = m_timerInfo->timerWait();
422 std::optional<std::chrono::milliseconds> milliWait;
423 if (nanoWait.has_value())
424 milliWait = std::chrono::duration_cast<std::chrono::milliseconds>(*nanoWait);
425 secondaryThreadWait(milliWait);
426 }
427}
428
429void QEventDispatcherWasm::asyncifyWait(std::optional<std::chrono::milliseconds> timeout)
430{
431 Q_ASSERT(emscripten_is_main_runtime_thread());
432 Q_ASSERT(isMainThreadEventDispatcher());
433 Q_ASSERT(useAsyncify());
434 if (timeout.has_value())
435 m_suspendTimer->setTimeout(timeout.value());
436 g_mainThreadSuspendResumeControl->suspend();
437}
438
439bool QEventDispatcherWasm::secondaryThreadWait(std::optional<std::chrono::milliseconds> timeout)
440{
441#if QT_CONFIG(thread)
442 Q_ASSERT(QThread::currentThread() == thread());
443 using namespace std::chrono_literals;
444 std::unique_lock<std::mutex> lock(m_mutex);
445
446 // If wakeUp() was called there might be pending events in the event
447 // queue which should be processed. Don't block, instead return
448 // so that the event loop can spin and call processEvents() again.
449 if (m_wakeUpCalled) {
450 m_wakeUpCalled = false;
451 return true;
452 }
453
454 auto waitTime = timeout.value_or(std::chrono::milliseconds::max());
455 bool wakeUpCalled = m_moreEvents.wait_for(lock, waitTime, [this] { return m_wakeUpCalled; });
456 m_wakeUpCalled = false;
457 return wakeUpCalled;
458#else
459 Q_UNREACHABLE();
460 return false;
461#endif
462}
463
464void QEventDispatcherWasm::onTimer()
465{
466 Q_ASSERT(emscripten_is_main_runtime_thread());
467 if (!g_mainThreadEventDispatcher)
468 return;
469
470 // If asyncify is in use then instance will resume and process timers
471 // in processEvents()
472 if (useAsyncify())
473 return;
474
475 g_mainThreadEventDispatcher->sendTimerEvents();
476}
477
478void QEventDispatcherWasm::onWakeup()
479{
480 Q_ASSERT(emscripten_is_main_runtime_thread());
481 if (!g_mainThreadEventDispatcher)
482 return;
483
484 // In the case where we are suspending from sendNativeEvents() we don't want
485 // to call processEvents() again, since we are then already in processEvents()
486 // and are already awake.
487 if (g_mainThreadEventDispatcher->m_isSendingNativeEvents)
488 return;
489
490 g_mainThreadEventDispatcher->processEvents(QEventLoop::AllEvents);
491}
492
493void QEventDispatcherWasm::onProcessNativeEventsResume()
494{
495 Q_ASSERT(emscripten_is_main_runtime_thread());
496 if (!g_mainThreadEventDispatcher)
497 return;
498 g_mainThreadEventDispatcher->m_wakeFromSuspendTimer = true;
499}
500
501// Updates the native timer based on currently registered Qt timers,
502// by setting a timeout equivalent to the shortest timer.
503// Must be called on the event dispatcher thread.
504void QEventDispatcherWasm::updateNativeTimer()
505{
506#if QT_CONFIG(thread)
507 Q_ASSERT(QThread::currentThread() == thread());
508#endif
509
510 // On secondary threads, the timeout is managed by setting the WaitForMoreEvents
511 // timeout in processEventsWait().
512 if (!isMainThreadEventDispatcher())
513 return;
514
515 // Clear any timer if there are no active timers
516 const std::optional<std::chrono::nanoseconds> nanoWait = m_timerInfo->timerWait();
517 if (!nanoWait.has_value()) {
518 m_nativeTimer->clearTimeout();
519 return;
520 }
521
522 auto milliWait = std::chrono::duration_cast<std::chrono::milliseconds>(*nanoWait);
523 const auto newTargetTime = m_timerInfo->currentTime + milliWait;
524
525 // Keep existing timer if the timeout has not changed.
526 if (m_nativeTimer->hasTimeout() && newTargetTime == m_timerTargetTime)
527 return;
528
529 // Clear current and set new timer
530 qCDebug(lcEventDispatcherTimers)
531 << "Created new native timer timeout" << milliWait.count() << "ms"
532 << "previous target time" << m_timerTargetTime.time_since_epoch()
533 << "new target time" << newTargetTime.time_since_epoch();
534 m_nativeTimer->clearTimeout();
535 m_nativeTimer->setTimeout(milliWait);
536 m_timerTargetTime = newTargetTime;
537}
538
539namespace {
540 int g_startupTasks = 0;
541}
542
543// The following functions manages sending the "qtLoaded" event/callback
544// from qtloader.js on startup, once Qt initialization has been completed
545// and the application is ready to display the first frame. This can be
546// either as soon as the event loop is running, or later, if additional
547// startup tasks (e.g. local font loading) have been registered.
548
549void QEventDispatcherWasm::registerStartupTask()
550{
551 ++g_startupTasks;
552}
553
554void QEventDispatcherWasm::completeStarupTask()
555{
556 --g_startupTasks;
557 callOnLoadedIfRequired();
558}
559
560void QEventDispatcherWasm::callOnLoadedIfRequired()
561{
562 if (g_startupTasks > 0)
563 return;
564
565 static bool qtLoadedCalled = false;
566 if (qtLoadedCalled)
567 return;
568 qtLoadedCalled = true;
569}
570
571void QEventDispatcherWasm::onLoaded()
572{
573 // TODO: call qtloader.js onLoaded from here, in order to delay
574 // hiding the "Loading..." message until the app is ready to paint
575 // the first frame. Currently onLoaded must be called early before
576 // main() in order to ensure that the screen/container elements
577 // have valid geometry at startup.
578}
579
580void QEventDispatcherWasm::registerSocketNotifier(QSocketNotifier *notifier)
581{
582 QWasmSocket::registerSocketNotifier(notifier);
583}
584
585void QEventDispatcherWasm::unregisterSocketNotifier(QSocketNotifier *notifier)
586{
587 QWasmSocket::unregisterSocketNotifier(notifier);
588}
589
590void QEventDispatcherWasm::socketSelect(int timeout, int socket, bool waitForRead, bool waitForWrite,
591 bool *selectForRead, bool *selectForWrite, bool *socketDisconnect)
592{
593 QEventDispatcherWasm *eventDispatcher = static_cast<QEventDispatcherWasm *>(
594 QAbstractEventDispatcher::instance(QThread::currentThread()));
595
596 if (!eventDispatcher) {
597 qWarning("QEventDispatcherWasm::socketSelect called without eventdispatcher instance");
598 return;
599 }
600
601 QWasmSocket::waitForSocketState(eventDispatcher, timeout, socket, waitForRead, waitForWrite,
602 selectForRead, selectForWrite, socketDisconnect);
603}
604
605QT_END_NAMESPACE
606
607#include "moc_qeventdispatcher_wasm_p.cpp"
static bool useAsyncify()
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")