diff options
| author | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2025-07-31 08:55:29 +0200 |
|---|---|---|
| committer | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2025-07-31 15:26:25 +0200 |
| commit | d580aae898e10e7b4491f2692f320d7706899c1e (patch) | |
| tree | 630e156fb6624b1f684bedb563f349c30239c2f7 /sources/pyside6 | |
| parent | 160884d98fc8fee21b1c7725f5706d1fae47ccb4 (diff) | |
libpyside: Fix corruption connecting several signals to one non-QObject receiver
The deletion of the weak ref in ~TrackingMethodDynamicSlot() did not
reliably stop the notification from being triggered, causing
corruption later on since the address of the dynamic slot was used as
user data.
Use the Python receiver object as user data instead (which is only
used for map look up) to delete the connection to protect against
multiple invocation.
Remove the deletion of the weakref since it is removed in the
notification.
Remove class TrackingMethodDynamicSlot as it does not really have any
functionality any more.
The comment about releasing the weakref in case DynamicSlot outlives
Python is apparently not an issue since otherwise the deletion of the
function in ~MethodDynamicSlot() would have caused issues.
Amends 33bd61d13d8d9e3794b6049891be62f3351313d9.
Pick-to: 6.9 6.8
Fixes: PYSIDE-3148
Task-number: PYSIDE-2810
Change-Id: Idc07d0774afaf99df93185c90e975291a42ffeaf
Reviewed-by: Shyamnath Premnadh <Shyamnath.Premnadh@qt.io>
Diffstat (limited to 'sources/pyside6')
| -rw-r--r-- | sources/pyside6/libpyside/dynamicslot.cpp | 51 | ||||
| -rw-r--r-- | sources/pyside6/tests/signals/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | sources/pyside6/tests/signals/nonqobject_receivers_test.py | 71 |
3 files changed, 82 insertions, 41 deletions
diff --git a/sources/pyside6/libpyside/dynamicslot.cpp b/sources/pyside6/libpyside/dynamicslot.cpp index cc6d50d82..3d9b9c1be 100644 --- a/sources/pyside6/libpyside/dynamicslot.cpp +++ b/sources/pyside6/libpyside/dynamicslot.cpp @@ -134,41 +134,8 @@ void MethodDynamicSlot::formatDebug(QDebug &debug) const << ", function=" << PySide::debugPyObject(m_function) << ')'; } -// Store a weak reference on pythonSelf. -class TrackingMethodDynamicSlot : public MethodDynamicSlot -{ - Q_DISABLE_COPY_MOVE(TrackingMethodDynamicSlot) -public: - explicit TrackingMethodDynamicSlot(PyObject *function, PyObject *pythonSelf, - PyObject *weakRef); - ~TrackingMethodDynamicSlot() override; - - void releaseWeakRef() { m_weakRef = nullptr; } - -private: - PyObject *m_weakRef; -}; - -TrackingMethodDynamicSlot::TrackingMethodDynamicSlot(PyObject *function, PyObject *pythonSelf, - PyObject *weakRef) : - MethodDynamicSlot(function, pythonSelf), - m_weakRef(weakRef) -{ -} - -TrackingMethodDynamicSlot::~TrackingMethodDynamicSlot() -{ - if (m_weakRef != nullptr) { - Shiboken::GilState gil; - // weakrefs must not be de-refed after the object has been deleted, - // else they get negative refcounts. - if (PepExt_Weakref_IsAlive(m_weakRef)) - Py_DECREF(m_weakRef); - } -} - // Delete the connection on receiver deletion by weakref -class PysideReceiverMethodSlot : public TrackingMethodDynamicSlot +class PysideReceiverMethodSlot : public MethodDynamicSlot { Q_DISABLE_COPY_MOVE(PysideReceiverMethodSlot) public: @@ -179,19 +146,21 @@ public: static void onPysideReceiverSlotDestroyed(void *data) { - auto *self = reinterpret_cast<PysideReceiverMethodSlot *>(data); - // Ensure the weakref is gone in case the connection stored in - // Qt's internals outlives Python. - self->releaseWeakRef(); + auto *pythonSelf = reinterpret_cast<PyObject *>(data); Py_BEGIN_ALLOW_THREADS - disconnectReceiver(self->pythonSelf()); + disconnectReceiver(pythonSelf); Py_END_ALLOW_THREADS } PysideReceiverMethodSlot::PysideReceiverMethodSlot(PyObject *function, PyObject *pythonSelf) : - TrackingMethodDynamicSlot(function, pythonSelf, - WeakRef::create(pythonSelf, onPysideReceiverSlotDestroyed, this)) + MethodDynamicSlot(function, pythonSelf) { + // PYSIDE-3148: The weakref is automatically deleted when the notification triggers. + // Note that notifications may trigger after deletion of TrackingMethodDynamicSlot in case + // of multiple connections to the same receiver, so, &DynamicSlot must not be used as user + // data. Also trying to actively deref a pending weak ref from ~TrackingMethodDynamicSlot() + // does not reliably prevent the notification from being triggered. + WeakRef::create(pythonSelf, onPysideReceiverSlotDestroyed, pythonSelf); } DynamicSlot* DynamicSlot::create(PyObject *callback) diff --git a/sources/pyside6/tests/signals/CMakeLists.txt b/sources/pyside6/tests/signals/CMakeLists.txt index dd8f4a016..094caaaf6 100644 --- a/sources/pyside6/tests/signals/CMakeLists.txt +++ b/sources/pyside6/tests/signals/CMakeLists.txt @@ -21,6 +21,7 @@ PYSIDE_TEST(qobject_callable_connect_test.py) PYSIDE_TEST(qobject_destroyed_test.py) PYSIDE_TEST(qobject_receivers_test.py) PYSIDE_TEST(qobject_sender_test.py) +PYSIDE_TEST(nonqobject_receivers_test.py) PYSIDE_TEST(ref01_test.py) PYSIDE_TEST(ref02_test.py) PYSIDE_TEST(ref03_test.py) diff --git a/sources/pyside6/tests/signals/nonqobject_receivers_test.py b/sources/pyside6/tests/signals/nonqobject_receivers_test.py new file mode 100644 index 000000000..4bb47e5df --- /dev/null +++ b/sources/pyside6/tests/signals/nonqobject_receivers_test.py @@ -0,0 +1,71 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +'''Test case for non-QObject.receivers''' + +import os +import sys +import unittest + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[1])) +from init_paths import init_test_paths # noqa: F401 +init_test_paths(False) + +from helper.usesqapplication import UsesQApplication # noqa: F401 + +from PySide6.QtGui import QAction # noqa: F401 + + +receiver_instances = 0 + + +class Receiver: + + def __init__(self): + global receiver_instances + receiver_instances += 1 + self.slot1Triggered = 0 + self.slot2Triggered = 0 + + def __del__(self): + global receiver_instances + receiver_instances -= 1 + + def slot1(self): + self.slot1Triggered += 1 + + def slot2(self): + self.slot2Triggered += 1 + + +class TestQObjectReceivers(UsesQApplication): + '''Test case for non-QObject.receivers''' + + @unittest.skipUnless(hasattr(sys, "getrefcount"), f"{sys.implementation.name} has no refcount") + def testBasic(self): + '''The test verifies that connections to methods of a non-QObject work + (TrackingMethodDynamicSlot). Also, despite being stored in the signal manager, + making connections should not increase references on the receiver or prevent + the receivers from being deleted, which is achieved using weak reference + tracking notifications. + 2 connections are used to trigger a corruption caused by multiple weak reference + notifications (PYSIDE-3148).''' + action1 = QAction("a1", qApp) # noqa: F821 + action2 = QAction("a2", qApp) # noqa: F821 + receiver = Receiver() + self.assertEqual(receiver_instances, 1) + base_ref_count = sys.getrefcount(receiver) + action1.triggered.connect(receiver.slot1) + action2.triggered.connect(receiver.slot2) + self.assertEqual(sys.getrefcount(receiver), base_ref_count) + action1.trigger() + action2.trigger() + self.assertEqual(receiver.slot1Triggered, 1) + self.assertEqual(receiver.slot2Triggered, 1) + receiver = 0 + self.assertEqual(receiver_instances, 0) + + +if __name__ == '__main__': + unittest.main() |
