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