Solved QQmlContext::setContextProperty and deleting objects
-
We have a bunch of code that follows this pattern:
- create QObject based object
- setContextProperty in root context for this object: root->setContextProperty("somename", object)
- open QML item/dialog that uses this context property
- delete object then item/dialog closes
I cannot find anything in the docs for setContextProperty that says this is okay or not okay. I do know that accessing a context property that has been deleted is an immediate seg fault.
Why am I asking questions about this? Because I have an object that is being set to a context property that is somehow causing unrelated code to segfault. When I look at the trace it looks like all internal QML and jvm interpreter calls in the call stack. I cannot trace it back to any C++ code. If I don't delete the object (creates a memory leak as is) then it no longer crashes. So I suspect that deleting an object that has been set as a context property of the root context somehow corrupts the lookup for context properties or other things in general.
I plan on creating a separate project to test this and see if I can recreate. One thing that complicates this is that other objects created in exactly the same way do NOT seem to be causing this issue. So I am not completely sold on the idea that setting a context property on the root context causes this. However, I cannot find any other causes.
-
Hi fcarney,
My first guess about this problem is the way you are using the contextProperty or the assignment. As I do not have code is kind of hard to help you deeply, but some recommendations to take advantage of the context property without leaking or seg faults:
1 - Use objects that are going to be there from the beginning to the end. For example: Controllers or Utility classes.
2 - If you want to use properties to expose objects that are not permanent, the first thing to have in mind is a QMLContextProperty is not the way to communicate QML and C++ or Python. It Is the way you are able to use an object as reference on QML for commodity, if you need data for dialog/items is better use models. (Sorry if this is obvious, but is only the good practice.)
3 - If there is no option, please use a pointer instead of a concrete object:
pQMLContext->setContextProperty("contextProperty", *pObject);
pObject will point to the object you need to access at that moment, if you want other dialog only delete the memory and move the pointer to point to your other object and if you have no object please use a pattern of null object, using an interface as reference. Use the architecture in our favor is the best option in this scenario.
Sorry if this was not helpful, normally the memory leaks and segmentation fault errors are complex.
Regards,
Kenneth -
Okay, this is a non-issue. The culprit was someone had used this property object in another inappropriate place. It will not crash if set to null (does not exist). So I may implement a strategy to set context properties to nullptr before delete so we can see QML errors saying the object is undefined rather than seg faulting.
-
For future reference my test setup to try and break context properties.
main.cpp:
#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include <QHash> #include <QRandomGenerator> #include <QObject> #include <QDebug> class ContextSetter : public QObject { Q_OBJECT // test multiple context properties in one object Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) QString m_name; public: ContextSetter(QObject* parent=nullptr) : QObject(parent) {} QString name() const { return m_name; } public slots: void setName(QString name) { if (m_name == name) return; m_name = name; emit nameChanged(m_name); } signals: void nameChanged(QString name); }; class ContextHolder : public QObject { Q_OBJECT public: ContextHolder(QObject* parent=nullptr) : QObject(parent) , m_rootcontext(nullptr) { m_seededgen = QRandomGenerator::securelySeeded(); } void setrootcontext(QQmlContext* context){ m_rootcontext = context; } QQmlContext* rootcontext(){ return m_rootcontext; } public slots: QString createContextPropertyObject(QString name = QString()){ if(m_rootcontext){ char set = std::round(m_seededgen.generateDouble()) ? 'a' : 'A'; char head = set + std::floor(m_seededgen.generateDouble() * 26); // a to Z int body = std::round(m_seededgen.generateDouble() * 100000); QString propname; if(name.length()) propname = name; else propname = QString("%1%2").arg(head).arg(body); auto obj = new ContextSetter(this); obj->setName(propname); m_propobjects[propname] = obj; m_rootcontext->setContextProperty(propname, obj); qDebug() << "Created:" << propname; return propname; } return QString(); } void deleteContextPropertyObject(QString propname){ if(m_propobjects.contains(propname)){ auto obj = m_propobjects[propname]; m_propobjects.remove(propname); obj->deleteLater(); m_rootcontext->setContextProperty(propname, nullptr); qDebug() << "Deleted:" << propname; } } private: QQmlContext* m_rootcontext; QHash<QString, QObject*> m_propobjects; QRandomGenerator m_seededgen; }; int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); QQmlApplicationEngine engine; auto rootcontext = engine.rootContext(); ContextHolder cholder; cholder.setrootcontext(rootcontext); rootcontext->setContextProperty("contextholder", &cholder); const QUrl url(QStringLiteral("qrc:/main.qml")); QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, [url](QObject *obj, const QUrl &objUrl) { if (!obj && url == objUrl) QCoreApplication::exit(-1); }, Qt::QueuedConnection); engine.load(url); return app.exec(); } #include "main.moc"
main.qml:
import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Window 2.12 Window { id: mainwin width: 640 height: 480 visible: true title: qsTr("setContextProperty object deletion testing") property var proplist: [] Timer { id: createtimer interval: 1000 repeat: true running: true onTriggered: { var propname = contextholder.createContextPropertyObject() proplist.push(propname) } } Timer { id: deletetimer interval: 1000 repeat: true running: true onTriggered: { var propname = proplist.pop() contextholder.deleteContextPropertyObject(propname) } } Timer { id: cycledialog interval: 500 repeat: true running: true property string propname: "KillMe" Component.onCompleted: { contextholder.createContextPropertyObject(propname) contextholder.deleteContextPropertyObject(propname) } onTriggered: { contextholder.createContextPropertyObject("Atestobject") contextholder.createContextPropertyObject("atestobject") contextholder.createContextPropertyObject("Ztestobject") contextholder.createContextPropertyObject("ztestobject") contextholder.createContextPropertyObject("Qtestobject") contextholder.createContextPropertyObject("qtestobject") testdialogloader.active = true } } Loader { id: testdialogloader active: false sourceComponent: testdialog } Component { id:testdialog Rectangle { width: 200 height: 200 color: "red" Column { Text { text: Atestobject.name } Text { text: atestobject.name } Text { text: Ztestobject.name } Text { text: atestobject.name } Text { text: Qtestobject.name } Text { text: qtestobject.name } Text { text: "propname: " + KillMe } } Timer { id: disabletimer interval: 250 repeat: false running: true onTriggered: { testdialogloader.active = false contextholder.deleteContextPropertyObject("Atestobject") contextholder.deleteContextPropertyObject("atestobject") contextholder.deleteContextPropertyObject("Ztestobject") contextholder.deleteContextPropertyObject("ztestobject") contextholder.deleteContextPropertyObject("Qtestobject") contextholder.deleteContextPropertyObject("qtestobject") } } } } }
Notice if you comment out:
m_rootcontext->setContextProperty(propname, nullptr);
Then it will crash on accessing the KillMe context property. So setting a previously used context property to nullptr is a way to protect yourself from seg faults.