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
qqmlpreviewobjectpatch.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 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// Qt-Security score:significant reason:default
4
6
7#include <private/qqmlcontextdata_p.h>
8#include <private/qqmldata_p.h>
9#include <private/qqmljavascriptexpression_p.h>
10#include <private/qqmlobjectcreator_p.h>
11#include <private/qqmlpreviewbindingpatchcontext_p.h>
12#include <private/qqmlscriptdata_p.h>
13#include <private/qqmlvme_p.h>
14#include <private/qqmlvmemetaobject_p.h>
15#include <private/qv4resolvedtypereference_p.h>
16
17#include <QtCore/qset.h>
18
20
21namespace QQmlPreview {
22
23// The indices at which object appears in oldUnit: its own (ddata) index plus any indices it
24// occupies through composite base levels (the VME meta-object chain).
25static QVarLengthArray<int, 4>
26objectIndices(QObject *object, const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit)
27{
28 QVarLengthArray<int, 4> objectIndices;
29
30 QQmlData *ddata = QQmlData::get(object);
31 if (ddata->compilationUnit == oldUnit)
32 objectIndices.push_back(ddata->cuObjectIndex);
33 if (ddata->hasVMEMetaObject) {
34 for (QQmlVMEMetaObject *vme =
35 static_cast<QQmlVMEMetaObject *>(QObjectPrivate::get(object)->metaObject);
36 vme; vme = vme->parentVMEMetaObject()) {
37 if (vme->compilationUnit() == oldUnit)
38 objectIndices.push_back(vme->qmlObjectId());
39 }
40 }
41
42 return objectIndices;
43}
44
46nonCompositeBaseType(const QQmlPropertyCache::ConstPtr &propertyCache)
47{
48 for (QQmlPropertyCache::ConstPtr parent = propertyCache; parent; parent = parent->parent()) {
49 if (!parent->isComposite())
50 return parent;
51 }
52
53 return QQmlPropertyCache::ConstPtr();
54}
55
56static bool
57hasChangedNonCompositeBaseType(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
58 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit,
59 int objectIndex)
60{
61 const QV4::CompiledData::Object *oldObj = oldUnit->objectAt(objectIndex);
62 const auto *oldTypeRef = oldUnit->resolvedType(oldObj->inheritedTypeNameIndex);
63 if (!oldTypeRef)
64 return false; // Group property sub-objects have no inherited type.
65
66 const QV4::CompiledData::Object *newObj = newUnit->objectAt(objectIndex);
67 const auto *newTypeRef = newUnit->resolvedType(newObj->inheritedTypeNameIndex);
68 if (!newTypeRef)
69 return true; // Type disappeared — definitely changed.
70
71 return nonCompositeBaseType(oldTypeRef->typePropertyCache())
72 != nonCompositeBaseType(newTypeRef->typePropertyCache());
73}
74
76{
77 QObject *object = nullptr;
78 int index = -1;
79};
80
81// Walk the type resolution chain starting from the object at cuIndex in unit,
82// collecting all composite (QML-defined) base type levels. Returns them ordered
83// deepest-first (e.g., grandparent before parent).
85collectCompositeLevels(const CompositeLevel &instanceLevel,
86 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
87 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
88{
89 std::vector<CompositeLevel> levels;
90
91 auto currentUnit = instanceLevel.newCu;
92 const QV4::CompiledData::Object *obj = currentUnit->objectAt(instanceLevel.objectIndex);
93 const QV4::ResolvedTypeReference *typeRef =
94 currentUnit->resolvedType(obj->inheritedTypeNameIndex);
95
96 while (typeRef && typeRef->type().isComposite()) {
97 QQmlRefPointer<QV4::ExecutableCompilationUnit> cu = typeRef->isSelfReference()
98 ? currentUnit
99 : currentUnit->engine->executableCompilationUnit(typeRef->compilationUnit());
100 Q_ASSERT(cu);
101
102 // Replace the old unit with the new one wherever it occurs
103 const auto oldCu = cu;
104 if (cu == oldUnit)
105 cu = newUnit;
106
107 int rootIndex;
108 QString icName;
109 if (typeRef->type().isInlineComponent()) {
110 icName = typeRef->type().elementName();
111 rootIndex = cu->inlineComponentId(icName);
112 } else {
113 rootIndex = 0;
114 }
115
116 levels.push_back({ oldCu, cu, rootIndex, icName, nullptr });
117
118 if (rootIndex >= cu->objectCount())
119 break;
120
121 // Walk deeper into the base type
122 const QV4::CompiledData::Object *rootObj = cu->objectAt(rootIndex);
123 typeRef = cu->resolvedType(rootObj->inheritedTypeNameIndex);
124 currentUnit = cu;
125 }
126
127 return levels;
128}
129
130// cuIndex is not necessarily the "outermost" index. There may be levels above oldUnit.
131// Those have to be preserved/rebuilt.
132static void rebuildObject(QObject *object, int cuIndex,
133 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
134 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
135{
136 // If the object's index doesn't exist in the new CU, it's obsolete.
137 if (cuIndex >= newUnit->objectCount())
138 return;
139
140 QQmlData *ddata = QQmlData::get(object);
141 Q_ASSERT(ddata);
142
143 QQmlRefPointer<QQmlContextData> outerContext =
144 QQmlRefPointer<QQmlContextData>(ddata->outerContext);
145
146 // If the object has no context, or is scheduled for deletion, it's half-dead already.
147 if (!ddata->context || !outerContext || !outerContext->isValid() || ddata->isQueuedForDeletion)
148 return;
149
150 CompositeLevel instanceLevel{ ddata->compilationUnit,
151 ddata->compilationUnit == oldUnit ? newUnit
152 : ddata->compilationUnit,
153 ddata->cuObjectIndex, QString(), outerContext };
154
155 // If the object doesn't exist anymore in the new CU it will be deleted via GC.
156 // Nothing to do here.
157 if (instanceLevel.objectIndex >= instanceLevel.newCu->objectCount())
158 return;
159
160 std::vector<CompositeLevel> levels = collectCompositeLevels(instanceLevel, oldUnit, newUnit);
161
162 // Build the set of compilation units that participate in this rebuild.
163 // Bindings from these CUs are "internal" and will be re-created by repopulateBindings.
164 std::vector<CompositeLevel> internalUnits;
165 internalUnits.push_back(instanceLevel);
166 for (const auto &level : levels)
167 internalUnits.push_back(level);
168
169 BindingPatchContext patchCtx(object, ddata->compilationUnit, ddata->cuObjectIndex);
170 QDuplicateTracker<QObject *> seenChildren;
171 patchCtx.stashExternalState(internalUnits, &seenChildren);
172 // Collect the CUs whose children will be recreated by repopulateBindings.
173 // This is oldUnit (being replaced) and the composite-level CUs.
174 // NOT newUnit (already-rebuilt objects pointing here must be preserved)
175 // and NOT instanceLevel.cu (its repopulateBindings only sets bindings, not children).
176 std::vector<QQmlRefPointer<QV4::ExecutableCompilationUnit>> unitsToUnparent;
177 unitsToUnparent.push_back(oldUnit);
178 for (const auto &level : levels)
179 unitsToUnparent.push_back(level.oldCu);
180 patchCtx.reset(unitsToUnparent, internalUnits);
181
182 QV4::ExecutionEngine *v4 = newUnit->engine;
183 Q_ASSERT(v4);
184 QQmlEnginePrivate *enginePrivate = QQmlEnginePrivate::get(v4);
185 Q_ASSERT(enginePrivate);
186
187 if (outerContext->contextObject() == object) {
188 outerContext->setContextObject(nullptr);
189 outerContext = enginePrivate->createComponentRootContext(
190 instanceLevel.newCu, outerContext->parent(), instanceLevel.objectIndex);
191 outerContext->setContextObject(object);
192 instanceLevel.context = outerContext;
193 }
194
195 for (QQmlRefPointer<QQmlContextData> ctx(ddata->context); ctx; ctx = ctx->linkedContext()) {
196 if (ctx->contextObject() == object)
197 ctx->setContextObject(nullptr);
198 }
199
200 ddata->clear();
201
202 QObjectPrivate *objectPrivate = QObjectPrivate::get(object);
203 delete std::exchange(objectPrivate->metaObject, nullptr);
204
205 QQmlRefPointer<QQmlContextData> levelContext = outerContext;
206 for (qsizetype i = 0, end = levels.size(); i < end; ++i) {
207 CompositeLevel &level = levels[i];
208 levelContext = level.context = enginePrivate->createComponentRootContext(
209 level.newCu, levelContext, level.objectIndex);
210 levelContext->setContextObject(object);
211 }
212
213 if (outerContext->contextObject() == object) {
214 ddata->ownContext = outerContext;
215 ddata->context = outerContext.data();
216 if (!levels.empty())
217 ddata->context->setLinkedContext(levels.front().context);
218 } else if (!levels.empty()) {
219 ddata->ownContext = levels.back().context;
220 ddata->context = ddata->ownContext.data();
221 if (levels.size() > 1)
222 ddata->context->setLinkedContext(levels.front().context);
223 } else {
224 // Non-root child: doesn't own a context. Re-link to the parent context.
225 ddata->context = outerContext.data();
226 }
227
228 for (auto it = levels.crbegin(), end = levels.crend(); it != end; ++it) {
229 it->context->addOwnedObject(ddata);
230 QQmlPropertyCache::ConstPtr cache = it->newCu->propertyCachesPtr()->at(it->objectIndex);
231 new QQmlVMEMetaObject(v4, object, cache, it->newCu, it->objectIndex);
232 }
233
234 outerContext->addOwnedObject(ddata);
235 if (QQmlPropertyCacheVector *caches = instanceLevel.newCu->propertyCachesPtr();
236 caches->count() > instanceLevel.objectIndex
237 && caches->needsVMEMetaObject(instanceLevel.objectIndex)) {
238 QQmlPropertyCache::ConstPtr cache = caches->at(instanceLevel.objectIndex);
239 new QQmlVMEMetaObject(v4, object, cache, instanceLevel.newCu, instanceLevel.objectIndex);
240 }
241
242 // Repopulate bindings at each composite level (deepest first).
243 // This sets up functions, evaluates bindings (creating child objects), etc.
244 // The object may get queued for deletion as result of some bindings.
245 // In that case we have to abort.
246 for (auto it = levels.crbegin(), end = levels.crend(); it != end && !ddata->isQueuedForDeletion;
247 ++it) {
248 QQmlObjectCreator creator(it->context, it->newCu, outerContext, it->icName, nullptr);
249 creator.repopulateBindings(it->objectIndex, object, it->context,
250 QQmlObjectCreator::InitFlag::IsContextObject
251 | QQmlObjectCreator::InitFlag::IsDocumentRoot);
252
253 QQmlInstantiationInterrupt interrupt;
254 creator.finalize(interrupt);
255 }
256
257 // The object may get queued for deletion as result of some bindings.
258 // In that case don't touch it any further.
259 if (!ddata->isQueuedForDeletion) {
260 // Repopulate bindings at the instance level in the parent CU.
261 QQmlObjectCreator creator(instanceLevel.context, instanceLevel.newCu, outerContext, QString(),
262 nullptr);
263 creator.repopulateBindings(instanceLevel.objectIndex, object, outerContext,
264 QQmlObjectCreator::InitFlag::None);
265 QQmlInstantiationInterrupt interrupt;
266 creator.finalize(interrupt);
267
268 // Restore externally set bindings and values that were stashed before reset.
269 // First refresh child object pointers — they may have been replaced during rebuild.
270 patchCtx.refreshObjects();
271 patchCtx.restoreExternalState();
272 }
273}
274
275bool applyDiff(std::vector<QObject *> &objects,
276 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit,
277 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &newUnit)
278{
279 // Ensure the new CU's runtime data (strings, lookups, functions) is populated.
280 if (!newUnit->runtimeStrings)
281 newUnit->populate();
282
283 // For now we always rebuild whole component roots: the document root (index 0) and any
284 // inline-component roots. We don't touch other objects directly. Rebuilding a root runs a
285 // full reset() + repopulateBindings() that cascades down the entire instantiation (and
286 // through its composite base levels), recreating every object below it. Since every
287 // non-root object is, by construction, a descendant of a component root, this is
288 // sufficient.
289 //
290 // A consequence worth remembering: because every live object is recreated, every
291 // QQmlJavaScriptExpression on those objects is recreated too, freshly bound to newUnit. By
292 // the time refreshBindings() runs, the only expressions still referencing oldUnit are the
293 // dead, detached leftovers of the resets. That is exactly why refreshBindings() can simply
294 // disable (null) them instead of remapping them to newUnit.
295 //
296 // When we switch to partial patching -- leaving objects the diff did not affect in place
297 // and only remapping their compilation unit instead of rebuilding them -- this invariant
298 // breaks and two things need care:
299 //
300 // * Affectedness: we need to classify each object as unaffected (keep it, just remap its
301 // CU), patchable (only constants or bindings have changed; no structural changes),
302 // needing VME rebuild, or having a changed non-composite (C++) base type (cannot
303 // be patched in place; its instantiating component must recreate it). The
304 // CompilationUnitDiff carries this information. We'll need to compute it from the
305 // given compilation units.
306 //
307 // * refreshBindings(): an object left in place keeps its original expressions, whose
308 // function() still points into oldUnit->runtimeFunctions. Disabling those would kill
309 // bindings that are supposed to keep working. Each such expression must instead be
310 // remapped to the function at the same index in newUnit->runtimeFunctions, i.e.
311 // refreshBindings() has to take newUnit again and translate functions rather than null
312 // them. As an additional complication, we must recognize that adding or removing functions
313 // shifts function indices. So the function a binding referred to in the old compilation
314 // unit may have a different index in the new one.
315 std::vector<ObjectAndIndex> rebuild;
316
317 for (QObject *object : objects) {
318 const QVarLengthArray<int, 4> indices = objectIndices(object, oldUnit);
319 for (int index : indices) {
320 // Objects instantiated by an enclosing component (indices beyond the CU, explicit
321 // Component content) are recreated when that enclosing root is rebuilt, so we don't
322 // record them here.
323 if (index >= oldUnit->objectCount())
324 continue;
325 const auto flags = oldUnit->objectAt(index)->flags();
326 if (index != 0 && !(flags & QV4::CompiledData::Object::IsInlineComponentRoot))
327 continue;
328
329 // A component root whose own non-composite (C++) base type changed cannot be
330 // rebuilt in place: reset() + repopulateBindings() reuses the same QObject, which is
331 // still of the old C++ class. Bail so the caller falls back to a full reload.
332 // (If the index no longer exists in the new CU the root is obsolete; rebuildObject()
333 // skips it and the remap loop below retires it.)
334 if (index < newUnit->objectCount()
335 && hasChangedNonCompositeBaseType(oldUnit, newUnit, index)) {
336 return false;
337 }
338
339 rebuild.push_back({ object, indices.first() });
340 break;
341 }
342 }
343
344 // Sort by ascending cuIndex so that parent objects are rebuilt before their
345 // children. A parent's reset() properly retires children (they still point to
346 // oldUnit) and repopulateBindings recreates them. This avoids leaking orphaned
347 // children and naturally handles cases where the context needs more ID slots.
348 std::sort(rebuild.begin(), rebuild.end(),
349 [](const ObjectAndIndex &a, const ObjectAndIndex &b) { return a.index < b.index; });
350
351 // Pre-compute which objects to skip: if an ancestor is also in the rebuild
352 // list, the child will be properly retired and recreated during the ancestor's
353 // rebuild. Rebuilding it individually would be wasted work on a stale pointer.
354 QSet<QObject *> skip;
355 for (const auto &[object, cuIndex] : rebuild) {
356 const QObjectList &children = object->children();
357 for (QObject *child : children)
358 skip.insert(child);
359 }
360
361 for (const auto &[object, cuIndex] : rebuild) {
362 if (skip.contains(object))
363 continue;
364
365 rebuildObject(object, cuIndex, oldUnit, newUnit);
366 }
367
368 for (QObject *object : objects) {
369 QQmlData *ddata = QQmlData::get(object);
370 if (ddata->compilationUnit == newUnit)
371 continue;
372 if (ddata->compilationUnit != oldUnit)
373 continue;
374
375 // Only remap objects whose cuObjectIndex is still valid in the new CU.
376 // Objects whose index is out of range are obsolete (they belonged to
377 // the old type definition and have no counterpart in the new one).
378 if (ddata->cuObjectIndex >= newUnit->objectCount()) {
379 // Null the VME's compilation unit so stale alias lookups
380 // (triggered by refreshBindings) safely return nullptr from
381 // findCompiledObject() instead of asserting.
382 if (ddata->hasVMEMetaObject) {
383 for (auto *vmeMeta = static_cast<QQmlVMEMetaObject *>(
384 QObjectPrivate::get(object)->metaObject);
385 vmeMeta; vmeMeta = vmeMeta->parentVMEMetaObject()) {
386 if (vmeMeta->compilationUnit() == oldUnit)
387 vmeMeta->setCompilationUnit(nullptr);
388 }
389 }
390 continue;
391 }
392
393 ddata->compilationUnit = newUnit;
394 if (!ddata->hasVMEMetaObject)
395 continue;
396
397 for (QQmlVMEMetaObject *vmeMeta =
398 static_cast<QQmlVMEMetaObject *>(QObjectPrivate::get(object)->metaObject);
399 vmeMeta; vmeMeta = vmeMeta->parentVMEMetaObject()) {
400 if (vmeMeta->compilationUnit() == oldUnit
401 && vmeMeta->qmlObjectId() < newUnit->objectCount()) {
402 vmeMeta->setCompilationUnit(newUnit);
403 }
404 }
405 }
406 return true;
407}
408
409// Remove any function of the old CU from a QQmlJavaScriptExpression.
410// Returns true if the function has been cleared or there was none to begin with.
411static bool
412clearOldExpressionFunction(QQmlJavaScriptExpression *expr,
413 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit)
414{
415 QV4::Function *f = expr->function();
416 if (!f)
417 return true;
418
419 if (f->executableCompilationUnit() != oldUnit.data())
420 return false;
421
422 expr->setFunction(nullptr);
423 return true;
424}
425
426static void
427cleanAndRefreshExpressionsRecursive(const QQmlRefPointer<QQmlContextData> &context,
428 const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit)
429{
430 for (auto child = context->childContexts(); child; child = child->nextChild())
431 cleanAndRefreshExpressionsRecursive(child, oldUnit);
432
433 for (auto *expr = context->expressions(); expr; expr = expr->nextExpression()) {
434 if (!clearOldExpressionFunction(expr, oldUnit))
435 expr->refresh();
436 }
437}
438
439void refreshBindings(const QQmlRefPointer<QV4::ExecutableCompilationUnit> &oldUnit)
440{
441 cleanAndRefreshExpressionsRecursive(
442 QQmlContextData::get(oldUnit->engine->qmlEngine()->rootContext()), oldUnit);
443}
444
445} // namespace QQmlPreview
446
447QT_END_NAMESPACE
bool applyDiff(std::vector< QObject * > &objects, const QQmlRefPointer< QV4::ExecutableCompilationUnit > &oldUnit, const QQmlRefPointer< QV4::ExecutableCompilationUnit > &newUnit)
static std::vector< CompositeLevel > collectCompositeLevels(const CompositeLevel &instanceLevel, const QQmlRefPointer< QV4::ExecutableCompilationUnit > &oldUnit, const QQmlRefPointer< QV4::ExecutableCompilationUnit > &newUnit)
static bool clearOldExpressionFunction(QQmlJavaScriptExpression *expr, const QQmlRefPointer< QV4::ExecutableCompilationUnit > &oldUnit)
static bool hasChangedNonCompositeBaseType(const QQmlRefPointer< QV4::ExecutableCompilationUnit > &oldUnit, const QQmlRefPointer< QV4::ExecutableCompilationUnit > &newUnit, int objectIndex)
static QVarLengthArray< int, 4 > objectIndices(QObject *object, const QQmlRefPointer< QV4::ExecutableCompilationUnit > &oldUnit)
static void cleanAndRefreshExpressionsRecursive(const QQmlRefPointer< QQmlContextData > &context, const QQmlRefPointer< QV4::ExecutableCompilationUnit > &oldUnit)
static void rebuildObject(QObject *object, int cuIndex, const QQmlRefPointer< QV4::ExecutableCompilationUnit > &oldUnit, const QQmlRefPointer< QV4::ExecutableCompilationUnit > &newUnit)
static QQmlPropertyCache::ConstPtr nonCompositeBaseType(const QQmlPropertyCache::ConstPtr &propertyCache)
void refreshBindings(const QQmlRefPointer< QV4::ExecutableCompilationUnit > &oldUnit)
Combined button and popup list for selecting options.