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