Revision control
Copy as Markdown
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qwasmsuspendresumecontrol_p.h"
#include "qstdweb_p.h"
#include <QtCore/qapplicationstatic.h>
#include <QtCore/qdebug.h>
#include <emscripten.h>
#include <emscripten/val.h>
#include <emscripten/bind.h>
using emscripten::val;
/*
QWasmSuspendResumeControl controls asyncify suspend and resume when handling native events.
The class supports registering C++ event handlers, and creates a corresponding
JavaScript event handler which can be passed to addEventListener() or similar
API:
auto handler = [](emscripten::val argument){
// handle event
};
uint32_t index = control->registerEventHandler(handler);
element.call<void>("addEventListener", "eventname", control->jsEventHandlerAt(index));
The wasm instance suspends itself by calling the suspend() function, which resumes
and returns whenever there was a native event. Call sendPendingEvents() to send
the native event and invoke the C++ event handlers.
// about to suspend
control->suspend(); // <- instance/app sleeps here
// was resumed, send event(s)
control->sendPendingEvents();
QWasmSuspendResumeControl also supports the case where the wasm instance returns
control to the browser's event loop (without suspending), and will call the C++
event handlers directly in that case.
*/
Q_GLOBAL_STATIC(QWasmSuspendResumeControl, s_suspendResumeControl);
// Setup/constructor function for Module.suspendResumeControl.
// FIXME if assigning to the Module object from C++ is/becomes possible
// then this does not need to be a separate JS function.
void qtSuspendResumeControlClearJs() {
EM_ASM({
Module.qtSuspendResumeControl = ({
resume: null,
asyncifyEnabled: false, // asyncify 1 or JSPI enabled
eventHandlers: {},
pendingEvents: [],
exclusiveEventHandler: 0,
});
});
}
// Suspends the calling thread
EM_ASYNC_JS(void, qtSuspendJs, (), {
return new Promise(resolve => {
Module.qtSuspendResumeControl.resume = resolve;
});
});
// Registers a JS event handler which when called registers its index
// as the "current" event handler, and then resumes the wasm instance.
// The wasm instance will then call the C++ event after it is resumed.
void qtRegisterEventHandlerJs(int index) {
EM_ASM({
// Create a shallow copy of an event by copying its properties
// to a new object.
function snapshotEvent(event) {
// Return non-events (and null) as-is
if (!(event instanceof Event))
return event;
// Create event copy
const copy = { isInstanceOfEvent: true };
for (const key in event) {
const value = event[key];
copy[key] = typeof value === 'function' ? value.bind(event) : value;
}
return copy;
}
let index = $0;
let control = Module.qtSuspendResumeControl;
let handler = (arg) => {
// Create a snapshot of the event for the queue. Browsers
// may recycle the event objects once control returns which
// means we can't store referenes to them in the queue.
arg = snapshotEvent(arg);
// Add event to event queue
control.pendingEvents.push({
index: index,
arg: arg
});
// Handle the event based on instance state and asyncify flag
if (control.exclusiveEventHandler > 0) {
// In exclusive mode, resume on exclusive event handler match only
if (index != control.exclusiveEventHandler)
return;
const resume = control.resume;
control.resume = null;
resume();
} else if (control.resume) {
// The instance is suspended in processEvents(), resume and process the event
const resume = control.resume;
control.resume = null;
resume();
} else {
if (control.asyncifyEnabled) {
// The instance is either not suspended or is supended outside of processEvents()
// (e.g. on emscripten_sleep()). Currently there is no way to determine
// which state the instance is in. Keep the event in the event queue to be
// processed on the next processEvents() call.
// FIXME: call event handler here if we can determine that the instance
// is not suspended.
} else {
// The instance is not suspended, call the handler directly
Module.qtSendPendingEvents();
}
}
};
control.eventHandlers[index] = handler;
}, index);
}
QWasmSuspendResumeControl::QWasmSuspendResumeControl()
{
#if QT_CONFIG(thread)
Q_ASSERT(emscripten_is_main_runtime_thread());
#endif
qtSuspendResumeControlClearJs();
suspendResumeControlJs().set("asyncifyEnabled", qstdweb::haveAsyncify());
}
QWasmSuspendResumeControl::~QWasmSuspendResumeControl()
{
if (!m_eventHandlers.empty())
qWarning() << "QWasmSuspendResumeControl::~QWasmSuspendResumeControl - still remaining " << m_eventHandlers.size() << " handlers";
qtSuspendResumeControlClearJs();
}
QWasmSuspendResumeControl *QWasmSuspendResumeControl::get()
{
if (!s_suspendResumeControl)
qFatal("QWasmSuspendResumeControl -- Object not created/destroyed");
return s_suspendResumeControl;
}
// Registers a C++ event handler.
uint32_t QWasmSuspendResumeControl::registerEventHandler(std::function<void(val)> handler)
{
static uint32_t i = 0;
++i;
m_eventHandlers.emplace(i, std::move(handler));
qtRegisterEventHandlerJs(i);
return i;
}
// Removes a C++ event handler
void QWasmSuspendResumeControl::removeEventHandler(uint32_t index)
{
m_eventHandlers.erase(index);
suspendResumeControlJs()["eventHandlers"].set(index, val::null());
}
// Returns the JS event handler for the given index
val QWasmSuspendResumeControl::jsEventHandlerAt(uint32_t index)
{
return suspendResumeControlJs()["eventHandlers"][index];
}
emscripten::val QWasmSuspendResumeControl::suspendResumeControlJs()
{
return val::module_property("qtSuspendResumeControl");
}
// Suspends the calling thread.
void QWasmSuspendResumeControl::suspend()
{
if (!qstdweb::canBlockCallingThread()) {
qFatal("Suspending the main thread requires asyncify or JSPI; "
"see the Qt for WebAssembly documentation for how to enable.");
}
qtSuspendJs();
}
void QWasmSuspendResumeControl::suspendExclusive(QList<uint32_t> eventHandlerIndices)
{
if (!qstdweb::canBlockCallingThread()) {
qFatal("Suspending the main thread requires asyncify or JSPI; "
"see the Qt for WebAssembly documentation for how to enable.");
}
m_eventFilter = [eventHandlerIndices](int handler) {
return eventHandlerIndices.contains(handler);
};
suspendResumeControlJs().set("exclusiveEventHandler", eventHandlerIndices.back());
qtSuspendJs();
}
// Sends any pending events. Returns the number of sent events.
int QWasmSuspendResumeControl::sendPendingEvents()
{
#if QT_CONFIG(thread)
Q_ASSERT(emscripten_is_main_runtime_thread());
#endif
emscripten::val control = suspendResumeControlJs();
emscripten::val pendingEvents = control["pendingEvents"];
int count = 0;
for (int i = 0; i < pendingEvents["length"].as<int>();) {
if (!m_eventFilter(pendingEvents[i]["index"].as<int>())) {
++i;
} else {
// Grab one event (handler and arg), and call it
emscripten::val event = pendingEvents[i];
pendingEvents.call<void>("splice", i, 1);
auto it = m_eventHandlers.find(event["index"].as<int>());
if (it != m_eventHandlers.end()) {
setCurrentEvent(event["arg"]);
it->second(currentEvent());
setCurrentEvent(emscripten::val::undefined());
}
++count;
}
}
if (control["exclusiveEventHandler"].as<int>() > 0) {
control.set("exclusiveEventHandler", 0);
m_eventFilter = [](int) { return true;};
}
return count;
}
void qtSendPendingEvents()
{
if (s_suspendResumeControl)
s_suspendResumeControl->sendPendingEvents();
}
EMSCRIPTEN_BINDINGS(qtSuspendResumeControl) {
emscripten::function("qtSendPendingEvents", qtSendPendingEvents QT_WASM_EMSCRIPTEN_ASYNC);
}
//
// The EventCallback class registers a callback function for an event on an html element.
//
QWasmEventHandler::QWasmEventHandler(emscripten::val element, const std::string &name, std::function<void(emscripten::val)> handler)
:m_element(element)
,m_name(name)
{
QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
m_eventHandlerIndex = suspendResume->registerEventHandler(std::move(handler));
m_element.call<void>("addEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
}
QWasmEventHandler::~QWasmEventHandler()
{
// Do nothing if this instance is default-constructed, or was moved from.
if (m_element.isUndefined())
return;
QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
m_element.call<void>("removeEventListener", m_name, suspendResume->jsEventHandlerAt(m_eventHandlerIndex));
suspendResume->removeEventHandler(m_eventHandlerIndex);
}
QWasmEventHandler::QWasmEventHandler(QWasmEventHandler&& other) noexcept
:m_element(std::move(other.m_element))
,m_name(std::move(other.m_name))
,m_eventHandlerIndex(other.m_eventHandlerIndex)
{
other.m_element = emscripten::val();
other.m_name = emscripten::val();
other.m_eventHandlerIndex = 0;
}
QWasmEventHandler& QWasmEventHandler::operator=(QWasmEventHandler&& other) noexcept
{
m_element = std::move(other.m_element);
other.m_element = emscripten::val();
m_name = std::move(other.m_name);
other.m_name = emscripten::val();
m_eventHandlerIndex = other.m_eventHandlerIndex;
other.m_eventHandlerIndex = 0;
return *this;
}
//
// The QWasmTimer class creates a native single-shot timer. The event handler is provided in the
// constructor and can be reused: each call setTimeout() sets a new timeout, though with the
// limitiation that there can be only one timeout at a time. (Setting a new timer clears the
// previous one).
//
QWasmTimer::QWasmTimer(QWasmSuspendResumeControl *suspendResume, std::function<void()> handler)
:m_suspendResume(suspendResume)
{
auto wrapper = [handler = std::move(handler), this](val argument) {
Q_UNUSED(argument); // no argument for timers
if (!m_timerId)
return; // timer was cancelled
m_timerId = 0;
handler();
};
m_handlerIndex = m_suspendResume->registerEventHandler(std::move(wrapper));
}
QWasmTimer::~QWasmTimer()
{
clearTimeout();
// We lack a test that checks validity of m_suspendResume
m_suspendResume->removeEventHandler(m_handlerIndex);
}
void QWasmTimer::setTimeout(std::chrono::milliseconds timeout)
{
Q_ASSERT(m_suspendResume == QWasmSuspendResumeControl::get());
if (hasTimeout())
clearTimeout();
val jsHandler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
using ArgType = double; // emscripten::val::call() does not support int64_t
ArgType timoutValue = static_cast<ArgType>(timeout.count());
ArgType timerId = val::global("window").call<ArgType>("setTimeout", jsHandler, timoutValue);
m_timerId = static_cast<int64_t>(std::round(timerId));
}
bool QWasmTimer::hasTimeout()
{
return m_timerId > 0;
}
void QWasmTimer::clearTimeout()
{
val::global("window").call<void>("clearTimeout", double(m_timerId));
m_timerId = 0;
}
//
// QWasmAnimationFrameMultiHandler
//
// Multiplexes multiple animate and draw callbacks to a single native requestAnimationFrame call.
// Animate callbacks are called before draw callbacks to ensure animations are advanced before drawing.
//
QWasmAnimationFrameMultiHandler::QWasmAnimationFrameMultiHandler()
{
auto wrapper = [this](val arg) {
handleAnimationFrame(arg.as<double>());
};
m_handlerIndex = QWasmSuspendResumeControl::get()->registerEventHandler(wrapper);
}
QWasmAnimationFrameMultiHandler::~QWasmAnimationFrameMultiHandler()
{
cancelAnimationFrameRequest();
QWasmSuspendResumeControl::get()->removeEventHandler(m_handlerIndex);
}
Q_GLOBAL_STATIC(QWasmAnimationFrameMultiHandler, s_animationFrameHandler);
QWasmAnimationFrameMultiHandler *QWasmAnimationFrameMultiHandler::instance()
{
return s_animationFrameHandler();
}
// Registers a permanent animation callback. Call unregisterAnimateCallback() to unregister
uint32_t QWasmAnimationFrameMultiHandler::registerAnimateCallback(Callback callback)
{
uint32_t handle = ++m_nextAnimateHandle;
m_animateCallbacks[handle] = std::move(callback);
ensureAnimationFrameRequested();
return handle;
}
// Registers a single-shot draw callback.
uint32_t QWasmAnimationFrameMultiHandler::registerDrawCallback(Callback callback)
{
uint32_t handle = ++m_nextDrawHandle;
m_drawCallbacks[handle] = std::move(callback);
ensureAnimationFrameRequested();
return handle;
}
void QWasmAnimationFrameMultiHandler::unregisterAnimateCallback(uint32_t handle)
{
m_animateCallbacks.erase(handle);
if (m_animateCallbacks.empty() && m_drawCallbacks.empty())
cancelAnimationFrameRequest();
}
void QWasmAnimationFrameMultiHandler::unregisterDrawCallback(uint32_t handle)
{
m_drawCallbacks.erase(handle);
if (m_animateCallbacks.empty() && m_drawCallbacks.empty())
cancelAnimationFrameRequest();
}
void QWasmAnimationFrameMultiHandler::handleAnimationFrame(double timestamp)
{
m_requestId = -1;
// Advance animations. Copy the callbacks list in case callbacks are
// unregistered during iteration
auto animateCallbacksCopy = m_animateCallbacks;
for (const auto &pair : animateCallbacksCopy)
pair.second(timestamp);
// Draw the frame. Note that draw callbacks are cleared after each
// frame, matching QWindow::requestUpdate() behavior. Copy the callbacks
// list in case new callbacks are registered while drawing the frame
auto drawCallbacksCopy = m_drawCallbacks;
m_drawCallbacks.clear();
for (const auto &pair : drawCallbacksCopy)
pair.second(timestamp);
// Request next frame if there are still callbacks registered
if (!m_animateCallbacks.empty() || !m_drawCallbacks.empty())
ensureAnimationFrameRequested();
}
void QWasmAnimationFrameMultiHandler::ensureAnimationFrameRequested()
{
if (m_requestId != -1)
return;
using ReturnType = double;
val handler = QWasmSuspendResumeControl::get()->jsEventHandlerAt(m_handlerIndex);
m_requestId = int64_t(val::global("window").call<ReturnType>("requestAnimationFrame", handler));
}
void QWasmAnimationFrameMultiHandler::cancelAnimationFrameRequest()
{
if (m_requestId == -1)
return;
val::global("window").call<void>("cancelAnimationFrame", double(m_requestId));
m_requestId = -1;
}