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 qtSuspendJs();
203}
204
205void QWasmSuspendResumeControl::suspendExclusive(QList<uint32_t> eventHandlerIndices)
206{
207 m_eventFilter = [eventHandlerIndices](int handler) {
208 return eventHandlerIndices.contains(handler);
209 };
210
211 suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndices.back());
212 qtSuspendJs();
213}
214
215// Sends any pending events. Returns the number of sent events.
216int QWasmSuspendResumeControl::sendPendingEvents()
217{
218#if QT_CONFIG(thread)
219 Q_ASSERT(emscripten_is_main_runtime_thread());
220#endif
221 emscripten::val control = suspendResumeControlJs();
222 emscripten::val pendingEvents = control["pendingEvents"];
223
224 int count = 0;
225 for (int i = 0; i < pendingEvents["length"].as<int>();) {
226 if (!m_eventFilter(pendingEvents[i]["index"].as<int>())) {
227 ++i;
228 } else {
229 // Grab one event (handler and arg), and call it
230 emscripten::val event = pendingEvents[i];
231 pendingEvents.call<void>("splice", i, 1);
232
233 auto it = m_eventHandlers.find(event["index"].as<int>());
234 if (it != m_eventHandlers.end()) {
235 setCurrentEvent(event["arg"]);
236 it->second(currentEvent());
237 setCurrentEvent(emscripten::val::undefined());
238 }
239 ++count;
240 }
241 }
242
243 if (control["exclusiveEventHandler"].as<int>() > 0) {
244 control.set("exclusiveEventHandler", 0);
245 m_eventFilter = [](int) { return true;};
246 }
247 return count;
248}
249
251{
252 if (s_suspendResumeControl)
253 s_suspendResumeControl->sendPendingEvents();
254}
255
257 emscripten::function("qtSendPendingEvents", qtSendPendingEvents QT_WASM_EMSCRIPTEN_ASYNC);
258}
259
260//
261// The EventCallback class registers a callback function for an event on an html element.
262//
263QWasmEventHandler::QWasmEventHandler(emscripten::val element, const std::string &name, std::function<void(emscripten::val)> handler)
264:m_element(element)
265,m_name(name)
266{
267 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
268 m_eventHandlerIndex = suspendResume->registerEventHandler(std::move(handler));
269 m_element.call<void>("addEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
270}
271
272QWasmEventHandler::~QWasmEventHandler()
273{
274 // Do nothing if this instance is default-constructed, or was moved from.
275 if (m_element.isUndefined())
276 return;
277
278 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
279 m_element.call<void>("removeEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
280 suspendResume->removeEventHandler(m_eventHandlerIndex);
281}
282
283QWasmEventHandler::QWasmEventHandler(QWasmEventHandler&& other) noexcept
284:m_element(std::move(other.m_element))
285,m_name(std::move(other.m_name))
286,m_eventHandlerIndex(other.m_eventHandlerIndex)
287{
288 other.m_element = emscripten::val();
289 other.m_name = emscripten::val();
290 other.m_eventHandlerIndex = 0;
291}
292
293QWasmEventHandler& QWasmEventHandler::operator=(QWasmEventHandler&& other) noexcept
294{
295 m_element = std::move(other.m_element);
296 other.m_element = emscripten::val();
297 m_name = std::move(other.m_name);
298 other.m_name = emscripten::val();
299 m_eventHandlerIndex = other.m_eventHandlerIndex;
300 other.m_eventHandlerIndex = 0;
301 return *this;
302}
303
304//
305// The QWasmTimer class creates a native single-shot timer. The event handler is provided in the
306// constructor and can be reused: each call setTimeout() sets a new timeout, though with the
307// limitiation that there can be only one timeout at a time. (Setting a new timer clears the
308// previous one).
309//
310QWasmTimer::QWasmTimer(QWasmSuspendResumeControl *suspendResume, std::function<void()> handler)
312{
313 auto wrapper = [handler = std::move(handler), this](val argument) {
314 Q_UNUSED(argument); // no argument for timers
315 if (!m_timerId)
316 return; // timer was cancelled
317 m_timerId = 0;
318 handler();
319 };
320
321 m_handlerIndex = m_suspendResume->registerEventHandler(std::move(wrapper));
322}
323
325{
327 // We lack a test that checks validity of m_suspendResume
328 m_suspendResume->removeEventHandler(m_handlerIndex);
329}
330
331void QWasmTimer::setTimeout(std::chrono::milliseconds timeout)
332{
333 Q_ASSERT(m_suspendResume == QWasmSuspendResumeControl::get());
334 if (hasTimeout())
336 val jsHandler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
337 using ArgType = double; // emscripten::val::call() does not support int64_t
338 ArgType timoutValue = static_cast<ArgType>(timeout.count());
339 ArgType timerId = val::global("window").call<ArgType>("setTimeout", jsHandler, timoutValue);
340 m_timerId = static_cast<int64_t>(std::round(timerId));
341}
342
344{
345 return m_timerId > 0;
346}
347
349{
350 val::global("window").call<void>("clearTimeout", double(m_timerId));
351 m_timerId = 0;
352}
353
354//
355// QWasmAnimationFrameMultiHandler
356//
357// Multiplexes multiple animate and draw callbacks to a single native requestAnimationFrame call.
358// Animate callbacks are called before draw callbacks to ensure animations are advanced before drawing.
359//
360QWasmAnimationFrameMultiHandler::QWasmAnimationFrameMultiHandler()
361{
362 auto wrapper = [this](val arg) {
363 handleAnimationFrame(arg.as<double>());
364 };
365 m_handlerIndex = QWasmSuspendResumeControl::get()->registerEventHandler(wrapper);
366}
367
368QWasmAnimationFrameMultiHandler::~QWasmAnimationFrameMultiHandler()
369{
370 cancelAnimationFrameRequest();
371 QWasmSuspendResumeControl::get()->removeEventHandler(m_handlerIndex);
372}
373
374Q_GLOBAL_STATIC(QWasmAnimationFrameMultiHandler, s_animationFrameHandler);
375QWasmAnimationFrameMultiHandler *QWasmAnimationFrameMultiHandler::instance()
376{
377 return s_animationFrameHandler();
378}
379
380// Registers a permanent animation callback. Call unregisterAnimateCallback() to unregister
381uint32_t QWasmAnimationFrameMultiHandler::registerAnimateCallback(Callback callback)
382{
383 uint32_t handle = ++m_nextAnimateHandle;
384 m_animateCallbacks[handle] = std::move(callback);
385 ensureAnimationFrameRequested();
386 return handle;
387}
388
389// Registers a single-shot draw callback.
390uint32_t QWasmAnimationFrameMultiHandler::registerDrawCallback(Callback callback)
391{
392 uint32_t handle = ++m_nextDrawHandle;
393 m_drawCallbacks[handle] = std::move(callback);
394 ensureAnimationFrameRequested();
395 return handle;
396}
397
398void QWasmAnimationFrameMultiHandler::unregisterAnimateCallback(uint32_t handle)
399{
400 m_animateCallbacks.erase(handle);
401 if (m_animateCallbacks.empty() && m_drawCallbacks.empty())
402 cancelAnimationFrameRequest();
403}
404
405void QWasmAnimationFrameMultiHandler::unregisterDrawCallback(uint32_t handle)
406{
407 m_drawCallbacks.erase(handle);
408 if (m_animateCallbacks.empty() && m_drawCallbacks.empty())
409 cancelAnimationFrameRequest();
410}
411
412void QWasmAnimationFrameMultiHandler::handleAnimationFrame(double timestamp)
413{
414 m_requestId = -1;
415
416 // Advance animations. Copy the callbacks list in case callbacks are
417 // unregistered during iteration
418 auto animateCallbacksCopy = m_animateCallbacks;
419 for (const auto &pair : animateCallbacksCopy)
420 pair.second(timestamp);
421
422 // Draw the frame. Note that draw callbacks are cleared after each
423 // frame, matching QWindow::requestUpdate() behavior. Copy the callbacks
424 // list in case new callbacks are registered while drawing the frame
425 auto drawCallbacksCopy = m_drawCallbacks;
426 m_drawCallbacks.clear();
427 for (const auto &pair : drawCallbacksCopy)
428 pair.second(timestamp);
429
430 // Request next frame if there are still callbacks registered
431 if (!m_animateCallbacks.empty() || !m_drawCallbacks.empty())
432 ensureAnimationFrameRequested();
433}
434
435void QWasmAnimationFrameMultiHandler::ensureAnimationFrameRequested()
436{
437 if (m_requestId != -1)
438 return;
439
440 using ReturnType = double;
441 val handler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
442 m_requestId = int64_t(val::global("window").call<ReturnType>("requestAnimationFrame", handler));
443}
444
445void QWasmAnimationFrameMultiHandler::cancelAnimationFrameRequest()
446{
447 if (m_requestId == -1)
448 return;
449
450 val::global("window").call<void>("cancelAnimationFrame", double(m_requestId));
451 m_requestId = -1;
452}
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()