Crash in PyQt/Python3 app on exit

Phil Thompson phil at riverbankcomputing.com
Thu Jan 26 15:31:57 GMT 2023


Using the current pre-release packages at...

https://www.riverbankcomputing.com/pypi/simple/

...I can't reproduce the problem. test.py crashes and test-cleanup.py 
doesn't (as you are expecting).

Phil

On 25/01/2023 22:56, Kevin Constantine wrote:
> Hey Phil-
> 
> I have an environment set up with:
> Sip: 6.7.5
> PyQt5: 5.15.7
> Qt: 5.15.2
> 
> I'm still seeing the crash.
> 
> I've updated the reproducer repository with the changes to support the
> newer version of Sip: https://github.com/kevinconstantine/pyqt-crash
> 
> -kevin
> 
> On Tue, Jan 10, 2023 at 12:44 PM Kevin Constantine <
> kevin.constantine at disneyanimation.com> wrote:
> 
>> Thanks Phil-
>> I'll give it a whirl and report back.
>> 
>> -kevin
>> 
>> On Tue, Jan 10, 2023 at 12:13 PM Phil Thompson <
>> phil at riverbankcomputing.com> wrote:
>> 
>>> Sorry, but the first thing to do is upgrade to current versions of 
>>> SIP
>>> and PyQt5.
>>> 
>>> Phil
>>> 
>>> On 09/01/2023 19:28, Kevin Constantine wrote:
>>> > Hello!
>>> >
>>> >
>>> >
>>> > We have a C++/Qt application that provides both a C++ api and a SIP
>>> > wrapped
>>> > Python api.  When converting everything from Python 2.7 to Python 3, we
>>> > began to see crashes when python applications using our api
>>> > exited/quit.  I
>>> > have been tracking this crash down, and I think I have a decent
>>> > understanding of what's causing it, which I'll attempt to explain
>>> > below.
>>> > However, I don't feel like I have a good understanding of why the code
>>> > path
>>> > that leads to the crash does what it does.  In other words, I'm not
>>> > sure if
>>> > it's expected behavior, or a bug.  That's what I'm hoping you might be
>>> > able
>>> > to assist with.
>>> >
>>> >
>>> >
>>> > I have a simple-ish reproducer:
>>> >
>>> > https://github.com/kevinconstantine/pyqt-crash
>>> >
>>> > that demonstrates the problem if needed. If it's a bug, I have a patch
>>> > that
>>> > prevents the crash, but given my current understanding of what's going
>>> > on,
>>> > it may not be the right direction.
>>> >
>>> >
>>> >
>>> > Versions
>>> >
>>> > Python: 3.7.7
>>> >
>>> > Qt: 5.15.2
>>> >
>>> > PyQt: 5.15.4 (problem likely exists in 6.4.0 as well given code
>>> > similarities)
>>> >
>>> > sip: 4.19.30
>>> >
>>> >
>>> >
>>> > Summary
>>> >
>>> > It looks like in Python3, PyQt has registered a call-back when the
>>> > application exits (cleanup_on_exit()).  The call chain eventually
>>> > results in
>>> > sip_api_visit_wrapper() looping over SIP objects and calling PyQt's
>>> > cleanup_qobject().  cleanup_qobject() checks if the object is owned by
>>> > Python and checks if the object is a QObject.  If the object is owned
>>> > by
>>> > C++ or not a QObject, it returns early.  Once we've established that
>>> > the
>>> > object is owned by Python, it looks like it transfers ownership of the
>>> > Python sipWrapper object to C++, and then calls delete on the address
>>> > of
>>> > the sipWrapper object.
>>> >
>>> >
>>> >
>>> > This delete causes the C++ destructors to get called, however, the SIP
>>> > destructor is never called.  I would expect that a python owned object
>>> > would have that SIP destructor called.  We are relying on doing some
>>> > cleanup inside of the SIP destructor, and because that doesn't get
>>> > executed, the application crashes.
>>> >
>>> >
>>> >
>>> >
>>> >
>>> > More detail on what we're doing
>>> >
>>> > For years, we've leveraged a couple of examples (here
>>> > <https://riverbankcomputing.com/pipermail/pyqt/2005-March/010031.html>,
>>> 
>>> > and
>>> > here
>>> > <
>>> https://www.riverbankcomputing.com/pipermail/pyqt/2017-September/039548.html
>>> >)
>>> > to handle tracking shared pointers on the C++ side of things as they
>>> > got
>>> > created through Python.  We have a hand-coded SIP destructor that gets
>>> > called from Python that removes the shared pointer from a map so that
>>> > it
>>> > too gets destructed properly.  This all worked great until we tried to
>>> > migrate to Python 3 (cue the ominous music).
>>> >
>>> >
>>> >
>>> > What's happening (at least my understanding of what's happening)
>>> >
>>> > In Python3, PyQt has registered cleanup_on_exit() with
>>> > sipRegisterExitNotifier()
>>> > in qpy/QtCore/qpycore_init.cpp.  So when the application is exiting,
>>> > cleanup_on_exit() is called and the call-stack looks like:
>>> >
>>> >
>>> >
>>> > PyQt: cleanup_on_exit()
>>> >
>>> >   PyQt: pyqt5_cleanup_qobjects()
>>> >
>>> >     SIP: sip_api_visit_wrappers() // Loop over sip objects
>>> >
>>> >       PyQt: cleanup_qobject()
>>> >
>>> >
>>> >
>>> > Within qpy/QtCore/qpycore_public_api.cpp:cleanup_qobject(), several
>>> > checks
>>> > are made that cause the function to return early without doing
>>> > anything:
>>> >
>>> > 1. Anything not owned by Python returns early
>>> >
>>> > 2. Non QObjects return early
>>> >
>>> > 3. Running threads return early
>>> >
>>> >
>>> >
>>> > But if an object passes all of these checks, the code calls
>>> > sipTransferTo()
>>> > to transfer ownership of the sipWrapper object to C++, and then calls
>>> > delete on the address of the sipWrapper object.
>>> >
>>> >
>>> >
>>> > This delete causes the C++ destructors to get called, but most
>>> > importantly,
>>> > the SIP destructor that we're relying on, is never called.  As
>>> > mentioned
>>> > earlier, we are relying on the SIP destructor to clean up the global
>>> > shared_ptr storage when the Python object is destroyed.  My expectation
>>> > is
>>> > that a Python owned object would have that SIP destructor called just
>>> > like
>>> > the SIP constructor is called when the object is created.
>>> >
>>> >
>>> >
>>> > I added debugging output to SIP, PyQt, and my codebase in an effort to
>>> > understand what was happening.  I'm hesitant to include that in this
>>> > initial email as it gets kind of dense trying to explain what's going
>>> > on,
>>> > but I'm happy to provide it if I can interest anyone in going down the
>>> > rabbit hole with me on this one.
>>> >
>>> >
>>> >
>>> > I also have a small patch that "fixes" my crash, but I'm not sure that
>>> > I
>>> > completely have the right understanding of all of these pieces and that
>>> > this doesn’t cause an issue elsewhere.
>>> >
>>> >
>>> >
>>> > --- PyQt5-5.15.4.vanilla/qpy/QtCore/qpycore_public_api.cpp 2021-03-05
>>> > 01:57:14.957003000 -0800
>>> >
>>> > +++ PyQt5-5.15.4.kcc/qpy/QtCore/qpycore_public_api.cpp 2022-12-15
>>> > 08:40:06.644173000 -0800
>>> >
>>> > @@ -60,6 +60,13 @@
>>> >
>>> >              return;
>>> >
>>> >      }
>>> >
>>> >
>>> >
>>> > +    // Try to destroy the object if it still has a reference
>>> >
>>> > +    // Goal is to get the SIP dtor to fire.
>>> >
>>> > +    if (Py_REFCNT((PyObject *)sw)) {
>>> >
>>> > +        sipInstanceDestroyed(sw);
>>> >
>>> > +        return;
>>> >
>>> > +    }
>>> >
>>> > +
>>> >
>>> >      sipTransferTo((PyObject *)sw, SIP_NULLPTR);
>>> >
>>> >
>>> >
>>> >      Py_BEGIN_ALLOW_THREADS
>>> >
>>> >
>>> >
>>> > By calling sipInstanceDestroyed(), the SIP destructor fires, and
>>> > subsequently the C++ dtors get executed as well.  Everything appears to
>>> > get
>>> > cleaned up in the proper order, and we avoid the crash.  I have not
>>> > reproduced the issue in PyQt6, but there are no differences in
>>> > cleanup_qobject() between the two versions, so I'm assuming the
>>> > behavior is
>>> > similar.
>>> >
>>> >
>>> >
>>> > Thanks so much for any help you can provide in fixing this, or helping
>>> > me
>>> > understand better what's going on.
>>> >
>>> >
>>> >
>>> > -kevin
>>> 
>> 


More information about the PyQt mailing list