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 <emscripten.h>
8#include <emscripten/val.h>
9#include <emscripten/bind.h>
10
11using emscripten::val;
12
13/*
14 QWasmSuspendResumeControl controls asyncify suspend and resume when handling native events.
15
16 The class supports registering C++ event handlers, and creates a corresponding
17 JavaScript event handler which can be passed to addEventListener() or similar
18 API:
19
20 auto handler = [](emscripten::val argument){
21 // handle event
22 };
23 uint32_t index = control->registerEventHandler(handler);
24 element.call<void>("addEventListener", "eventname", control->jsEventHandlerAt(index));
25
26 The wasm instance suspends itself by calling the suspend() function, which resumes
27 and returns whenever there was a native event. Call sendPendingEvents() to send
28 the native event and invoke the C++ event handlers.
29
30 // about to suspend
31 control->suspend(); // <- instance/app sleeps here
32 // was resumed, send event(s)
33 control->sendPendingEvents();
34
35 QWasmSuspendResumeControl also supports the case where the wasm instance returns
36 control to the browser's event loop (without suspending), and will call the C++
37 event handlers directly in that case.
38*/
39
40QWasmSuspendResumeControl *QWasmSuspendResumeControl::s_suspendResumeControl = nullptr;
41
42// Setup/constructor function for Module.suspendResumeControl.
43// FIXME if assigning to the Module object from C++ is/becomes possible
44// then this does not need to be a separate JS function.
46 EM_ASM({
47 Module.qtSuspendResumeControl = ({
48 resume: null,
49 asyncifyEnabled: false, // asyncify 1 or JSPI enabled
50 eventHandlers: {},
51 pendingEvents: [],
52 exclusiveEventHandler: 0,
53 });
54 });
55}
56
57// Suspends the calling thread
59 return new Promise(resolve => {
61 });
62});
63
64// Registers a JS event handler which when called registers its index
65// as the "current" event handler, and then resumes the wasm instance.
66// The wasm instance will then call the C++ event after it is resumed.
67void qtRegisterEventHandlerJs(int index) {
68 EM_ASM({
69
70 function createNamedFunction(name, parent, obj) {
71 return {
72 [name]: function(...args) {
73 return obj.call(parent, args);
74 }
75 }[name];
76 }
77
78 function deepShallowClone(parent, obj, depth) {
79 if (obj === null)
80 return obj;
81
82 if (typeof obj === 'function') {
83 if (obj.name !== "")
84 return createNamedFunction(obj.name, parent, obj);
85 }
86
87 if (depth >= 1)
88 return obj;
89
90 if (typeof obj !== 'object')
91 return obj;
92
93 if (Array.isArray(obj)) {
94 const arrCopy = [];
95 for (let i = 0; i < obj.length; i++)
96 arrCopy[i] = deepShallowClone(obj, obj[i], depth + 1);
97
98 return arrCopy;
99 }
100
101 const objCopy = {};
102 for (const key in obj)
103 objCopy[key] = deepShallowClone(obj, obj[key], depth + 1);
104
105 return objCopy;
106 }
107
108 let index = $0;
109 let control = Module.qtSuspendResumeControl;
110 let handler = (arg) => {
111 // Copy the top level object, alias the rest.
112 // functions are copied by creating new forwarding functions.
113 arg = deepShallowClone(arg, arg, 0);
114
115 // Add event to event queue
116 control.pendingEvents.push({
117 index: index,
118 arg: arg
119 });
120
121 // Handle the event based on instance state and asyncify flag
122 if (control.exclusiveEventHandler > 0) {
123 // In exclusive mode, resume on exclusive event handler match only
124 if (index != control.exclusiveEventHandler)
125 return;
126
127 const resume = control.resume;
128 control.resume = null;
129 resume();
130 } else if (control.resume) {
131 // The instance is suspended in processEvents(), resume and process the event
132 const resume = control.resume;
133 control.resume = null;
134 resume();
135 } else {
136 if (control.asyncifyEnabled) {
137 // The instance is either not suspended or is supended outside of processEvents()
138 // (e.g. on emscripten_sleep()). Currently there is no way to determine
139 // which state the instance is in. Keep the event in the event queue to be
140 // processed on the next processEvents() call.
141 // FIXME: call event handler here if we can determine that the instance
142 // is not suspended.
143 } else {
144 // The instance is not suspended, call the handler directly
145 Module.qtSendPendingEvents();
146 }
147 }
148 };
149 control.eventHandlers[index] = handler;
150 }, index);
151}
152
153QWasmSuspendResumeControl::QWasmSuspendResumeControl()
154{
155#if QT_CONFIG(thread)
156 Q_ASSERT(emscripten_is_main_runtime_thread());
157#endif
158 qtSuspendResumeControlClearJs();
159 suspendResumeControlJs().set("asyncifyEnabled", qstdweb::haveAsyncify());
160 QWasmSuspendResumeControl::s_suspendResumeControl = this;
161}
162
163QWasmSuspendResumeControl::~QWasmSuspendResumeControl()
164{
165 qtSuspendResumeControlClearJs();
166 QWasmSuspendResumeControl::s_suspendResumeControl = nullptr;
167}
168
169QWasmSuspendResumeControl *QWasmSuspendResumeControl::get()
170{
171 Q_ASSERT_X(s_suspendResumeControl, "QWasmSuspendResumeControl", "Must create a QWasmSuspendResumeControl instance first");
172 return s_suspendResumeControl;
173}
174
175// Registers a C++ event handler.
176uint32_t QWasmSuspendResumeControl::registerEventHandler(std::function<void(val)> handler)
177{
178 static uint32_t i = 0;
179 ++i;
180 m_eventHandlers.emplace(i, std::move(handler));
181 qtRegisterEventHandlerJs(i);
182 return i;
183}
184
185// Removes a C++ event handler
186void QWasmSuspendResumeControl::removeEventHandler(uint32_t index)
187{
188 m_eventHandlers.erase(index);
189 suspendResumeControlJs()["eventHandlers"].set(index, val::null());
190}
191
192// Returns the JS event handler for the given index
193val QWasmSuspendResumeControl::jsEventHandlerAt(uint32_t index)
194{
195 return suspendResumeControlJs()["eventHandlers"][index];
196}
197
198emscripten::val QWasmSuspendResumeControl::suspendResumeControlJs()
199{
200 return val::module_property("qtSuspendResumeControl");
201}
202
203// Suspends the calling thread.
204void QWasmSuspendResumeControl::suspend()
205{
206 qtSuspendJs();
207}
208
209void QWasmSuspendResumeControl::suspendExclusive(uint32_t eventHandlerIndex)
210{
211 suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndex);
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 if (control["exclusiveEventHandler"].as<int>() > 0)
225 return sendPendingExclusiveEvent();
226
227 if (pendingEvents["length"].as<int>() == 0)
228 return 0;
229
230 int count = 0;
231 while (pendingEvents["length"].as<int>() > 0) { // Make sure it is reentrant
232 // Grab one event (handler and arg), and call it
233 emscripten::val event = pendingEvents.call<val>("shift");
234 auto it = m_eventHandlers.find(event["index"].as<int>());
235 if (it != m_eventHandlers.end())
236 it->second(event["arg"]);
237 ++count;
238 }
239 return count;
240}
241
242// Sends the pending exclusive event, and resets the "exclusive" state
243int QWasmSuspendResumeControl::sendPendingExclusiveEvent()
244{
245 emscripten::val control = suspendResumeControlJs();
246 int exclusiveHandlerIndex = control["exclusiveEventHandler"].as<int>();
247 control.set("exclusiveEventHandler", 0);
248 emscripten::val event = control["pendingEvents"].call<val>("pop");
249 int eventHandlerIndex = event["index"].as<int>();
250 Q_ASSERT(exclusiveHandlerIndex == eventHandlerIndex);
251 auto it = m_eventHandlers.find(eventHandlerIndex);
252 Q_ASSERT(it != m_eventHandlers.end());
253 it->second(event["arg"]);
254 return 1;
255}
256
258{
259 if (QWasmSuspendResumeControl::s_suspendResumeControl)
260 QWasmSuspendResumeControl::s_suspendResumeControl->sendPendingEvents();
261}
262
264 emscripten::function("qtSendPendingEvents", qtSendPendingEvents QT_WASM_EMSCRIPTEN_ASYNC);
265}
266
267//
268// The EventCallback class registers a callback function for an event on an html element.
269//
270QWasmEventHandler::QWasmEventHandler(emscripten::val element, const std::string &name, std::function<void(emscripten::val)> handler)
271:m_element(element)
272,m_name(name)
273{
274 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
275 Q_ASSERT(suspendResume); // must construct the event dispatcher or platform integration first
276 m_eventHandlerIndex = suspendResume->registerEventHandler(std::move(handler));
277 m_element.call<void>("addEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
278}
279
280QWasmEventHandler::~QWasmEventHandler()
281{
282 // Do nothing if this instance is default-constructed, or was moved from.
283 if (m_element.isUndefined())
284 return;
285
286 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
287 Q_ASSERT(suspendResume);
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 m_suspendResume->removeEventHandler(m_handlerIndex);
337}
338
339void QWasmTimer::setTimeout(std::chrono::milliseconds timeout)
340{
341 if (hasTimeout())
343 val jsHandler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
344 using ArgType = double; // emscripten::val::call() does not support int64_t
345 ArgType timoutValue = static_cast<ArgType>(timeout.count());
346 ArgType timerId = val::global("window").call<ArgType>("setTimeout", jsHandler, timoutValue);
347 m_timerId = static_cast<int64_t>(std::round(timerId));
348}
349
351{
352 return m_timerId > 0;
353}
354
356{
357 val::global("window").call<void>("clearTimeout", double(m_timerId));
358 m_timerId = 0;
359}
void setTimeout(std::chrono::milliseconds timeout)
QWasmTimer(QWasmSuspendResumeControl *suspendResume, std::function< void()> handler)
#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()