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 function createNamedFunction(name, parent, obj) {
75 return {
76 [name]: function(...args) {
77 return obj.call(parent, args);
78 }
79 }[name];
80 }
81
82 function deepShallowClone(obj) {
83 if (obj === null)
84 return obj;
85
86 if (!(obj instanceof Event))
87 return obj;
88
89 const objCopy = {};
90 for (const key in obj) {
91 if (typeof obj[key] === 'function')
92 objCopy[key] = createNamedFunction(obj[key].name, obj, obj[key]);
93 else
94 objCopy[key] = obj[key];
95 }
96
97 objCopy['isInstanceOfEvent'] = true;
98 return objCopy;
99 }
100
101 let index = $0;
102 let control = Module.qtSuspendResumeControl;
103 let handler = (arg) => {
104 // Copy the top level object, alias the rest.
105 // functions are copied by creating new forwarding functions.
106 arg = deepShallowClone(arg);
107
108 // Add event to event queue
109 control.pendingEvents.push({
110 index: index,
111 arg: arg
112 });
113
114 // Handle the event based on instance state and asyncify flag
115 if (control.exclusiveEventHandler > 0) {
116 // In exclusive mode, resume on exclusive event handler match only
117
118 if (index != control.exclusiveEventHandler)
119 return;
120
121 const resume = control.resume;
122 control.resume = null;
123 resume();
124 } else if (control.resume) {
125 // The instance is suspended in processEvents(), resume and process the event
126 const resume = control.resume;
127 control.resume = null;
128 resume();
129 } else {
130 if (control.asyncifyEnabled) {
131 // The instance is either not suspended or is supended outside of processEvents()
132 // (e.g. on emscripten_sleep()). Currently there is no way to determine
133 // which state the instance is in. Keep the event in the event queue to be
134 // processed on the next processEvents() call.
135 // FIXME: call event handler here if we can determine that the instance
136 // is not suspended.
137 } else {
138 // The instance is not suspended, call the handler directly
139 Module.qtSendPendingEvents();
140 }
141 }
142 };
143 control.eventHandlers[index] = handler;
144 }, index);
145}
146
147QWasmSuspendResumeControl::QWasmSuspendResumeControl()
148{
149#if QT_CONFIG(thread)
150 Q_ASSERT(emscripten_is_main_runtime_thread());
151#endif
152 qtSuspendResumeControlClearJs();
153 suspendResumeControlJs().set("asyncifyEnabled", qstdweb::haveAsyncify());
154}
155
156QWasmSuspendResumeControl::~QWasmSuspendResumeControl()
157{
158 if (!m_eventHandlers.empty())
159 qWarning() << "QWasmSuspendResumeControl::~QWasmSuspendResumeControl - still remaining " << m_eventHandlers.size() << " handlers";
160 qtSuspendResumeControlClearJs();
161}
162
163QWasmSuspendResumeControl *QWasmSuspendResumeControl::get()
164{
165 if (!s_suspendResumeControl)
166 qFatal("QWasmSuspendResumeControl -- Object not created/destroyed");
167
168 return s_suspendResumeControl;
169}
170
171// Registers a C++ event handler.
172uint32_t QWasmSuspendResumeControl::registerEventHandler(std::function<void(val)> handler)
173{
174 static uint32_t i = 0;
175 ++i;
176 m_eventHandlers.emplace(i, std::move(handler));
177 qtRegisterEventHandlerJs(i);
178 return i;
179}
180
181// Removes a C++ event handler
182void QWasmSuspendResumeControl::removeEventHandler(uint32_t index)
183{
184 m_eventHandlers.erase(index);
185 suspendResumeControlJs()["eventHandlers"].set(index, val::null());
186}
187
188// Returns the JS event handler for the given index
189val QWasmSuspendResumeControl::jsEventHandlerAt(uint32_t index)
190{
191 return suspendResumeControlJs()["eventHandlers"][index];
192}
193
194emscripten::val QWasmSuspendResumeControl::suspendResumeControlJs()
195{
196 return val::module_property("qtSuspendResumeControl");
197}
198
199// Suspends the calling thread.
200void QWasmSuspendResumeControl::suspend()
201{
202 if (!qstdweb::canBlockCallingThread()) {
203 qFatal("Suspending the main thread requires asyncify or JSPI; "
204 "see the Qt for WebAssembly documentation for how to enable.");
205 }
206 qtSuspendJs();
207}
208
209void QWasmSuspendResumeControl::suspendExclusive(QList<uint32_t> eventHandlerIndices)
210{
211 if (!qstdweb::canBlockCallingThread()) {
212 qFatal("Suspending the main thread requires asyncify or JSPI; "
213 "see the Qt for WebAssembly documentation for how to enable.");
214 }
215
216 m_eventFilter = [eventHandlerIndices](int handler) {
217 return eventHandlerIndices.contains(handler);
218 };
219
220 suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndices.back());
221 qtSuspendJs();
222}
223
224// Sends any pending events. Returns the number of sent events.
225int QWasmSuspendResumeControl::sendPendingEvents()
226{
227#if QT_CONFIG(thread)
228 Q_ASSERT(emscripten_is_main_runtime_thread());
229#endif
230 emscripten::val control = suspendResumeControlJs();
231 emscripten::val pendingEvents = control["pendingEvents"];
232
233 int count = 0;
234 for (int i = 0; i < pendingEvents["length"].as<int>();) {
235 if (!m_eventFilter(pendingEvents[i]["index"].as<int>())) {
236 ++i;
237 } else {
238 // Grab one event (handler and arg), and call it
239 emscripten::val event = pendingEvents[i];
240 pendingEvents.call<void>("splice", i, 1);
241
242 auto it = m_eventHandlers.find(event["index"].as<int>());
243 if (it != m_eventHandlers.end()) {
244 setCurrentEvent(event["arg"]);
245 it->second(currentEvent());
246 setCurrentEvent(emscripten::val::undefined());
247 }
248 ++count;
249 }
250 }
251
252 if (control["exclusiveEventHandler"].as<int>() > 0) {
253 control.set("exclusiveEventHandler", 0);
254 m_eventFilter = [](int) { return true;};
255 }
256 return count;
257}
258
260{
261 if (s_suspendResumeControl)
262 s_suspendResumeControl->sendPendingEvents();
263}
264
266 emscripten::function("qtSendPendingEvents", qtSendPendingEvents QT_WASM_EMSCRIPTEN_ASYNC);
267}
268
269//
270// The EventCallback class registers a callback function for an event on an html element.
271//
272QWasmEventHandler::QWasmEventHandler(emscripten::val element, const std::string &name, std::function<void(emscripten::val)> handler)
273:m_element(element)
274,m_name(name)
275{
276 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
277 m_eventHandlerIndex = suspendResume->registerEventHandler(std::move(handler));
278 m_element.call<void>("addEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
279}
280
281QWasmEventHandler::~QWasmEventHandler()
282{
283 // Do nothing if this instance is default-constructed, or was moved from.
284 if (m_element.isUndefined())
285 return;
286
287 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
288 m_element.call<void>("removeEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
289 suspendResume->removeEventHandler(m_eventHandlerIndex);
290}
291
292QWasmEventHandler::QWasmEventHandler(QWasmEventHandler&& other) noexcept
293:m_element(std::move(other.m_element))
294,m_name(std::move(other.m_name))
295,m_eventHandlerIndex(other.m_eventHandlerIndex)
296{
297 other.m_element = emscripten::val();
298 other.m_name = emscripten::val();
299 other.m_eventHandlerIndex = 0;
300}
301
302QWasmEventHandler& QWasmEventHandler::operator=(QWasmEventHandler&& other) noexcept
303{
304 m_element = std::move(other.m_element);
305 other.m_element = emscripten::val();
306 m_name = std::move(other.m_name);
307 other.m_name = emscripten::val();
308 m_eventHandlerIndex = other.m_eventHandlerIndex;
309 other.m_eventHandlerIndex = 0;
310 return *this;
311}
312
313//
314// The QWasmTimer class creates a native single-shot timer. The event handler is provided in the
315// constructor and can be reused: each call setTimeout() sets a new timeout, though with the
316// limitiation that there can be only one timeout at a time. (Setting a new timer clears the
317// previous one).
318//
319QWasmTimer::QWasmTimer(QWasmSuspendResumeControl *suspendResume, std::function<void()> handler)
321{
322 auto wrapper = [handler = std::move(handler), this](val argument) {
323 Q_UNUSED(argument); // no argument for timers
324 if (!m_timerId)
325 return; // timer was cancelled
326 m_timerId = 0;
327 handler();
328 };
329
330 m_handlerIndex = m_suspendResume->registerEventHandler(std::move(wrapper));
331}
332
334{
336 // We lack a test that checks validity of m_suspendResume
337 m_suspendResume->removeEventHandler(m_handlerIndex);
338}
339
340void QWasmTimer::setTimeout(std::chrono::milliseconds timeout)
341{
342 Q_ASSERT(m_suspendResume == QWasmSuspendResumeControl::get());
343 if (hasTimeout())
345 val jsHandler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
346 using ArgType = double; // emscripten::val::call() does not support int64_t
347 ArgType timoutValue = static_cast<ArgType>(timeout.count());
348 ArgType timerId = val::global("window").call<ArgType>("setTimeout", jsHandler, timoutValue);
349 m_timerId = static_cast<int64_t>(std::round(timerId));
350}
351
353{
354 return m_timerId > 0;
355}
356
358{
359 val::global("window").call<void>("clearTimeout", double(m_timerId));
360 m_timerId = 0;
361}
362
363//
364// QWasmAnimationFrameMultiHandler
365//
366// Multiplexes multiple animate and draw callbacks to a single native requestAnimationFrame call.
367// Animate callbacks are called before draw callbacks to ensure animations are advanced before drawing.
368//
369QWasmAnimationFrameMultiHandler::QWasmAnimationFrameMultiHandler()
370{
371 auto wrapper = [this](val arg) {
372 handleAnimationFrame(arg.as<double>());
373 };
374 m_handlerIndex = QWasmSuspendResumeControl::get()->registerEventHandler(wrapper);
375}
376
377QWasmAnimationFrameMultiHandler::~QWasmAnimationFrameMultiHandler()
378{
379 cancelAnimationFrameRequest();
380 QWasmSuspendResumeControl::get()->removeEventHandler(m_handlerIndex);
381}
382
383Q_GLOBAL_STATIC(QWasmAnimationFrameMultiHandler, s_animationFrameHandler);
384QWasmAnimationFrameMultiHandler *QWasmAnimationFrameMultiHandler::instance()
385{
386 return s_animationFrameHandler();
387}
388
389// Registers a permanent animation callback. Call unregisterAnimateCallback() to unregister
390uint32_t QWasmAnimationFrameMultiHandler::registerAnimateCallback(Callback callback)
391{
392 uint32_t handle = ++m_nextAnimateHandle;
393 m_animateCallbacks[handle] = std::move(callback);
394 ensureAnimationFrameRequested();
395 return handle;
396}
397
398// Registers a single-shot draw callback.
399uint32_t QWasmAnimationFrameMultiHandler::registerDrawCallback(Callback callback)
400{
401 uint32_t handle = ++m_nextDrawHandle;
402 m_drawCallbacks[handle] = std::move(callback);
403 ensureAnimationFrameRequested();
404 return handle;
405}
406
407void QWasmAnimationFrameMultiHandler::unregisterAnimateCallback(uint32_t handle)
408{
409 m_animateCallbacks.erase(handle);
410 if (m_animateCallbacks.empty() && m_drawCallbacks.empty())
411 cancelAnimationFrameRequest();
412}
413
414void QWasmAnimationFrameMultiHandler::unregisterDrawCallback(uint32_t handle)
415{
416 m_drawCallbacks.erase(handle);
417 if (m_animateCallbacks.empty() && m_drawCallbacks.empty())
418 cancelAnimationFrameRequest();
419}
420
421void QWasmAnimationFrameMultiHandler::handleAnimationFrame(double timestamp)
422{
423 m_requestId = -1;
424
425 // Advance animations. Copy the callbacks list in case callbacks are
426 // unregistered during iteration
427 auto animateCallbacksCopy = m_animateCallbacks;
428 for (const auto &pair : animateCallbacksCopy)
429 pair.second(timestamp);
430
431 // Draw the frame. Note that draw callbacks are cleared after each
432 // frame, matching QWindow::requestUpdate() behavior. Copy the callbacks
433 // list in case new callbacks are registered while drawing the frame
434 auto drawCallbacksCopy = m_drawCallbacks;
435 m_drawCallbacks.clear();
436 for (const auto &pair : drawCallbacksCopy)
437 pair.second(timestamp);
438
439 // Request next frame if there are still callbacks registered
440 if (!m_animateCallbacks.empty() || !m_drawCallbacks.empty())
441 ensureAnimationFrameRequested();
442}
443
444void QWasmAnimationFrameMultiHandler::ensureAnimationFrameRequested()
445{
446 if (m_requestId != -1)
447 return;
448
449 using ReturnType = double;
450 val handler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
451 m_requestId = int64_t(val::global("window").call<ReturnType>("requestAnimationFrame", handler));
452}
453
454void QWasmAnimationFrameMultiHandler::cancelAnimationFrameRequest()
455{
456 if (m_requestId == -1)
457 return;
458
459 val::global("window").call<void>("cancelAnimationFrame", double(m_requestId));
460 m_requestId = -1;
461}
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()