Crash in PyQt/Python3 app on exit

Phil Thompson phil at riverbankcomputing.com
Tue Jan 10 17:13:32 GMT 2023


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