QML-registered PySide6 object ownership
-
Hi all,
As I am trying to debug intermittent segfaults during application exit, I realized that I do not fully understand how the lifetime management of Qt-/QML-objects is supposed to work - in particular for PySide6 classes registered in the QML typesystem.
Unfortunately I have not been able to create a minimal example that causes a segfault. But I hope that someone here could point me to the "correct" way of doing things.
What I have understood from the various documentation and my experience:
- From docs: Shiboken (PySide6) tries to determine whether Python's garbage collector will destroy the underlying object along with the wrapper, or whether the underlying object will get destroyed by some other mechanism (manual deletion in C++ / deletion due to parent-child relationship / deletion by a QML engine?)
- From internet search and experience: When QML instantiates an item that was registered through qmlRegisterType, its Qt parent will be set as null.
- From internet search: A null-parented QObject will be destroyed at application exit by Qt.
- From experience: At Python exit, Shiboken visits all remaining Python Shiboken wrappers and deletes them as necessary.
- From QML docs: Any QML item which is part of the child "data" of another, automatically becomes its Qt child as well (that is to say: any item declared in the "normal QML way" should automatically become the Qt child of its QML parent).
So who should be responsible for deleting a PySide6 QObject-derived class instance, which was registered with qmlRegisterType and instantiated in QML?
- Should QMLEngine delete it when its visual parent is deleted? It would seem to make sense that normally everything QMLEngine creates, should be destroyed when the QMLEngine is destroyed. Point 5. also suggests this above (but it is not what I see happening).
- Should I manually manage the lifetime of each object QML creates? The fact that these classes appear with a null parent seems to suggest so.
- Should I let Shiboken clean up the object? This seems fragile. I don't suppose QMLEngine somehow keeps a Python object reference so that it won't get garbage collected.
As for what I see in practice:
- I created a class derived from PySide6 QObject and registered it using qmlRegisterType.
- I instantiated this class in the normal way in a QML tree.
- On application exit I sometimes get segfaults.
- I built a debug version of libShiboken and added some print statements. Turns out the Python class objects are still alive at application exit, or at least Shiboken thinks so. At Python "atexit" handler, Shiboken visits and destructs all objects. At that point, it finds at least one class instance of the Python class above. At that point, according to Shiboken:
- Python owns the underlying QQuickItem (not C++, thus not a QmlEngine either);
- Its C++ object is still valid;
- Its parent is null;
- In some cases it segfaults trying to get a C++ pointer to the object, and in some cases it segfaults afterwards while trying to destruct it.
It sounds to me like two parties are trying to destroy these objects at the same time, which could cause a segfault. It could also not cause a segfault if one beats the other and e.g. Shiboken sees the object is already gone.
To me, it seems unlikely that Shiboken or the Python GC should clean these objects up. After all, they are created by the QMLEngine. So why are they still alive at Python atexit, when the QMLEngine is already gone?Does anyone have any pointers? Thanks in any case.
-
Have you called del on the QmlEngine/QuickView before exiting the app (see for example https://doc.qt.io/qtforpython-6/examples/example_quick_models_objectlistmodel.html ) ?
-
Have you called del on the QmlEngine/QuickView before exiting the app (see for example https://doc.qt.io/qtforpython-6/examples/example_quick_models_objectlistmodel.html ) ?
@friedemannkleint said in QML-registered PySide6 object ownership:
Have you called del on the QmlEngine/QuickView before exiting the app (see for example https://doc.qt.io/qtforpython-6/examples/example_quick_models_objectlistmodel.html ) ?
My deinitialization code is:
self.engine.deleteLater() self.wait(100) self.engine = None self.wait(10)
(this is on a QQmlApplicationEngine which is stored only in this member, nowhere else).
This is quite different, as deleteLater operates on the underlying C++ object. However Shiboken wraps that method too, so I don't know if it does anything special when it is called.
In any case, good thing to try, I'll report back if this has any influence on my segfaults.
-
-
@friedemannkleint said in QML-registered PySide6 object ownership:
Have you called del on the QmlEngine/QuickView before exiting the app (see for example https://doc.qt.io/qtforpython-6/examples/example_quick_models_objectlistmodel.html ) ?
My deinitialization code is:
self.engine.deleteLater() self.wait(100) self.engine = None self.wait(10)
(this is on a QQmlApplicationEngine which is stored only in this member, nowhere else).
This is quite different, as deleteLater operates on the underlying C++ object. However Shiboken wraps that method too, so I don't know if it does anything special when it is called.
In any case, good thing to try, I'll report back if this has any influence on my segfaults.
@SanderVc said in QML-registered PySide6 object ownership:
My deinitialization code is:
self.engine.deleteLater() self.wait(100) self.engine = None self.wait(10)
@SanderVc said in QML-registered PySide6 object ownership:
Changing from deleteLater() to Python del actually increased the amount of segfaults.
Is self.wait() causing the thread to yield? Is the engine's thread this thread? If so, the deleteLater() hasn't had a chance to occur. That requires giving the event loop a chance to run. Even if the engine is in another thread, a wait is guessing. The QObject.destroyed() signal is a better option.
-
@SanderVc said in QML-registered PySide6 object ownership:
My deinitialization code is:
self.engine.deleteLater() self.wait(100) self.engine = None self.wait(10)
@SanderVc said in QML-registered PySide6 object ownership:
Changing from deleteLater() to Python del actually increased the amount of segfaults.
Is self.wait() causing the thread to yield? Is the engine's thread this thread? If so, the deleteLater() hasn't had a chance to occur. That requires giving the event loop a chance to run. Even if the engine is in another thread, a wait is guessing. The QObject.destroyed() signal is a better option.
Is self.wait() causing the thread to yield? Is the engine's thread this thread? If so, the deleteLater() hasn't had a chance to occur. That requires giving the event loop a chance to run. Even if the engine is in another thread, a wait is guessing. The QObject.destroyed() signal is a better option.
Good point, it was not very helpful for me to post that without showing what wait() does. But this is the implementation:
def wait(self, ms): end = time.time() + ms * 0.001 while time.time() < end: self.processEvents() self.sendPostedEvents()
(edit: "self" here, and in my previous snippet, is a QApplication-derived object)
So using processEvents() should let the event loop run.
But are you suggesting that Qt, in the main event loop should clean up all QML-instantiated objects when a QML engine is destroyed? Since I noticed the parent of the object is null, I don't think it will be destroyed in the regular way.
Or should the QML engine clean it up by some other mechanism than parent-child-relationship?
-
@SanderVc said in QML-registered PySide6 object ownership:
Is self.wait() causing the thread to yield? Is the engine's thread this thread? If so, the deleteLater() hasn't had a chance to occur. That requires giving the event loop a chance to run. Even if the engine is in another thread, a wait is guessing. The QObject.destroyed() signal is a better option.
Good point, it was not very helpful for me to post that without showing what wait() does. But this is the implementation:
self.processEvents()
Invoking processEvents() and sendPostedEvents() within a function makes me nervous. It's easy to invalidate something that was temporarily cached earlier in the function.
(edit: "self" here, and in my previous snippet, is a QApplication-derived object)
So using processEvents() should let the event loop run.
But are you suggesting that Qt, in the main event loop should clean up all QML-instantiated objects when a QML engine is destroyed? Since I noticed the parent of the object is null, I don't think it will be destroyed in the regular way.
That wasn't my intent, but it makes sense that it happens there or in the engine destructor. Otherwise, I would expect anything that the engine is managing and that hasn't already been destroyed to be permanently orphaned. That would make destroying an engine potentially very costly.
-
@SanderVc said in QML-registered PySide6 object ownership:
Is self.wait() causing the thread to yield? Is the engine's thread this thread? If so, the deleteLater() hasn't had a chance to occur. That requires giving the event loop a chance to run. Even if the engine is in another thread, a wait is guessing. The QObject.destroyed() signal is a better option.
Good point, it was not very helpful for me to post that without showing what wait() does. But this is the implementation:
self.processEvents()
Invoking processEvents() and sendPostedEvents() within a function makes me nervous. It's easy to invalidate something that was temporarily cached earlier in the function.
(edit: "self" here, and in my previous snippet, is a QApplication-derived object)
So using processEvents() should let the event loop run.
But are you suggesting that Qt, in the main event loop should clean up all QML-instantiated objects when a QML engine is destroyed? Since I noticed the parent of the object is null, I don't think it will be destroyed in the regular way.
That wasn't my intent, but it makes sense that it happens there or in the engine destructor. Otherwise, I would expect anything that the engine is managing and that hasn't already been destroyed to be permanently orphaned. That would make destroying an engine potentially very costly.
@jeremy_k said in QML-registered PySide6 object ownership:
@SanderVc said in QML-registered PySide6 object ownership:
But are you suggesting that Qt, in the main event loop should clean up all QML-instantiated objects when a QML engine is destroyed? Since I noticed the parent of the object is null, I don't think it will be destroyed in the regular way.
That wasn't my intent, but it makes sense that it happens there or in the engine destructor.
Possibly interesting toy program:
#include <QGuiApplication> #include <QQmlApplicationEngine> int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine *engine = new QQmlApplicationEngine; QObject::connect(engine, &QQmlApplicationEngine::objectCreated, [engine](){ qDebug() << "destroying engine"; delete engine; qDebug() << "done";}); engine->loadData(R"(import QtQuick; Item { Component.onDestruction: print("going away") })"); return 0; }
Output:
destroying engine qml: going away done
-
Thanks for your insights. I should probably get rid of these processEvents() calls.
I will also try to add some onDestruction handlers akin to your toy program. I wonder if the lifetime of the PySide object is tightly coupled to its Qml instantiation. The fact that Shiboken reports it being "owned" by the Python garbage collector suggests that maybe my Qml object is already destroyed but its Python object remains.
-
-
OK, so although I still feel like I don't fully know what is going on, I do know that in my case my problem had little to do with the object ownership model.
It turns out my ~QQmlEngine() was never called because of the way I was exiting the application. I was eventually calling app.exit() directly. Probably this never returned control to the event loop and didn't give objects a proper way to deinitialize.
Now I am calling deleteLater() on my qml engine, then setting a QTimer to call application.quit(). It does the trick.
-
I had somehow forgotten that python was involved.
Predictability of object destruction has been a recurring issue for me, although I don't recall seeing a normal program termination result in an object never being destroyed. I also don't remember seeing deleteLater() needing an additional delay as long as the event loop is still running when the function is called.
If there's a brief example that you don't mind sharing, I'm interested in seeing the suspected cause. Maybe it's a new-to-me pitfall that I should be avoiding.