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 });
53 });
54}
55
56// Suspends the calling thread
58 return new Promise(resolve => {
60 });
61});
62
63// Registers a JS event handler which when called registers its index
64// as the "current" event handler, and then resumes the wasm instance.
65// The wasm instance will then call the C++ event after it is resumed.
66void qtRegisterEventHandlerJs(int index) {
67 EM_ASM({
68
69 function createNamedFunction(name, parent, obj) {
70 return {
71 [name]: function(...args) {
72 return obj.call(parent, args);
73 }
74 }[name];
75 }
76
77 function deepShallowClone(parent, obj, depth) {
78 if (obj === null)
79 return obj;
80
81 if (typeof obj === 'function') {
82 if (obj.name !== "")
83 return createNamedFunction(obj.name, parent, obj);
84 }
85
86 if (depth >= 1)
87 return obj;
88
89 if (typeof obj !== 'object')
90 return obj;
91
92 if (Array.isArray(obj)) {
93 const arrCopy = [];
94 for (let i = 0; i < obj.length; i++)
95 arrCopy[i] = deepShallowClone(obj, obj[i], depth + 1);
96
97 return arrCopy;
98 }
99
100 const objCopy = {};
101 for (const key in obj)
102 objCopy[key] = deepShallowClone(obj, obj[key], depth + 1);
103
104 return objCopy;
105 }
106
107 let index = $0;
108 let control = Module.qtSuspendResumeControl;
109 let handler = (arg) => {
110 // Copy the top level object, alias the rest.
111 // functions are copied by creating new forwarding functions.
112 arg = deepShallowClone(arg, arg, 0);
113
114 // Add event to event queue
115 control.pendingEvents.push({
116 index: index,
117 arg: arg
118 });
119
120 // Handle the event based on instance state and asyncify flag
121 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 Q_ASSERT(!QWasmSuspendResumeControl::s_suspendResumeControl);
152 QWasmSuspendResumeControl::s_suspendResumeControl = this;
153}
154
155QWasmSuspendResumeControl::~QWasmSuspendResumeControl()
156{
157 qtSuspendResumeControlClearJs();
158 Q_ASSERT(QWasmSuspendResumeControl::s_suspendResumeControl);
159 QWasmSuspendResumeControl::s_suspendResumeControl = nullptr;
160}
161
162QWasmSuspendResumeControl *QWasmSuspendResumeControl::get()
163{
164 Q_ASSERT_X(s_suspendResumeControl, "QWasmSuspendResumeControl", "Must create a QWasmSuspendResumeControl instance first");
165 return s_suspendResumeControl;
166}
167
168// Registers a C++ event handler.
169uint32_t QWasmSuspendResumeControl::registerEventHandler(std::function<void(val)> handler)
170{
171 static uint32_t i = 0;
172 ++i;
173 m_eventHandlers.emplace(i, std::move(handler));
174 qtRegisterEventHandlerJs(i);
175 return i;
176}
177
178// Removes a C++ event handler
179void QWasmSuspendResumeControl::removeEventHandler(uint32_t index)
180{
181 m_eventHandlers.erase(index);
182 suspendResumeControlJs()["eventHandlers"].set(index, val::null());
183}
184
185// Returns the JS event handler for the given index
186val QWasmSuspendResumeControl::jsEventHandlerAt(uint32_t index)
187{
188 return suspendResumeControlJs()["eventHandlers"][index];
189}
190
191emscripten::val QWasmSuspendResumeControl::suspendResumeControlJs()
192{
193 return val::module_property("qtSuspendResumeControl");
194}
195
196// Suspends the calling thread.
197void QWasmSuspendResumeControl::suspend()
198{
199 qtSuspendJs();
200}
201
202// Sends any pending events. Returns true if an event was sent, false otherwise.
203int QWasmSuspendResumeControl::sendPendingEvents()
204{
205#if QT_CONFIG(thread)
206 Q_ASSERT(emscripten_is_main_runtime_thread());
207#endif
208 emscripten::val pendingEvents = suspendResumeControlJs()["pendingEvents"];
209 if (pendingEvents["length"].as<int>() == 0)
210 return 0;
211
212 int count = 0;
213 while (pendingEvents["length"].as<int>() > 0) { // Make sure it is reentrant
214 // Grab one event (handler and arg), and call it
215 emscripten::val event = pendingEvents.call<val>("shift");
216 auto it = m_eventHandlers.find(event["index"].as<int>());
217 Q_ASSERT(it != m_eventHandlers.end());
218 it->second(event["arg"]);
219 ++count;
220 }
221 return count;
222}
223
225{
226 if (QWasmSuspendResumeControl::s_suspendResumeControl)
227 QWasmSuspendResumeControl::s_suspendResumeControl->sendPendingEvents();
228}
229
231 emscripten::function("qtSendPendingEvents", qtSendPendingEvents QT_WASM_EMSCRIPTEN_ASYNC);
232}
233
234//
235// The EventCallback class registers a callback function for an event on an html element.
236//
237QWasmEventHandler::QWasmEventHandler(emscripten::val element, const std::string &name, std::function<void(emscripten::val)> handler)
238:m_element(element)
239,m_name(name)
240{
241 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
242 Q_ASSERT(suspendResume); // must construct the event dispatcher or platform integration first
243 m_eventHandlerIndex = suspendResume->registerEventHandler(std::move(handler));
244 m_element.call<void>("addEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
245}
246
247QWasmEventHandler::~QWasmEventHandler()
248{
249 // Do nothing if this instance is default-constructed, or was moved from.
250 if (m_element.isUndefined())
251 return;
252
253 QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
254 Q_ASSERT(suspendResume);
255 m_element.call<void>("removeEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
256 suspendResume->removeEventHandler(m_eventHandlerIndex);
257}
258
259QWasmEventHandler::QWasmEventHandler(QWasmEventHandler&& other) noexcept
260:m_element(std::move(other.m_element))
261,m_name(std::move(other.m_name))
262,m_eventHandlerIndex(other.m_eventHandlerIndex)
263{
264 other.m_element = emscripten::val();
265 other.m_name = emscripten::val();
266 other.m_eventHandlerIndex = 0;
267}
268
269QWasmEventHandler& QWasmEventHandler::operator=(QWasmEventHandler&& other) noexcept
270{
271 m_element = std::move(other.m_element);
272 other.m_element = emscripten::val();
273 m_name = std::move(other.m_name);
274 other.m_name = emscripten::val();
275 m_eventHandlerIndex = other.m_eventHandlerIndex;
276 other.m_eventHandlerIndex = 0;
277 return *this;
278}
279
280//
281// The QWasmTimer class creates a native single-shot timer. The event handler is provided in the
282// constructor and can be reused: each call setTimeout() sets a new timeout, though with the
283// limitiation that there can be only one timeout at a time. (Setting a new timer clears the
284// previous one).
285//
286QWasmTimer::QWasmTimer(QWasmSuspendResumeControl *suspendResume, std::function<void()> handler)
288{
289 auto wrapper = [handler = std::move(handler), this](val argument) {
290 Q_UNUSED(argument); // no argument for timers
291 if (!m_timerId)
292 return; // timer was cancelled
293 m_timerId = 0;
294 handler();
295 };
296
297 m_handlerIndex = m_suspendResume->registerEventHandler(std::move(wrapper));
298}
299
301{
303 m_suspendResume->removeEventHandler(m_handlerIndex);
304}
305
306void QWasmTimer::setTimeout(std::chrono::milliseconds timeout)
307{
308 if (hasTimeout())
310 val jsHandler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
311 using ArgType = double; // emscripten::val::call() does not support int64_t
312 ArgType timoutValue = static_cast<ArgType>(timeout.count());
313 ArgType timerId = val::global("window").call<ArgType>("setTimeout", jsHandler, timoutValue);
314 m_timerId = static_cast<int64_t>(std::round(timerId));
315}
316
318{
319 return m_timerId > 0;
320}
321
323{
324 val::global("window").call<void>("clearTimeout", double(m_timerId));
325 m_timerId = 0;
326}
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()