Revision control

Copy as Markdown

// Copyright (C) 2026 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 "qqmlpreviewobjectpatch_p.h"
#include <private/qqmlcomponent_p.h>
#include <private/qqmlcontextdata_p.h>
#include <private/qqmldata_p.h>
#include <private/qqmljavascriptexpression_p.h>
#include <private/qqmlobjectcreator_p.h>
#include <private/qqmlpreviewbindingpatchcontext_p.h>
#include <private/qqmlpreviewdiff_p.h>
#include <private/qqmlpropertyresolver_p.h>
#include <private/qqmlscriptdata_p.h>
#include <private/qqmlsignalnames_p.h>
#include <private/qqmlvme_p.h>
#include <private/qqmlvmemetaobject_p.h>
#include <private/qv4functionobject_p.h>
#include <private/qv4generatorobject_p.h>
#include <private/qv4qmlcontext_p.h>
#include <private/qv4resolvedtypereference_p.h>
#include <QtQml/qqmlcomponent.h>
#include <QtQml/qqmlproperty.h>
#include <QtCore/qset.h>
QT_BEGIN_NAMESPACE
namespace QQmlPreview {
// The indices at which object appears in oldUnit: its own (ddata) index plus any indices it
// occupies through composite base levels (the VME meta-object chain).
static QVarLengthArray<int, 4>
objectIndices(QObject *object, const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit)
{
QVarLengthArray<int, 4> objectIndices;
QQmlData *ddata = QQmlData::get(object);
if (!ddata)
return objectIndices;
if (ddata->compilationUnit == oldUnit)
objectIndices.push_back(ddata->cuObjectIndex);
if (ddata->hasVMEMetaObject) {
for (QQmlVMEMetaObject *vme =
static_cast<QQmlVMEMetaObject *>(QObjectPrivate::get(object)->metaObject);
vme; vme = vme->parentVMEMetaObject()) {
if (vme->compilationUnit() == oldUnit)
objectIndices.push_back(vme->qmlObjectId());
}
}
return objectIndices;
}
static QQmlPropertyCache::ConstPtr
nonCompositeBaseType(const QQmlPropertyCache::ConstPtr &propertyCache)
{
for (QQmlPropertyCache::ConstPtr parent = propertyCache; parent; parent = parent->parent()) {
if (!parent->isComposite())
return parent;
}
return QQmlPropertyCache::ConstPtr();
}
static bool
hasChangedNonCompositeBaseType(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit,
int objectIndex)
{
const QV4::CompiledData::Object *oldObj = oldUnit->objectAt(objectIndex);
const auto *oldTypeRef = oldUnit->resolvedType(oldObj->inheritedTypeNameIndex);
if (!oldTypeRef)
return false; // Grouped property sub-objects have no inherited type.
const QV4::CompiledData::Object *newObj = newUnit->objectAt(objectIndex);
const auto *newTypeRef = newUnit->resolvedType(newObj->inheritedTypeNameIndex);
if (!newTypeRef)
return true; // Type disappeared — definitely changed.
return nonCompositeBaseType(oldTypeRef->typePropertyCache())
!= nonCompositeBaseType(newTypeRef->typePropertyCache());
}
// Whether the object at objectIndex is the root of a component: The document, an inline component,
// or an explicit or implicit Component.
static bool isComponentRoot(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &unit,
int objectIndex)
{
if (objectIndex == 0)
return true;
if (objectIndex >= unit->objectCount())
return false;
if (unit->objectAt(objectIndex)->hasFlag(QV4::CompiledData::Object::IsInlineComponentRoot))
return true;
if (unit->baseCompilationUnit()->implicitComponentForObject(objectIndex) >= 0)
return true;
for (int i = 0, end = unit->objectCount(); i < end; ++i) {
const QV4::CompiledData::Object *object = unit->objectAt(i);
if (object->hasFlag(QV4::CompiledData::Object::IsComponent)
&& int(object->bindingTable()->value.objectIndex) == objectIndex) {
return true;
}
}
return false;
}
// reset() + repopulateBindings() cannot faithfully reproduce some components in place,
// because either:
// - Its index doesn't denote a component root anymore in the new unit, because objects were
// added or removed in front of it.
// - Its non-composite (C++) base type changed: the reused QObject is still an instance of the old
// class (see hasChangedNonCompositeBaseType).
// - It carries deferred bindings. We cannot re-install those because their handling is in the
// type's discretion.
// Fore those return false here. Otherwise return true.
static bool
canRebuildComponentRootInPlace(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit,
int objectIndex)
{
// An index out of range in the new CU is obsolete: rebuildObject() skips it and applyDiff()
// retires it afterwards. There is nothing to recreate from an enclosing root.
if (objectIndex >= newUnit->objectCount())
return true;
if (!isComponentRoot(newUnit, objectIndex))
return false;
if (newUnit->objectHasDeferredBindings(objectIndex))
return false;
return !hasChangedNonCompositeBaseType(oldUnit, newUnit, objectIndex);
}
// Re-resolve the object's precomputed binding-target table against its relinked property cache.
static void refreshBindingPropertyData(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &cu,
int objectIndex, const QQmlPropertyCache::ConstPtr &cache)
{
QList<QV4::CompiledData::BindingPropertyData> &table =
cu->baseCompilationUnit()->bindingPropertyDataPerObject;
if (objectIndex >= table.size())
return;
QV4::CompiledData::BindingPropertyData &bindingData = table[objectIndex];
const QV4::CompiledData::Object *obj = cu->objectAt(objectIndex);
const QQmlPropertyResolver resolver(cache);
const QV4::CompiledData::Binding *binding = obj->bindingTable();
for (qsizetype i = 0, end = bindingData.size(); i < end; ++i, ++binding) {
if (!bindingData.at(i))
continue;
Q_ASSERT(i < obj->nBindings);
const QString name = BindingPatchContext::targetPropertyName(cu, objectIndex, binding);
if (name.isEmpty())
continue;
if (cu->bindingIsSignalHandlerExpression(objectIndex, i)
|| cu->bindingIsSignalHandlerObject(objectIndex, i)) {
bindingData[i] = resolver.signal(
*QQmlSignalNames::anyHandlerNameToSignalName(name), nullptr,
QQmlPropertyResolver::IgnoreRevision);
} else if (cu->bindingIsPropertyObserver(objectIndex, i)) {
bindingData[i] = resolver.property(
*QQmlSignalNames::changedHandlerNameToPropertyName(name), nullptr,
QQmlPropertyResolver::IgnoreRevision);
} else {
bindingData[i] = resolver.property(
name, nullptr, QQmlPropertyResolver::IgnoreRevision);
}
}
}
// Ensure the property cache at objectIndex in cu is derived from actualParent, the cache we just
// used for the level below it in the VME chain.
static QQmlPropertyCache::ConstPtr
relinkCache(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &cu, int objectIndex,
const QQmlPropertyCache::ConstPtr &actualParent)
{
QQmlPropertyCacheVector *caches = cu->propertyCachesPtr();
QQmlPropertyCache::ConstPtr cache = caches->at(objectIndex);
// The bottom-most composite level's own base is a non-composite (C++) type. That base never
// changes across a reload since a changed non-composite base is rejected by
// hasChangedNonCompositeBaseType.
if (!actualParent)
return cache;
// A type that needs no VME meta-object of its own reuses its base type's property cache.
if (!caches->needsVMEMetaObject(objectIndex)) {
if (cache != actualParent) {
caches->set(objectIndex, actualParent);
refreshBindingPropertyData(cu, objectIndex, actualParent);
}
return actualParent;
}
// A type with its own cache derived from the base: re-derive it from the relinked base so its
// inherited offsets match the (possibly changed) base layout.
if (cache->parent() != actualParent) {
cache = cache->rebased(actualParent);
caches->set(objectIndex, cache);
refreshBindingPropertyData(cu, objectIndex, cache);
}
return cache;
}
// Re-link every property cache in cu that (transitively) derives oldBaseCache so it derives
// newBaseCache instead.
static void relinkDerivedCaches(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &cu,
const QQmlPropertyCache::ConstPtr &oldBaseCache,
const QQmlPropertyCache::ConstPtr &newBaseCache)
{
QQmlPropertyCacheVector *caches = cu->propertyCachesPtr();
Q_ASSERT(caches);
QHash<const QQmlPropertyCache *, QQmlPropertyCache::ConstPtr> replacements;
replacements.insert(oldBaseCache.data(), newBaseCache);
for (bool changed = true; changed;) {
changed = false;
for (int i = 0, end = caches->count(); i < end; ++i) {
const QQmlPropertyCache::ConstPtr cache = caches->at(i);
if (!cache)
continue;
const auto it = replacements.constFind(cache->parent().data());
if (it == replacements.constEnd())
continue;
const QQmlPropertyCache::ConstPtr rebased = cache->rebased(*it);
replacements.insert(cache.data(), rebased);
caches->set(i, rebased);
refreshBindingPropertyData(cu, i, rebased);
changed = true;
}
}
}
struct RebuildTarget
{
QObject *object = nullptr;
int index = -1;
QQmlRefPointer<QV4::ExecutableCompilationUnit> oldCu;
QQmlRefPointer<QV4::ExecutableCompilationUnit> newCu;
};
// Walk the type resolution chain starting from the object at cuIndex in unit,
// collecting all composite (QML-defined) base type levels. Returns them ordered
// deepest-first (e.g., grandparent before parent).
static std::vector<CompositeLevel>
collectCompositeLevels(const CompositeLevel &instanceLevel,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
std::vector<CompositeLevel> levels;
auto currentUnit = instanceLevel.newCu;
const QV4::CompiledData::Object *obj = currentUnit->objectAt(instanceLevel.objectIndex);
const QV4::ResolvedTypeReference *typeRef =
currentUnit->resolvedType(obj->inheritedTypeNameIndex);
while (typeRef && typeRef->type().isComposite()) {
QQmlRefPointer<QV4::ExecutableCompilationUnit> cu = typeRef->isSelfReference()
? currentUnit
: currentUnit->engine->executableCompilationUnit(typeRef->compilationUnit());
Q_ASSERT(cu);
// Replace the old unit with the new one wherever it occurs
const auto oldCu = cu;
if (cu == oldUnit)
cu = newUnit;
int rootIndex;
QString icName;
if (typeRef->type().isInlineComponent()) {
icName = typeRef->type().elementName();
rootIndex = cu->inlineComponentId(icName);
} else {
rootIndex = 0;
}
levels.push_back({ oldCu, cu, rootIndex, icName, nullptr });
if (rootIndex < 0 || rootIndex >= cu->objectCount())
break;
// Walk deeper into the base type
const QV4::CompiledData::Object *rootObj = cu->objectAt(rootIndex);
typeRef = cu->resolvedType(rootObj->inheritedTypeNameIndex);
currentUnit = cu;
}
return levels;
}
// Rebuilding a component root replaces the contexts it is the context object of. Instances of
// inner components created in such a context still have it as parent context. When rebuilding
// those, we have to parent their new contexts to the replacements, or they keep resolving ids
// against the old context, whose objects have been retired.
struct ReplacedContext
{
QQmlRefPointer<QQmlContextData> oldContext;
QQmlRefPointer<QQmlContextData> newContext;
};
using ReplacedContexts = QHash<const QQmlContextData *, ReplacedContext>;
static QQmlRefPointer<QQmlContextData>
currentContext(const QQmlRefPointer<QQmlContextData> &context,
const ReplacedContexts &replacedContexts)
{
const auto it = replacedContexts.constFind(context.data());
return it == replacedContexts.constEnd() ? context : it->newContext;
}
// The contexts of the composite levels of object, in the order of collectCompositeLevels().
static QVarLengthArray<QQmlRefPointer<QQmlContextData>, 4>
levelContexts(QObject *object, const QQmlRefPointer<QQmlContextData> &outerContext)
{
QVarLengthArray<QQmlRefPointer<QQmlContextData>, 4> contexts;
for (QQmlRefPointer<QQmlContextData> ctx(QQmlData::get(object)->context); ctx;
ctx = ctx->linkedContext()) {
if (ctx != outerContext && ctx->contextObject() == object)
contexts.append(ctx);
}
// The linked contexts are ordered from the deepest base type to the most derived one.
std::reverse(contexts.begin(), contexts.end());
return contexts;
}
// cuIndex is not necessarily the "outermost" index. There may be levels above oldUnit.
// Those have to be preserved/rebuilt.
static void rebuildObject(QObject *object, int cuIndex,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit,
ReplacedContexts *replacedContexts)
{
// If the object's index doesn't exist in the new CU, it's obsolete.
if (cuIndex >= newUnit->objectCount())
return;
QQmlData *ddata = QQmlData::get(object);
Q_ASSERT(ddata);
QQmlRefPointer<QQmlContextData> outerContext =
QQmlRefPointer<QQmlContextData>(ddata->outerContext);
// If the object has no context, or is scheduled for deletion, it's half-dead already.
if (!ddata->context || !outerContext || !outerContext->isValid() || ddata->isQueuedForDeletion)
return;
CompositeLevel instanceLevel{ ddata->compilationUnit,
ddata->compilationUnit == oldUnit ? newUnit
: ddata->compilationUnit,
ddata->cuObjectIndex, QString(), outerContext };
// If the object doesn't exist anymore in the new CU it will be deleted via GC.
// Nothing to do here.
if (instanceLevel.objectIndex >= instanceLevel.newCu->objectCount())
return;
std::vector<CompositeLevel> levels = collectCompositeLevels(instanceLevel, oldUnit, newUnit);
// Build the set of compilation units that participate in this rebuild.
// Bindings from these CUs are "internal" and will be re-created by repopulateBindings.
std::vector<CompositeLevel> internalUnits;
internalUnits.push_back(instanceLevel);
for (const auto &level : levels)
internalUnits.push_back(level);
BindingPatchContext patchCtx(object, ddata->compilationUnit, ddata->cuObjectIndex);
QDuplicateTracker<QObject *> seenChildren;
patchCtx.stashExternalState(internalUnits, &seenChildren);
// Collect the CUs whose children will be recreated by repopulateBindings.
// This is oldUnit (being replaced) and the composite-level CUs.
// NOT newUnit (already-rebuilt objects pointing here must be preserved)
// and NOT instanceLevel.cu (its repopulateBindings only sets bindings, not children).
std::vector<QQmlRefPointer<QV4::ExecutableCompilationUnit>> unitsToUnparent;
unitsToUnparent.push_back(oldUnit);
for (const auto &level : levels)
unitsToUnparent.push_back(level.oldCu);
patchCtx.reset(unitsToUnparent, internalUnits);
QV4::ExecutionEngine *v4 = newUnit->engine;
Q_ASSERT(v4);
QQmlEnginePrivate *enginePrivate = QQmlEnginePrivate::get(v4);
Q_ASSERT(enginePrivate);
const QVarLengthArray<QQmlRefPointer<QQmlContextData>, 4> oldLevelContexts =
levelContexts(object, outerContext);
if (outerContext->contextObject() == object) {
const QQmlRefPointer<QQmlContextData> oldOuterContext = outerContext;
oldOuterContext->setContextObject(nullptr);
outerContext = enginePrivate->createComponentRootContext(
instanceLevel.newCu, currentContext(oldOuterContext->parent(), *replacedContexts),
instanceLevel.objectIndex);
outerContext->setContextObject(object);
instanceLevel.context = outerContext;
replacedContexts->insert(oldOuterContext.data(), { oldOuterContext, outerContext });
}
for (QQmlRefPointer<QQmlContextData> ctx(ddata->context); ctx; ctx = ctx->linkedContext()) {
if (ctx->contextObject() == object)
ctx->setContextObject(nullptr);
}
ddata->clear();
QObjectPrivate *objectPrivate = QObjectPrivate::get(object);
delete std::exchange(objectPrivate->metaObject, nullptr);
QQmlRefPointer<QQmlContextData> levelContext = outerContext;
qsizetype replacedLevels = 0;
for (qsizetype i = 0, end = levels.size(); i < end; ++i) {
CompositeLevel &level = levels[i];
levelContext = level.context = enginePrivate->createComponentRootContext(
level.newCu, levelContext, level.objectIndex);
levelContext->setContextObject(object);
// The base type chain may have changed. Only match the old contexts up to the first
// level that differs.
if (i >= oldLevelContexts.size() || i != replacedLevels)
continue;
const QQmlRefPointer<QQmlContextData> &oldLevelContext = oldLevelContexts[i];
const QQmlRefPointer<QV4::ExecutableCompilationUnit> oldLevelCu =
oldLevelContext->typeCompilationUnit();
if ((oldLevelCu == oldUnit ? newUnit : oldLevelCu) != level.newCu)
continue;
replacedContexts->insert(oldLevelContext.data(), { oldLevelContext, levelContext });
++replacedLevels;
}
if (outerContext->contextObject() == object) {
ddata->ownContext = outerContext;
ddata->context = outerContext.data();
if (!levels.empty())
ddata->context->setLinkedContext(levels.front().context);
} else if (!levels.empty()) {
ddata->ownContext = levels.back().context;
ddata->context = ddata->ownContext.data();
if (levels.size() > 1)
ddata->context->setLinkedContext(levels.front().context);
} else {
// Non-root child: doesn't own a context. Re-link to the parent context.
ddata->context = outerContext.data();
}
// Build the VME meta-object chain base-first, relinking each level's property cache to the
// actual (possibly freshly reloaded) parent cache we just used, so the whole chain's offsets
// stay consistent even when a composite base type's layout changed on reload.
QQmlPropertyCache::ConstPtr parentCache;
for (auto it = levels.crbegin(), end = levels.crend(); it != end; ++it) {
it->context->addOwnedObject(ddata);
QQmlPropertyCache::ConstPtr cache = relinkCache(it->newCu, it->objectIndex, parentCache);
if (it->newCu->propertyCachesPtr()->needsVMEMetaObject(it->objectIndex))
new QQmlVMEMetaObject(v4, object, cache, it->newCu, it->objectIndex);
parentCache = cache;
}
outerContext->addOwnedObject(ddata);
if (QQmlPropertyCacheVector *caches = instanceLevel.newCu->propertyCachesPtr();
caches->count() > instanceLevel.objectIndex) {
QQmlPropertyCache::ConstPtr cache =
relinkCache(instanceLevel.newCu, instanceLevel.objectIndex, parentCache);
if (caches->needsVMEMetaObject(instanceLevel.objectIndex))
new QQmlVMEMetaObject(v4, object, cache, instanceLevel.newCu, instanceLevel.objectIndex);
}
// Repopulate bindings at each composite level (deepest first).
// This sets up functions, evaluates bindings (creating child objects), etc.
// The object may get queued for deletion as result of some bindings.
// In that case we have to abort.
for (auto it = levels.crbegin(), end = levels.crend(); it != end && !ddata->isQueuedForDeletion;
++it) {
QQmlObjectCreator creator(it->context, it->newCu, outerContext, it->icName, nullptr);
creator.repopulateBindings(it->objectIndex, object, it->context,
QQmlObjectCreator::InitFlag::IsContextObject
| QQmlObjectCreator::InitFlag::IsDocumentRoot);
QQmlInstantiationInterrupt interrupt;
creator.finalize(interrupt);
}
// The object may get queued for deletion as result of some bindings.
// In that case don't touch it any further.
if (!ddata->isQueuedForDeletion) {
// Repopulate bindings at the instance level in the parent CU.
QQmlObjectCreator creator(instanceLevel.context, instanceLevel.newCu, outerContext, QString(),
nullptr);
creator.repopulateBindings(instanceLevel.objectIndex, object, outerContext,
QQmlObjectCreator::InitFlag::None);
QQmlInstantiationInterrupt interrupt;
creator.finalize(interrupt);
// Restore externally set bindings and values that were stashed before reset.
// First refresh child object pointers — they may have been replaced during rebuild.
patchCtx.refreshObjects();
patchCtx.restoreExternalState();
}
}
enum class BindingKind { Literal, Script, Translation, Unpatchable };
static BindingKind bindingKind(const QV4::CompiledData::Binding *binding)
{
switch (binding->type()) {
case QV4::CompiledData::Binding::Type_Script:
return BindingKind::Script;
case QV4::CompiledData::Binding::Type_Translation:
case QV4::CompiledData::Binding::Type_TranslationById:
return BindingKind::Translation;
case QV4::CompiledData::Binding::Type_Number:
case QV4::CompiledData::Binding::Type_Boolean:
case QV4::CompiledData::Binding::Type_String:
case QV4::CompiledData::Binding::Type_Null:
return BindingKind::Literal;
default:
return BindingKind::Unpatchable;
}
}
// A trivial diff is one we can apply without rebuilding any VME meta-object and without
// adding or removing objects: only existing bindings and binding-expression bodies change. Such a
// diff is patched in place (see patchInPlace()). Everything else falls back to rebuilding roots.
static bool changeIsTrivial(const QV4::CompiledData::Change &change,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
using ChangeType = QV4::CompiledData::ChangeType;
switch (change.type) {
// Source-location-only changes have no runtime effect.
case ChangeType::AliasLocationChanged:
case ChangeType::BindingLocationChanged:
case ChangeType::EnumLocationChanged:
case ChangeType::FunctionLocationChanged:
case ChangeType::ImportLocationChanged:
case ChangeType::InlineComponentLocationChanged:
case ChangeType::ObjectLocationChanged:
case ChangeType::PropertyLocationChanged:
case ChangeType::SignalLocationChanged:
// Internal table changes that are byproducts of recompiling expressions. They are picked up
// when we translate functions and remap compilation units; no structure changes.
case ChangeType::UnitMetadataChanged:
case ChangeType::ConstantAdded:
case ChangeType::ConstantChanged:
case ChangeType::ConstantRemoved:
case ChangeType::StringDataAdded:
case ChangeType::StringDataChanged:
case ChangeType::StringDataRemoved:
case ChangeType::LookupAdded:
case ChangeType::LookupChanged:
case ChangeType::LookupRemoved:
case ChangeType::RegExpAdded:
case ChangeType::RegExpChanged:
case ChangeType::RegExpRemoved:
case ChangeType::ClassAdded:
case ChangeType::ClassChanged:
case ChangeType::ClassRemoved:
case ChangeType::BlockAdded:
case ChangeType::BlockChanged:
case ChangeType::BlockRemoved:
case ChangeType::TemplateObjectAdded:
case ChangeType::TemplateObjectChanged:
case ChangeType::TemplateObjectRemoved:
case ChangeType::JSClassAdded:
case ChangeType::JSClassChanged:
case ChangeType::JSClassRemoved:
case ChangeType::TranslationDataAdded:
case ChangeType::TranslationDataChanged:
case ChangeType::TranslationDataRemoved:
return true;
case ChangeType::FunctionChanged:
// A recompiled function body at a stable index. Binding/handler expressions pick up the new
// body via refreshBindings(), VME methods via refreshVmeMethods(). Neither needs a rebuild,
// so a function change is trivial regardless of whether the function backs a binding or a
// method.
return true;
case ChangeType::BindingChanged: {
Q_ASSERT(change.objectIndex >= 0);
Q_ASSERT(change.objectIndex < oldUnit->objectCount());
Q_ASSERT(change.objectIndex < newUnit->objectCount());
const QV4::CompiledData::Object *oldObj = oldUnit->objectAt(change.objectIndex);
Q_ASSERT(change.index < oldObj->nBindings);
const QV4::CompiledData::Binding *oldBinding = oldObj->bindingTable() + change.index;
const QV4::CompiledData::Object *newObj = newUnit->objectAt(change.objectIndex);
Q_ASSERT(change.index < newObj->nBindings);
const QV4::CompiledData::Binding *newBinding = newObj->bindingTable() + change.index;
const BindingKind oldKind = bindingKind(oldBinding);
const BindingKind newKind = bindingKind(newBinding);
if (oldKind == BindingKind::Unpatchable || newKind == BindingKind::Unpatchable)
return false;
// We cannot swap a binding for one of a different kind, yet: turning a literal into a
// script or translation binding (or vice versa) would have to install or drop a live
// binding, and we cannot recompile a script binding into a translation binding either.
// TODO: Fix this.
if (oldKind != newKind)
return false;
// A BindingChanged at a stable index can also mean the binding was moved to a different
// target property (its propertyNameIndex changed). In-place patching cannot relocate a
// binding: it would have to remove it from the old property and install it on the new one.
// That is structural, so fall back to the rebuild path.
return BindingPatchContext::targetPropertyName(oldUnit, change.objectIndex, oldBinding)
== BindingPatchContext::targetPropertyName(newUnit, change.objectIndex, newBinding);
}
default:
// ObjectAdded/Removed/Changed, Binding Added/Removed, Property/Signal/Alias/Enum changes,
// Function Added/Removed, Import/InlineComponent/RequiredPropertyExtraData changes:
// structural, need a rebuild.
return false;
}
}
static bool isTrivialDiff(const QV4::CompiledData::CompilationUnitDiff &diff,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
return std::all_of(diff.changes.cbegin(), diff.changes.cend(),
[&](const QV4::CompiledData::Change &change) {
return changeIsTrivial(change, oldUnit, newUnit);
});
}
// Re-apply changed literal bindings. A literal value is written only if the property still holds
// the value the old unit assigned; anything else is an external override we must leave untouched.
// Changed script bindings carry no literal value and are handled by function translation instead.
//
// The binding may live on a value-type group sub-object (font.pixelSize) or an attached object
// (Keys.enabled), which has no standalone QObject at its compilation-unit index. We use
// BindingPatchContext to map every compilation-unit object index reachable from a live instance to
// the owning QObject and the property-name prefix to address it.
static void patchConstantBindings(const std::vector<QObject *> &objects,
const QV4::CompiledData::CompilationUnitDiff &diff,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
for (QObject *object : objects) {
// Skip half-dead objects, mirroring rebuildObject(): an invalidated context means the
// object is on its way out and must not be patched.
QQmlData *ddata = QQmlData::get(object);
if (!ddata->context || !ddata->outerContext || !ddata->outerContext->isValid()
|| ddata->isQueuedForDeletion) {
continue;
}
QVarLengthArray<BindingPatchContext, 4> contexts;
for (int index : objectIndices(object, oldUnit))
contexts.append(BindingPatchContext(object, oldUnit, index));
for (const QV4::CompiledData::Change &change : diff.changes) {
if (change.type != QV4::CompiledData::ChangeType::BindingChanged)
continue;
for (BindingPatchContext &context : contexts) {
if (context.applyBindingChange(newUnit, change))
break;
}
}
}
}
// Point every live object that still references oldUnit at newUnit, in both its ddata and its
// VME meta-object chain. Used after in-place patching, which keeps all object indices stable.
//
// The ddata compilation unit and the VME chain are remapped independently: a composite-type
// instance can carry oldUnit in its VME chain while its ddata->compilationUnit is a different
// executable unit (the "topmost", possibly inaddressible, instantiation).
static void remapObjectsToNewUnit(const std::vector<QObject *> &objects,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
for (QObject *object : objects) {
QQmlData *ddata = QQmlData::get(object);
// A QQmlComponent (e.g. an explicit or implicit `delegate:`) caches the compilation unit it
// instantiates from. Remap it so future create() calls produce instances from newUnit.
if (QQmlComponent *component = qobject_cast<QQmlComponent *>(object)) {
QQmlComponentPrivate *cp = QQmlComponentPrivate::get(component);
if (cp->compilationUnit() == oldUnit)
cp->setCompilationUnit(newUnit);
}
// Remap the ddata compilation unit if it points at oldUnit. An object whose index is out
// of range in the new CU is obsolete (it belonged to the old type definition); leave its
// ddata alone and let the VME nulling below retire it.
if (ddata->compilationUnit == oldUnit && ddata->cuObjectIndex < newUnit->objectCount())
ddata->compilationUnit = newUnit;
if (!ddata->hasVMEMetaObject)
continue;
// Remap (or retire) every VME meta-object in the chain that points at oldUnit.
auto *mainVme = static_cast<QQmlVMEMetaObject *>(QObjectPrivate::get(object)->metaObject);
for (QQmlVMEMetaObject *vmeMeta = mainVme; vmeMeta;
vmeMeta = vmeMeta->parentVMEMetaObject()) {
if (vmeMeta->compilationUnit() != oldUnit)
continue;
if (vmeMeta->qmlObjectId() < newUnit->objectCount()) {
vmeMeta->setCompilationUnit(newUnit);
// Give the live instance the reloaded type's identity. The instance must present
// newUnit's property cache.
const QQmlPropertyCache::ConstPtr newCache =
newUnit->propertyCachesPtr()->at(vmeMeta->qmlObjectId());
if (newCache) {
vmeMeta->setPropertyCache(newCache);
if (vmeMeta == mainVme)
ddata->propertyCache = newCache;
}
} else {
// Obsolete: null it so stale alias lookups (triggered by refreshBindings) safely
// return nullptr from findCompiledObject() instead of asserting.
vmeMeta->setCompilationUnit(nullptr);
}
}
}
}
// Returns true if the object is instantiated as a component root of the old unit that we can
// rebuild in place (see canRebuildComponentRootInPlace()), or as a component root of a different,
// unchanged unit. The latter is rebuilt against its own unit. Otherwise, for example if the object
// is not a component root at all or not even created from QML, return false.
static bool isRebuildableComponentRoot(
QObject *object,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
bool foundComponentRoot = false;
for (int index : objectIndices(object, oldUnit)) {
if (index >= oldUnit->objectCount() || !isComponentRoot(oldUnit, index))
continue;
if (!canRebuildComponentRootInPlace(oldUnit, newUnit, index))
return false;
foundComponentRoot = true;
}
if (foundComponentRoot)
return true;
const QQmlData *ddata = QQmlData::get(object);
return ddata && ddata->compilationUnit && ddata->compilationUnit != oldUnit
&& isComponentRoot(ddata->compilationUnit, ddata->cuObjectIndex);
}
// A component root whose non-composite base type changed or that carries deferred bindings cannot
// be rebuilt in place. Instead we travel the scope hierarchy upwards to the enclosing component
// root that instantiates it and rebuild that: its reset() + repopulateBindings() recreates the
// problematic object from scratch, as a fresh QObject. If that enclosing root is problematic, too,
// we keep travelling up. Returns the object to rebuild, or nullptr if we reach the root scope
// without finding a rebuildable enclosing root.
static QObject *outerRebuildTarget(
QObject *object,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
Q_ASSERT(object);
const QQmlData *ddata = QQmlData::get(object);
if (!ddata)
return nullptr;
// Walk up the context hierarchy. The root of a component instance is the context object of
// its own context. Contexts may also have no context object or one that is not a component
// root at all (e.g. a delegate model's item). If we reach the root context, we give up.
for (QQmlRefPointer<QQmlContextData> context(ddata->outerContext); context;
context = context->parent()) {
QObject *outer = context->contextObject();
if (outer && outer != object && isRebuildableComponentRoot(outer, oldUnit, newUnit))
return outer;
}
return nullptr;
}
// TODO: This is dangerous. We are manipulating compilation units exposed to multiple
// engines on potentially multiple threads.
void redirectResolvedTypeReferences(
QV4::ExecutionEngine *engine,
const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &newUnit)
{
const auto units = engine->compilationUnits();
for (const auto &cu : units) {
for (auto *typeRef : std::as_const(cu->baseCompilationUnit()->resolvedTypes)) {
if (typeRef->isSelfReference())
continue;
if (typeRef->compilationUnit() != oldUnit)
continue;
typeRef->setCompilationUnit(newUnit);
QQmlPropertyCache::ConstPtr newCache;
const QQmlType type = typeRef->type();
if (type.isInlineComponent()) {
if (const int icId = newUnit->inlineComponentId(type.elementName()); icId >= 0)
newCache = newUnit->propertyCaches.at(icId);
} else {
newCache = newUnit->rootPropertyCache();
}
if (!newCache)
continue;
const QQmlPropertyCache::ConstPtr oldCache = typeRef->typePropertyCache();
typeRef->setTypePropertyCache(newCache);
relinkDerivedCaches(cu, oldCache, newCache);
}
}
}
// Re-create the VME method function objects from newUnit so that an object's methods follow the
// unit remap, mirroring what refreshBindings() does for QQmlJavaScriptExpression-based bindings.
static void refreshVmeMethods(const std::vector<QObject *> &objects,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
QV4::ExecutionEngine *v4 = newUnit->engine;
QV4::Scope scope(v4);
QV4::ScopedValue function(scope);
for (QObject *object : objects) {
QQmlData *ddata = QQmlData::get(object);
if (!ddata || !ddata->hasVMEMetaObject)
continue;
auto *mainVme = static_cast<QQmlVMEMetaObject *>(QObjectPrivate::get(object)->metaObject);
for (QQmlVMEMetaObject *vme = mainVme; vme; vme = vme->parentVMEMetaObject()) {
const auto cu = vme->compilationUnit();
if (cu != newUnit)
continue;
const QQmlRefPointer<QQmlContextData> context = vme->contextData();
if (!context)
continue;
const int objectIndex = vme->qmlObjectId();
if (objectIndex < 0 || objectIndex >= cu->objectCount())
continue;
const QV4::CompiledData::Object *obj = cu->objectAt(objectIndex);
const QQmlPropertyCache::ConstPtr cache = cu->propertyCachesPtr()->at(objectIndex);
if (!cache)
continue;
QV4::Scoped<QV4::QmlContext> qmlContext(
scope, QV4::QmlContext::create(v4->rootContext(), context, object));
const quint32_le *functionIdx = obj->functionOffsetTable();
for (quint32 i = 0; i < obj->nFunctions; ++i, ++functionIdx) {
QV4::Function *runtimeFunction = cu->runtimeFunctions[*functionIdx];
const QString name = runtimeFunction->name()->toQString();
const QQmlPropertyData *property = cache->property(name, object, context);
if (!property || !property->isVMEFunction())
continue;
// Mirror QQmlObjectCreator::setupFunctions(): generators need their own creator.
function = runtimeFunction->isGenerator()
? QV4::GeneratorFunction::create(qmlContext, runtimeFunction)
: QV4::FunctionObject::createScriptFunction(qmlContext, runtimeFunction);
mainVme->setVmeMethod(property->coreIndex(), function);
}
}
}
}
PatchResult applyDiff(std::vector<QObject *> &objects,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
// Ensure the new CU's runtime data (strings, lookups, functions) is populated.
if (!newUnit->runtimeStrings)
newUnit->populate();
// Fast path: if only existing bindings changed -- nothing structural about any VME
// meta-object, and no objects added or removed -- patch the affected objects in place. We
// leave every object and its VME meta-object where it is, re-apply the changed literal
// bindings (patchConstantBindings), point the objects at newUnit (remapObjectsToNewUnit), and
// let the in-place refreshBindings() translate the still-live expressions' functions to
// newUnit. Because no functions are added or removed in a trivial diff, function indices are
// stable, which is what makes that same-index translation correct.
const QV4::CompiledData::CompilationUnitDiff diff =
QV4::CompiledData::diffCompilationUnits(oldUnit->unitData(), newUnit->unitData());
if (diff.success && isTrivialDiff(diff, oldUnit, newUnit)) {
patchConstantBindings(objects, diff, oldUnit, newUnit);
remapObjectsToNewUnit(objects, oldUnit, newUnit);
refreshVmeMethods(objects, newUnit);
return PatchResult::PatchedInPlace;
}
// Otherwise we rebuild whole component roots: the document root (index 0) and any
// inline-component roots. We don't touch other objects directly. Rebuilding a root runs a
// full reset() + repopulateBindings() that cascades down the entire instantiation (and
// through its composite base levels), recreating every object below it. Since every
// non-root object is, by construction, a descendant of a component root, this is
// sufficient. A component root that cannot be rebuilt in place - its C++ base type changed, or
// it carries deferred bindings (see below) - is handled by rebuilding the enclosing component
// root instead, which may live in a different compilation unit and recreates the root as a
// fresh object (of the new C++ class, with its deferred bindings armed and executed).
//
// A consequence worth remembering: because every live object is recreated, every
// QQmlJavaScriptExpression on those objects is recreated too, freshly bound to newUnit. By
// the time refreshBindings() runs, the only expressions still referencing oldUnit are the
// dead, detached leftovers of the resets. That is why the rebuild path's refreshBindings()
// can simply disable (null) them instead of remapping them to newUnit.
// A rebuild target carries the compilation units to rebuild it with. Usually that's oldUnit ->
// newUnit. But when we travel up to an enclosing component root in a *different* compilation
// unit (because a root's base type changed), that unit is itself unchanged: we rebuild it
// against itself (a full re-instantiation of its children) after redirecting the resolved type
// references, so the problematic object is recreated with its new C++ base type.
std::vector<RebuildTarget> rebuild;
QSet<QObject *> rebuildSet;
bool redirectedTypes = false;
const auto addRebuild = [&](QObject *object, int index,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldCu,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newCu) {
if (rebuildSet.contains(object))
return;
rebuildSet.insert(object);
rebuild.push_back({ object, index, oldCu, newCu });
};
for (QObject *object : objects) {
const QVarLengthArray<int, 4> indices = objectIndices(object, oldUnit);
for (int index : indices) {
// Objects instantiated by an enclosing component (indices beyond the CU) are recreated
// when that enclosing root is rebuilt, so we don't record them here. Neither do we
// record any other objects but component roots. Instances of inner components are not
// recreated by rebuilding the enclosing component. Therefore, we record their roots.
if (index >= oldUnit->objectCount() || !isComponentRoot(oldUnit, index))
continue;
// A component root whose own non-composite base type changed, which carries deferred
// bidnings, or which isn't a component root anymore cannot be rebuilt in place. Rebuild
// the enclosing component root instead, so that the object is recreated from scratch
// as an instance of the new C++ class with new deferred bindings. A surviving instance
// of an inner component stays on oldUnit then. Travelling up the scope hierarchy this
// way only fails once we hit the root scope.
if (!canRebuildComponentRootInPlace(oldUnit, newUnit, index)) {
QObject *outer = outerRebuildTarget(object, oldUnit, newUnit);
if (!outer)
return PatchResult::Failed;
// The enclosing root may recreate the object from a different compilation unit
// (the object was instantiated as an external type). Point that unit at newUnit
// first, so the recreated object gets the new definition.
if (!redirectedTypes) {
redirectResolvedTypeReferences(newUnit->engine, oldUnit->baseCompilationUnit(),
newUnit->baseCompilationUnit());
redirectedTypes = true;
}
// Rebuild the enclosing root against its own compilation unit (or newUnit, if that
// root lives in oldUnit itself, e.g. an inline component). The unchanged outer unit
// re-instantiates its children, recreating the problematic object.
const QQmlRefPointer<QV4::ExecutableCompilationUnit> outerOld =
QQmlData::get(outer)->compilationUnit;
const QQmlRefPointer<QV4::ExecutableCompilationUnit> outerNew =
outerOld->baseCompilationUnit() == oldUnit->baseCompilationUnit()
? newUnit
: outerOld;
addRebuild(outer, QQmlData::get(outer)->cuObjectIndex, outerOld, outerNew);
break;
}
addRebuild(object, indices.first(), oldUnit, newUnit);
break;
}
}
// Sort by ascending cuIndex so that parent objects are rebuilt before their
// children. A parent's reset() properly retires children (they still point to
// oldUnit) and repopulateBindings recreates them. This avoids leaking orphaned
// children and naturally handles cases where the context needs more ID slots.
std::sort(rebuild.begin(), rebuild.end(),
[](const RebuildTarget &a, const RebuildTarget &b) { return a.index < b.index; });
// Pre-compute which objects to skip: if an ancestor is also in the rebuild
// list, the child will be properly retired and recreated during the ancestor's
// rebuild. Rebuilding it individually would be wasted work on a stale pointer.
QSet<QObject *> skip;
for (const RebuildTarget &target : rebuild) {
const QObjectList &children = target.object->children();
for (QObject *child : children)
skip.insert(child);
}
ReplacedContexts replacedContexts;
for (const RebuildTarget &target : rebuild) {
if (skip.contains(target.object))
continue;
rebuildObject(target.object, target.index, target.oldCu, target.newCu, &replacedContexts);
}
// Everything lexically inside the rebuilt components has been recreated or rebuilt now. Whatever
// was instantiated by oldUnit and survived without being rebuilt is obsolete. We cannot remap it
// to newUnit by index since indices may have shifted. Retire it.
for (QObject *object : objects) {
const QQmlData *ddata = QQmlData::get(object);
if (ddata->compilationUnit != oldUnit || ddata->isQueuedForDeletion)
continue;
BindingPatchContext::clearBindingsRecursive(object);
BindingPatchContext::retireObject(object);
}
return PatchResult::Rebuilt;
}
// Translate a still-live expression's function from oldUnit to the function at the same index in
// newUnit, or clear it if there is no newUnit. Expressions whose function does not point into
// oldUnit are left untouched. Same-index translation is valid because we only do this with trivial
// diffs that neither add nor remove functions.
// Returns true if the resulting function is valid afterwards (and should be re-evaluated)
static bool
translateExpressionFunction(QQmlJavaScriptExpression *expr,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
QV4::Function *f = expr->function();
if (!f)
return false;
if (f->executableCompilationUnit() != oldUnit.data())
return true;
if (newUnit) {
const qsizetype index = oldUnit->runtimeFunctions.indexOf(f);
if (index >= 0 && index < newUnit->runtimeFunctions.size())
expr->setFunction(newUnit->runtimeFunctions[index]);
} else {
expr->setFunction(nullptr);
return false;
}
return true;
}
static void translateAndRefreshExpressionsRecursive(
const QQmlRefPointer<QQmlContextData> &context,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
for (auto child = context->childContexts(); child; child = child->nextChild())
translateAndRefreshExpressionsRecursive(child, oldUnit, newUnit);
for (auto *expr = context->expressions(); expr; expr = expr->nextExpression()) {
if (translateExpressionFunction(expr, oldUnit, newUnit))
expr->refresh();
}
}
void refreshBindings(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
{
translateAndRefreshExpressionsRecursive(
QQmlContextData::get(oldUnit->engine->qmlEngine()->rootContext()), oldUnit, newUnit);
}
} // namespace QQmlPreview
QT_END_NAMESPACE