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
qwasmsuspendresumecontrol.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 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#include "qstdweb_p.h"
7
8#include <QtCore/qapplicationstatic.h>
9#include <QtCore/qdebug.h>
10
11#include <emscripten.h>
12#include <emscripten/val.h>
13#include <emscripten/bind.h>
14
15using emscripten::val;
16
17/*
18 QWasmSuspendResumeControl controls asyncify suspend and resume when handling native events.
19
20 The class supports registering C++ event handlers, and creates a corresponding
21 JavaScript event handler which can be passed to addEventListener() or similar
22 API:
23
24 auto handler = [](emscripten::val argument){
25 // handle event
26 };
27 uint32_t index = control->registerEventHandler(handler);
28 element.call<void>("addEventListener", "eventname", control->jsEventHandlerAt(index));
29
30 The wasm instance suspends itself by calling the suspend() function, which resumes
31 and returns whenever there was a native event. Call sendPendingEvents() to send
32 the native event and invoke the C++ event handlers.
33
34 // about to suspend
35 control->suspend(); // <- instance/app sleeps here
36 // was resumed, send event(s)
37 control->sendPendingEvents();
38
39 QWasmSuspendResumeControl also supports the case where the wasm instance returns
40 control to the browser's event loop (without suspending), and will call the C++
41 event handlers directly in that case.
42*/
43
44Q_GLOBAL_STATIC(QWasmSuspendResumeControl, s_suspendResumeControl);
45
46// Setup/constructor function for Module.suspendResumeControl.
47// FIXME if assigning to the Module object from C++ is/becomes possible
48// then this does not need to be a separate JS function.
50 EM_ASM({
51 Module.qtSuspendResumeControl = ({
52 resume: null,
53 asyncifyEnabled: false, // asyncify 1 or JSPI enabled
54 eventHandlers: {},
55 pendingEvents: [],
56 exclusiveEventHandler: 0,
57 });
58 });
59}
60
61// Suspends the calling thread
63 return new Promise(resolve => {
65 });
66});
67
68// Registers a JS event handler which when called registers its index
69// as the "current" event handler, and then resumes the wasm instance.
70// The wasm instance will then call the C++ event after it is resumed.
71void qtRegisterEventHandlerJs(int index) {
72 EM_ASM({
73
74 // Create a shallow copy of an event by copying its properties
75 // to a new object.
76 function snapshotEvent(event) {
77
78 // Return non-events (and null) as-is
79 if (!(event instanceof Event))
80 return event;
81
82 // Create event copy
83 const copy = { isInstanceOfEvent: true };
84 for (const key in event) {
85 const value = event[key];
86 copy[key] = typeof value === 'function' ? value.bind(event) : value;
87 }
88
89 return copy;
90 }
91
92 let index = $0;
93 let control = Module.qtSuspendResumeControl;
94 let handler = (arg) => {
95
96 // Create a snapshot of the event for the queue. Browsers
97 // may recycle the event objects once control returns which
98 // means we can't store referenes to them in the queue.
99 arg = snapshotEvent(arg);
100
101 // Add event to event queue
102 control.pendingEvents.push({
103 index: index,
104 arg: arg
105 });
106
107 // Handle the event based on instance state and asyncify flag
108 if (control.exclusiveEventHandler > 0) {
109 // In exclusive mode, resume on exclusive event handler match only
110
111 if (index != control.exclusiveEventHandler)
112 return;
113
114 const resume = control.resume;
115 control.resume = null;
116 resume();
117 } else if (control.resume) {
118 // The instance is suspended in processEvents(), resume and process the event
119 const resume = control.resume;
120 control.resume = null;
121 resume();
122 } else {
123 if (control.asyncifyEnabled) {
124 // The instance is either not suspended or is supended outside of processEvents()
125 // (e.g. on emscripten_sleep()). Currently there is no way to determine
126 // which state the instance is in. Keep the event in the event queue to be
127 // processed on the next processEvents() call.
128 // FIXME: call event handler here if we can determine that the instance
129 // is not suspended.
130 } else {
131 // The instance is not suspended, call the handler directly
132 Module.qtSendPendingEvents();
133 }
134 }
135 };
136 control.eventHandlers[index] = handler;
137 }, index);
138}
139
140QWasmSuspendResumeControl::QWasmSuspendResumeControl()
141{
142#if QT_CONFIG(thread)
143 Q_ASSERT(emscripten_is_main_runtime_thread());
144#endif
145 qtSuspendResumeControlClearJs();
146 suspendResumeControlJs().set("asyncifyEnabled", qstdweb::haveAsyncify());
147}
148
149QWasmSuspendResumeControl::~QWasmSuspendResumeControl()
150{
151 if (!m_eventHandlers.empty())
152 qWarning() << "QWasmSuspendResumeControl::~QWasmSuspendResumeControl - still remaining " << m_eventHandlers.size() << " handlers";
153 qtSuspendResumeControlClearJs();
154}
155
156QWasmSuspendResumeControl *QWasmSuspendResumeControl::get()
157{
158 if (!s_suspendResumeControl)
159 qFatal("QWasmSuspendResumeControl -- Object not created/destroyed");
160
161 return s_suspendResumeControl;
162}
163
164// Registers a C++ event handler.
165uint32_t QWasmSuspendResumeControl::registerEventHandler(std::function<void(val)> handler)
166{
167 static uint32_t i = 0;
168 ++i;
169 m_eventHandlers.emplace(i, std::move(handler));
170 qtRegisterEventHandlerJs(i);
171 return i;
172}
173
174// Removes a C++ event handler
175void QWasmSuspendResumeControl::removeEventHandler(uint32_t index)
176{
177 m_eventHandlers.erase(index);
178 suspendResumeControlJs()["eventHandlers"].set(index, val::null());
179}
180
181// Returns the JS event handler for the given index
182val QWasmSuspendResumeControl::jsEventHandlerAt(uint32_t index)
183{
184 return suspendResumeControlJs()["eventHandlers"][index];
185}
186
187emscripten::val QWasmSuspendResumeControl::suspendResumeControlJs()
188{
189 return val::module_property("qtSuspendResumeControl");
190}
191
192// Suspends the calling thread.
193void QWasmSuspendResumeControl::suspend()
194{
195 if (!qstdweb::canBlockCallingThread()) {
196 qFatal("Suspending the main thread requires asyncify or JSPI; "
197 "see the Qt for WebAssembly documentation for how to enable.");
198 }
199 qtSuspendJs();
200}
201
202void QWasmSuspendResumeControl::suspendExclusive(QList<uint32_t> eventHandlerIndices)
203{
204 if (!qstdweb::canBlockCallingThread()) {
205 qFatal("Suspending the main thread requires asyncify or JSPI; "
206 "see the Qt for WebAssembly documentation for how to enable.");
207 }
208
209 m_eventFilter = [eventHandlerIndices](int handler) {
210 return eventHandlerIndices.contains(handler);
211 };
212
213 suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndices.back());
214 qtSuspendJs();
215}
216
217// Sends any pending events. Returns the number of sent events.
218int QWasmSuspendResumeControl::sendPendingEvents()
219{
220#if QT_CONFIG(thread)
221 Q_ASSERT(emscripten_is_main_runtime_thread());
222#endif
223 emscripten::val control = suspendResumeControlJs();
224 emscripten::val pendingEvents = control["pendingEvents"];
225
226 int count = 0;
227 for (int i = 0; i < pendingEvents["length"].as<int>();) {
228 if (!m_eventFilter(pendingEvents[i]["index"].as<int>())) {
229 ++i;
230 } else {
231 // Grab one event (handler and arg), and call it
232 emscripten::val event = pendingEvents[i];
233 pendingEvents.call<void>("splice", i, 1);
234
235 auto it = m_eventHandlers.find(event["index"].as<int>());
236 if (it != m_eventHandlers.end()) {
237 setCurrentEvent(event["arg"]);
238 it->second(currentEvent());
239 setCurrentEvent(emscripten::val::undefined());
240 }
241 ++count;
242 }
243 }
244
245 if (control["exclusiveEventHandler"].as<int>() > 0) {
246 control.set("exclusiveEventHandler", 0);
247 m_eventFilter = [](int) { return true;};
248 }
249 return count;
250}
251
253{
254 if (s_suspendResumeControl)
255 s_suspendResumeControl->sendPendingEvents();
256}
257
259 emscripten::function("qtSendPendingEvents", qtSendPendingEvents QT_WASM_EMSCRIPTEN_ASYNC);
260}
261
262//
263// The EventCallback class registers a callback function for an event on an html element.
264//
265QWasmEventHandler::QWasmEventHandler(emscripten::val element, const std::string &name, std::function<void(emscripten::val)> handler)
266:m_element(element)
267,m_name(name)
268{
269 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
270 m_eventHandlerIndex = suspendResume->registerEventHandler(std::move(handler));
271 m_element.call<void>("addEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
272}
273
274QWasmEventHandler::~QWasmEventHandler()
275{
276 // Do nothing if this instance is default-constructed, or was moved from.
277 if (m_element.isUndefined())
278 return;
279
280 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
281 m_element.call<void>("removeEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
282 suspendResume->removeEventHandler(m_eventHandlerIndex);
283}
284
285QWasmEventHandler::QWasmEventHandler(QWasmEventHandler&& other) noexcept
286:m_element(std::move(other.m_element))
287,m_name(std::move(other.m_name))
288,m_eventHandlerIndex(other.m_eventHandlerIndex)
289{
290 other.m_element = emscripten::val();
291 other.m_name = emscripten::val();
292 other.m_eventHandlerIndex = 0;
293}
294
295QWasmEventHandler& QWasmEventHandler::operator=(QWasmEventHandler&& other) noexcept
296{
297 m_element = std::move(other.m_element);
298 other.m_element = emscripten::val();
299 m_name = std::move(other.m_name);
300 other.m_name = emscripten::val();
301 m_eventHandlerIndex = other.m_eventHandlerIndex;
302 other.m_eventHandlerIndex = 0;
303 return *this;
304}
305
306//
307// The QWasmTimer class creates a native single-shot timer. The event handler is provided in the
308// constructor and can be reused: each call setTimeout() sets a new timeout, though with the
309// limitiation that there can be only one timeout at a time. (Setting a new timer clears the
310// previous one).
311//
312QWasmTimer::QWasmTimer(QWasmSuspendResumeControl *suspendResume, std::function<void()> handler)
314{
315 auto wrapper = [handler = std::move(handler), this](val argument) {
316 Q_UNUSED(argument); // no argument for timers
317 if (!m_timerId)
318 return; // timer was cancelled
319 m_timerId = 0;
320 handler();
321 };
322
323 m_handlerIndex = m_suspendResume->registerEventHandler(std::move(wrapper));
324}
325
327{
329 // We lack a test that checks validity of m_suspendResume
330 m_suspendResume->removeEventHandler(m_handlerIndex);
331}
332
333void QWasmTimer::setTimeout(std::chrono::milliseconds timeout)
334{
335 Q_ASSERT(m_suspendResume == QWasmSuspendResumeControl::get());
336 if (hasTimeout())
338 val jsHandler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
339 using ArgType = double; // emscripten::val::call() does not support int64_t
340 ArgType timoutValue = static_cast<ArgType>(timeout.count());
341 ArgType timerId = val::global("window").call<ArgType>("setTimeout", jsHandler, timoutValue);
342 m_timerId = static_cast<int64_t>(std::round(timerId));
343}
344
346{
347 return m_timerId > 0;
348}
349
351{
352 val::global("window").call<void>("clearTimeout", double(m_timerId));
353 m_timerId = 0;
354}
355
356//
357// QWasmAnimationFrameMultiHandler
358//
359// Multiplexes multiple animate and draw callbacks to a single native requestAnimationFrame call.
360// Animate callbacks are called before draw callbacks to ensure animations are advanced before drawing.
361//
362QWasmAnimationFrameMultiHandler::QWasmAnimationFrameMultiHandler()
363{
364 auto wrapper = [this](val arg) {
365 handleAnimationFrame(arg.as<double>());
366 };
367 m_handlerIndex = QWasmSuspendResumeControl::get()->registerEventHandler(wrapper);
368}
369
370QWasmAnimationFrameMultiHandler::~QWasmAnimationFrameMultiHandler()
371{
372 cancelAnimationFrameRequest();
373 QWasmSuspendResumeControl::get()->removeEventHandler(m_handlerIndex);
374}
375
376Q_GLOBAL_STATIC(QWasmAnimationFrameMultiHandler, s_animationFrameHandler);
377QWasmAnimationFrameMultiHandler *QWasmAnimationFrameMultiHandler::instance()
378{
379 return s_animationFrameHandler();
380}
381
382// Registers a permanent animation callback. Call unregisterAnimateCallback() to unregister
383uint32_t QWasmAnimationFrameMultiHandler::registerAnimateCallback(Callback callback)
384{
385 uint32_t handle = ++m_nextAnimateHandle;
386 m_animateCallbacks[handle] = std::move(callback);
387 ensureAnimationFrameRequested();
388 return handle;
389}
390
391// Registers a single-shot draw callback.
392uint32_t QWasmAnimationFrameMultiHandler::registerDrawCallback(Callback callback)
393{
394 uint32_t handle = ++m_nextDrawHandle;
395 m_drawCallbacks[handle] = std::move(callback);
396 ensureAnimationFrameRequested();
397 return handle;
398}
399
400void QWasmAnimationFrameMultiHandler::unregisterAnimateCallback(uint32_t handle)
401{
402 m_animateCallbacks.erase(handle);
403 if (m_animateCallbacks.empty() && m_drawCallbacks.empty())
404 cancelAnimationFrameRequest();
405}
406
407void QWasmAnimationFrameMultiHandler::unregisterDrawCallback(uint32_t handle)
408{
409 m_drawCallbacks.erase(handle);
410 if (m_animateCallbacks.empty() && m_drawCallbacks.empty())
411 cancelAnimationFrameRequest();
412}
413
414void QWasmAnimationFrameMultiHandler::handleAnimationFrame(double timestamp)
415{
416 m_requestId = -1;
417
418 // Advance animations. Copy the callbacks list in case callbacks are
419 // unregistered during iteration
420 auto animateCallbacksCopy = m_animateCallbacks;
421 for (const auto &pair : animateCallbacksCopy)
422 pair.second(timestamp);
423
424 // Draw the frame. Note that draw callbacks are cleared after each
425 // frame, matching QWindow::requestUpdate() behavior. Copy the callbacks
426 // list in case new callbacks are registered while drawing the frame
427 auto drawCallbacksCopy = m_drawCallbacks;
428 m_drawCallbacks.clear();
429 for (const auto &pair : drawCallbacksCopy)
430 pair.second(timestamp);
431
432 // Request next frame if there are still callbacks registered
433 if (!m_animateCallbacks.empty() || !m_drawCallbacks.empty())
434 ensureAnimationFrameRequested();
435}
436
437void QWasmAnimationFrameMultiHandler::ensureAnimationFrameRequested()
438{
439 if (m_requestId != -1)
440 return;
441
442 using ReturnType = double;
443 val handler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
444 m_requestId = int64_t(val::global("window").call<ReturnType>("requestAnimationFrame", handler));
445}
446
447void QWasmAnimationFrameMultiHandler::cancelAnimationFrameRequest()
448{
449 if (m_requestId == -1)
450 return;
451
452 val::global("window").call<void>("cancelAnimationFrame", double(m_requestId));
453 m_requestId = -1;
454}
void setTimeout(std::chrono::milliseconds timeout)
QWasmTimer(QWasmSuspendResumeControl *suspendResume, std::function< void()> handler)
Q_GLOBAL_STATIC(DefaultRoleNames, qDefaultRoleNames, { { Qt::DisplayRole, "display" }, { Qt::DecorationRole, "decoration" }, { Qt::EditRole, "edit" }, { Qt::ToolTipRole, "toolTip" }, { Qt::StatusTipRole, "statusTip" }, { Qt::WhatsThisRole, "whatsThis" }, }) const QHash< int
#define QT_WASM_EMSCRIPTEN_ASYNC
Definition qstdweb_p.h:42
EMSCRIPTEN_BINDINGS(qtSuspendResumeControl)
EM_ASYNC_JS(void, qtSuspendJs,(), { return new Promise(resolve=> { Module.qtSuspendResumeControl.resume=resolve;});})
void qtSuspendResumeControlClearJs()
void qtRegisterEventHandlerJs(int index)
void qtSendPendingEvents()