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