Source code

Revision control

Copy as Markdown

// Copyright (C) 2018 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
#include "qqmlinplacepreviewhandler.h"
#include "qqmlpreviewservice.h"
#include <QtGui/qguiapplication.h>
#include <QtQml/qqmlcomponent.h>
#include <QtQuick/qquickwindow.h>
#include <QtQuick/qquickitem.h>
#include <private/qqmlmetatype_p.h>
#include <private/qv4compileddata_p.h>
#include <private/qqmlpreviewobjectpatch_p.h>
#include <private/qv4mm_p.h>
#include <private/qqmlcomponent_p.h>
#include <private/qv4resolvedtypereference_p.h>
QT_BEGIN_NAMESPACE
struct ComponentUpdate
{
std::unique_ptr<QQmlComponent> component;
QQmlRefPointer<QV4::ExecutableCompilationUnit> oldUnit;
QQmlRefPointer<QV4::ExecutableCompilationUnit> newUnit;
QQmlPreview::PatchResult patchResult = QQmlPreview::PatchResult::Failed;
};
struct InplaceUpdate
{
InplaceUpdate(QQmlInPlacePreviewHandler *handler, QQmlEngine *engine)
: engine(engine), handler(handler)
{
}
QQmlEngine *engine = nullptr;
std::vector<ComponentUpdate> pendingComponentUpdates;
std::vector<ComponentUpdate> processedComponentUpdates;
QList<QQmlRefPointer<QV4::CompiledData::CompilationUnit>> droppedUnits;
void emitReloadFailure(const QString &message) { emit handler->hotReloadFailure(message); }
void emitError(const QString &message) { emit handler->error(message); }
private:
// Sending a signal is fine from a different thread. Anything else not so much.
QQmlInPlacePreviewHandler *handler = nullptr;
};
QQmlInPlacePreviewHandler::QQmlInPlacePreviewHandler(QObject *parent) : QQmlPreviewHandler(parent)
{
}
QQmlInPlacePreviewHandler::~QQmlInPlacePreviewHandler() = default;
void QQmlInPlacePreviewHandler::connectToService(QQmlPreviewServiceImpl *service)
{
QQmlPreviewHandler::connectToService(service);
connect(this, &QQmlInPlacePreviewHandler::hotReloadFailure, service,
&QQmlPreviewServiceImpl::forwardHotReloadFailure, Qt::DirectConnection);
connect(service, &QQmlPreviewServiceImpl::drop, this, [this](const QUrl &url) {
if (!m_droppedUrls.contains(url))
m_droppedUrls.push_back(url);
});
connect(service, &QQmlPreviewServiceImpl::rerun, this, [this]() {
emit error(QLatin1String("You cannot rerun if in-place updates are enabled."));
});
connect(service, &QQmlPreviewServiceImpl::zoom, this, [this](qreal zoomFactor) {
QQmlPreviewPosition position;
zoomWindow(currentWindow(), zoomFactor, &position);
});
}
QQuickWindow *findCurrentWindow()
{
QQuickWindow *found = nullptr;
for (QWindow *window : QGuiApplication::allWindows()) {
if (QQuickWindow *quickWindow = qobject_cast<QQuickWindow *>(window)) {
if (std::exchange(found, quickWindow))
return nullptr; // Multiple windows available. We can't decide
}
}
return found;
}
void findCurrentRootObject(QQmlEngine *engine, const QUrl &url, QQmlPreviewHandler *receiver)
{
// Schedule this on the engine's thread
QMetaObject::invokeMethod(engine, [engine, url, receiver]() {
QV4::ExecutionEngine *v4 = engine->handle();
const QQmlRefPointer<QV4::ExecutableCompilationUnit> unit = v4->compilationUnitForUrl(url);
if (!unit)
return;
const std::vector<QObject *> objects =
v4->memoryManager->findObjectsForCompilationUnits({ unit->baseCompilationUnit() });
QPointer<QObject> found;
for (QObject *object : objects) {
QQmlData *ddata = QQmlData::get(object);
if (ddata && ddata->compilationUnit == unit && ddata->cuObjectIndex == 0) {
if (std::exchange(found, object))
return; // Multiple candidates. We can't decide
}
}
// Schedule reply on the debug server thread
QMetaObject::invokeMethod(receiver, [receiver, currentRoot = std::move(found)]() {
if (QQuickItem *rootItem = qobject_cast<QQuickItem *>(currentRoot)) {
receiver->setCurrentRootItem(rootItem);
receiver->setCurrentWindow(findCurrentWindow());
return;
}
if (QQuickWindow *window = qobject_cast<QQuickWindow *>(currentRoot)) {
receiver->setCurrentWindow(window);
receiver->setCurrentRootItem(nullptr);
return;
}
receiver->setCurrentRootItem(nullptr);
receiver->setCurrentWindow(findCurrentWindow());
});
});
}
// Replace resolved type references that still point to any of the dropped (old) base CUs with
// freshly compiled ones.
static void updateResolvedTypeReferences(
QV4::ExecutionEngine *v4,
const QList<QQmlRefPointer<QV4::CompiledData::CompilationUnit>> &droppedUnits)
{
for (const auto &oldCU : droppedUnits) {
if (const auto newCU = QQmlMetaType::obtainCompilationUnit(oldCU->finalUrl()))
QQmlPreview::redirectResolvedTypeReferences(v4, oldCU, newCU);
}
}
static void updateInplace(QQmlComponent *component, std::shared_ptr<InplaceUpdate> inplaceUpdate)
{
ComponentUpdate componentUpdate;
const auto componentUpdateIt =
std::find_if(inplaceUpdate->pendingComponentUpdates.begin(),
inplaceUpdate->pendingComponentUpdates.end(),
[component](const ComponentUpdate &engineUpdate) {
return engineUpdate.component.get() == component;
});
if (componentUpdateIt == inplaceUpdate->pendingComponentUpdates.end())
return;
componentUpdate = std::move(*componentUpdateIt);
inplaceUpdate->pendingComponentUpdates.erase(componentUpdateIt);
switch (component->status()) {
case QQmlComponent::Null:
case QQmlComponent::Loading:
Q_UNREACHABLE_RETURN();
case QQmlComponent::Ready:
break;
case QQmlComponent::Error:
inplaceUpdate->emitError(component->errorString());
return;
}
QV4::ExecutionEngine *v4 = component->engine()->handle();
if (const auto oldExecCU = componentUpdate.oldUnit) {
const auto newExecCU = QQmlComponentPrivate::get(component)->compilationUnit();
std::vector<QObject *> objects = v4->memoryManager->findObjectsForCompilationUnits(
{ oldExecCU->baseCompilationUnit() });
const QQmlPreview::PatchResult result =
QQmlPreview::applyDiff(objects, oldExecCU, newExecCU);
if (result == QQmlPreview::PatchResult::Failed)
inplaceUpdate->emitReloadFailure("Could not apply diff");
componentUpdate.newUnit = newExecCU;
componentUpdate.patchResult = result;
inplaceUpdate->processedComponentUpdates.push_back(std::move(componentUpdate));
}
// Once all components have been handled, update resolved type references in all
// compilation units and refresh all bindings
if (inplaceUpdate->pendingComponentUpdates.empty()) {
updateResolvedTypeReferences(v4, inplaceUpdate->droppedUnits);
for (const ComponentUpdate &update : inplaceUpdate->processedComponentUpdates) {
// Objects patched in place keep their original (still-live) expressions; translate
// those to the new unit. Rebuilt roots only leave dead expressions behind; null them.
QQmlPreview::refreshBindings(
update.oldUnit,
update.patchResult == QQmlPreview::PatchResult::PatchedInPlace ? update.newUnit
: nullptr);
}
}
}
void updateEngine(std::shared_ptr<InplaceUpdate> inplaceUpdate, const QList<QUrl> &urls)
{
QV4::ExecutionEngine *v4 = inplaceUpdate->engine->handle();
std::vector<QQmlComponent *> components;
QQmlTypeLoader *typeLoader = v4->typeLoader();
for (const QUrl &url : urls) {
// Any file the application requests from the preview client is reported as dropped.
// Only QML documents can be recompiled as components. Scripts and qmldir files are merely
// evicted from the cache.
const bool isQmlDocument = typeLoader->isTypeLoaded(QQmlMetaType::normalizedUrl(url));
// First remove any cached instance of the CU
// NB: Don't remove from the ExecutionEngine's list of CUs. We need those for the GC.
if (!typeLoader->removeFromCache(url) || !isQmlDocument)
continue;
// Hold on to the old unit.
QQmlRefPointer<QV4::ExecutableCompilationUnit> oldUnit = v4->compilationUnitForUrl(url);
// Then have it re-compile with updated source code. newUnit and patchResult are filled
// in by updateInplace() once the component has recompiled and the diff has been applied.
inplaceUpdate->pendingComponentUpdates.push_back({
std::make_unique<QQmlComponent>(inplaceUpdate->engine, url),
std::move(oldUnit),
{},
QQmlPreview::PatchResult::Failed,
});
// Additionally store in another vector since updateInplace deletes from inplaceUpdate
components.push_back(inplaceUpdate->pendingComponentUpdates.back().component.get());
}
for (QQmlComponent *component : components) {
switch (component->status()) {
case QQmlComponent::Null:
case QQmlComponent::Loading:
QObject::connect(component, &QQmlComponent::statusChanged, component,
[component, inplaceUpdate]() {
switch (component->status()) {
case QQmlComponent::Ready:
case QQmlComponent::Error:
updateInplace(component, inplaceUpdate);
break;
default:
break;
}
});
break;
case QQmlComponent::Ready:
case QQmlComponent::Error:
updateInplace(component, inplaceUpdate);
break;
}
}
}
void QQmlInPlacePreviewHandler::load(const QUrl &url)
{
// Find the updated objects from the URLs we were asked to drop. Those are the ones that have
// been updated.
const QList<QQmlEngine *> seenEngines = engines();
const QList<QUrl> urls = std::exchange(m_droppedUrls, {});
QList<QQmlRefPointer<QV4::CompiledData::CompilationUnit>> droppedUnits;
for (const QUrl &droppedUrl : urls) {
while (const auto cu = QQmlMetaType::obtainCompilationUnit(droppedUrl)) {
droppedUnits.append(cu);
QQmlMetaType::deepClearCompositeType(cu);
}
}
for (QQmlEngine *engine : seenEngines) {
std::shared_ptr<InplaceUpdate> inplaceUpdate =
std::make_shared<InplaceUpdate>(this, engine);
inplaceUpdate->droppedUnits = droppedUnits;
// Schedule this on the engine's thread. The shared pointer keeps the update alive as
// long as necessary.
QMetaObject::invokeMethod(engine, [update = std::move(inplaceUpdate), urls]() {
updateEngine(update, urls);
});
}
// Update current window and root item based on passed URL
// so that we can zoom and report FPS.
// We can only do that if we have an unambiguous root item or window.
if (seenEngines.size() == 1) {
findCurrentRootObject(seenEngines[0], url, this);
} else {
setCurrentRootItem(nullptr);
setCurrentWindow(findCurrentWindow());
return;
}
}
QT_END_NAMESPACE
#include "moc_qqmlinplacepreviewhandler.cpp"