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