Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. Qt for Python
  4. QML-registered PySide6 object ownership
Forum Updated to NodeBB v4.3 + New Features

QML-registered PySide6 object ownership

Scheduled Pinned Locked Moved Solved Qt for Python
11 Posts 3 Posters 1.5k Views 1 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • S Offline
    S Offline
    SanderVc
    wrote on last edited by
    #1

    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:

    1. 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?)
    2. From internet search and experience: When QML instantiates an item that was registered through qmlRegisterType, its Qt parent will be set as null.
    3. From internet search: A null-parented QObject will be destroyed at application exit by Qt.
    4. From experience: At Python exit, Shiboken visits all remaining Python Shiboken wrappers and deletes them as necessary.
    5. 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.

    1 Reply Last reply
    0
    • F Offline
      F Offline
      friedemannkleint
      wrote on last edited by
      #2

      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 ) ?

      S 1 Reply Last reply
      0
      • F friedemannkleint

        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 ) ?

        S Offline
        S Offline
        SanderVc
        wrote on last edited by SanderVc
        #3

        @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.

        jeremy_kJ 1 Reply Last reply
        0
        • M mliberty referenced this topic on
        • S Offline
          S Offline
          SanderVc
          wrote on last edited by
          #4

          Changing from deleteLater() to Python del actually increased the amount of segfaults. I will keep trying to make a minimal example, but also still interested in the general approach recommended.

          1 Reply Last reply
          0
          • S SanderVc

            @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.

            jeremy_kJ Offline
            jeremy_kJ Offline
            jeremy_k
            wrote on last edited by
            #5

            @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.

            Asking a question about code? http://eel.is/iso-c++/testcase/

            S 1 Reply Last reply
            0
            • jeremy_kJ jeremy_k

              @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.

              S Offline
              S Offline
              SanderVc
              wrote on last edited by SanderVc
              #6

              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?

              1 Reply Last reply
              0
              • jeremy_kJ Offline
                jeremy_kJ Offline
                jeremy_k
                wrote on last edited by
                #7

                @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.

                Asking a question about code? http://eel.is/iso-c++/testcase/

                jeremy_kJ 1 Reply Last reply
                0
                • jeremy_kJ jeremy_k

                  @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_kJ Offline
                  jeremy_kJ Offline
                  jeremy_k
                  wrote on last edited by
                  #8

                  @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
                  

                  Asking a question about code? http://eel.is/iso-c++/testcase/

                  1 Reply Last reply
                  0
                  • S Offline
                    S Offline
                    SanderVc
                    wrote on last edited by
                    #9

                    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.

                    1 Reply Last reply
                    0
                    • S SanderVc has marked this topic as solved on
                    • S Offline
                      S Offline
                      SanderVc
                      wrote on last edited by
                      #10

                      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.

                      1 Reply Last reply
                      0
                      • jeremy_kJ Offline
                        jeremy_kJ Offline
                        jeremy_k
                        wrote on last edited by
                        #11

                        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.

                        Asking a question about code? http://eel.is/iso-c++/testcase/

                        1 Reply Last reply
                        0

                        • Login

                        • Login or register to search.
                        • First post
                          Last post
                        0
                        • Categories
                        • Recent
                        • Tags
                        • Popular
                        • Users
                        • Groups
                        • Search
                        • Get Qt Extensions
                        • Unsolved