diff options
| author | Linus Jahn <lnj@kaidan.im> | 2026-08-06 01:03:07 +0200 |
|---|---|---|
| committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2026-09-08 14:12:44 +0000 |
| commit | a5fca50366313c05cb0e1a573761871d93554cfb (patch) | |
| tree | 44f7b31554329f1dfa4328d4601cde2d290e5c4d | |
| parent | 374476a50c5031d1790cda7a2a286e92b50b5432 (diff) | |
QtQuick Templates: Don't begin deferred execution re-entrantly
Populating a deferred property can re-enter quickBeginDeferred():
writing the property notifies its observers, and a binding among them
may read the same property back. If the deferred binding evaluates to
null, the pointer stays null, so the caller's "if (!delegate ||
complete)" check does not stop that second execution.
The second QtQuickPrivate::beginDeferred() then incremented
QQmlEnginePrivate::inProgressCreations and stored its DeferredState in
the delegate, where it replaced the outer one. The outer state ended up
in a local variable of the nested call and was destroyed without ever
being completed, so the counter was never decremented and the engine
complained on teardown:
There are still "N" items in the process of being created at engine
destruction.
Bail out when the delegate is already executing.
This is hit in practice by kirigami-addons' FormRadioDelegate, which
uses RadioButton { contentItem: null }, in combination with
qqc2-desktop-style, whose indicator reads control.contentItem from its
x/y bindings.
Pick-to: 6.11
Fixes: QTBUG-148846
Change-Id: I0ddb8ef374efeb68ce59db285d54664e99606854
Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
Reviewed-by: Olivier De Cannière <olivier.decanniere@qt.io>
(cherry picked from commit 77fc1fd6d4d3b2c3222564f34a0f80ea102fa9e4)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
| -rw-r--r-- | src/quicktemplates/qquickdeferredexecute_p_p.h | 5 | ||||
| -rw-r--r-- | tests/auto/quickcontrols/deferred/data/reentrantExecution.qml | 9 | ||||
| -rw-r--r-- | tests/auto/quickcontrols/deferred/tst_qquickdeferred.cpp | 66 |
3 files changed, 80 insertions, 0 deletions
diff --git a/src/quicktemplates/qquickdeferredexecute_p_p.h b/src/quicktemplates/qquickdeferredexecute_p_p.h index aa6ddc4c87..946ae584fb 100644 --- a/src/quicktemplates/qquickdeferredexecute_p_p.h +++ b/src/quicktemplates/qquickdeferredexecute_p_p.h @@ -41,6 +41,11 @@ void quickBeginDeferred(QObject *object, const QString &property, QQuickDeferred if (!QQmlVME::componentCompleteEnabled()) return; + // Populating can re-enter this: writing the property notifies observers, and a binding among + // them may read it back. Beginning again would orphan the outer DeferredState. + if (delegate.isExecuting()) + return; + QtQuickPrivate::beginDeferred(object, property, &delegate, delegate.setExecuting(true)); delegate.setExecuting(false); } diff --git a/tests/auto/quickcontrols/deferred/data/reentrantExecution.qml b/tests/auto/quickcontrols/deferred/data/reentrantExecution.qml new file mode 100644 index 0000000000..069f40510e --- /dev/null +++ b/tests/auto/quickcontrols/deferred/data/reentrantExecution.qml @@ -0,0 +1,9 @@ +// Copyright (C) 2026 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +import test + +// Overrides the default from classBegin(), so the property changes while it is being executed. +ReentrantDeferredTester { + objectProperty: null +} diff --git a/tests/auto/quickcontrols/deferred/tst_qquickdeferred.cpp b/tests/auto/quickcontrols/deferred/tst_qquickdeferred.cpp index 1c50531d57..bc1685b4c6 100644 --- a/tests/auto/quickcontrols/deferred/tst_qquickdeferred.cpp +++ b/tests/auto/quickcontrols/deferred/tst_qquickdeferred.cpp @@ -7,6 +7,8 @@ #include <QtQuick/qquickitem.h> #include <QtQuickTemplates2/private/qquickdeferredexecute_p_p.h> #include <QQmlIncubator> +#include <QtQml/private/qqmlengine_p.h> +#include <QQmlParserStatus> class DeferredPropertyTester : public QObject { @@ -42,6 +44,55 @@ private: QQuickDeferredPointer<QQuickItem> m_object = nullptr; }; +// Mirrors how QQuickControl handles its deferred contentItem. +class ReentrantDeferredTester : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + Q_PROPERTY(QQuickItem *objectProperty READ objectProperty WRITE setObjectProperty NOTIFY objectChanged) + Q_CLASSINFO("DeferredPropertyNames", "objectProperty") + +public: + void classBegin() override { m_object = new QQuickItem; } // the default a style would set + void componentComplete() override { execute(true); } + + QQuickItem *objectProperty() + { + execute(false); + return m_object; + } + + void setObjectProperty(QQuickItem *obj) + { + if (m_object == obj) + return; + m_object = obj; + + // Stands in for setContentItem_helper(), where writing the property updates the implicit + // size, which notifies observers, and a binding among them reads the property back. + if (m_object.isExecuting()) + objectProperty(); + else + emit objectChanged(); + } + +signals: + void objectChanged(); + +private: + void execute(bool complete) + { + if (m_object.wasExecuted()) + return; + if (!m_object || complete) + quickBeginDeferred(this, "objectProperty", m_object); + if (complete) + quickCompleteDeferred(this, "objectProperty", m_object); + } + + QQuickDeferredPointer<QQuickItem> m_object = nullptr; +}; + class tst_qquickdeferred : public QQmlDataTest { Q_OBJECT @@ -50,6 +101,7 @@ public: private slots: void noSpuriousBinding(); void abortedIncubation(); + void reentrantExecution(); }; @@ -84,6 +136,20 @@ void tst_qquickdeferred::abortedIncubation() } } +// Reading the property back while it is being populated must not start a second deferred +// creation: the outer one would be orphaned and never completed. +void tst_qquickdeferred::reentrantExecution() +{ + qmlRegisterType<ReentrantDeferredTester>("test", 1, 0, "ReentrantDeferredTester"); + + QQmlEngine engine; + QQmlComponent comp(&engine, testFileUrl("reentrantExecution.qml")); + std::unique_ptr<QObject> root(comp.create()); + QVERIFY2(root, qPrintable(comp.errorString())); + + QCOMPARE(QQmlEnginePrivate::get(&engine)->inProgressCreations, 0); +} + QTEST_MAIN(tst_qquickdeferred) #include "tst_qquickdeferred.moc" |
