qml: apply fixes for strongly held references in PyQt python wrappers#10701
qml: apply fixes for strongly held references in PyQt python wrappers#10701accumulator wants to merge 2 commits into
Conversation
accumulator
commented
Jun 12, 2026
- remove lambda style signal handlers, unless absolutely necessary
- replace signal self-connects with QMetaObject.invokeMethod
- refactor ad-hoc broadcastSucceeded/broadcastFailed signal self-connect in QETxDetails to callbacks
|
fixes issues found by #10699 |
9984484 to
ab08004
Compare
- remove lambda style signal handlers, unless absolutely necessary - replace signal self-connects with QMetaObject.invokeMethod - refactor ad-hoc broadcastSucceeded/broadcastFailed signal self-connect in QETxDetails to callbacks
ab08004 to
f62096f
Compare
|
|
||
| self.register_callbacks() | ||
| self.destroyed.connect(lambda: self.on_destroy()) | ||
| self.destroyed.connect(self.on_destroy) |
There was a problem hiding this comment.
why were the lambdas there before?
are you sure they are not needed?
Sure it can work: just use a lambda, as I originally suggested. That is, you can just do: self.destroyed.connect(lambda: self.method()). The closure will keep self alive, as well as all its attributes and methods (although you must be careful which ones you access whilst the object is being deleted). Of course, if destroyed is directly connected to an instance method, it cannot work, because the connection will be automatically broken if the method is deleted.
(1) I tested with the following patch (on master), opening the addresses list and then closing the app:
diff --git a/electrum/gui/qml/qeaddresslistmodel.py b/electrum/gui/qml/qeaddresslistmodel.py
index fa7a546a6..6cd551214 100644
--- a/electrum/gui/qml/qeaddresslistmodel.py
+++ b/electrum/gui/qml/qeaddresslistmodel.py
@@ -123,7 +123,7 @@ class QEAddressCoinListModel(QAbstractListModel, QtEventListener):
self._filterModel = None
self.register_callbacks()
- self.destroyed.connect(lambda: self.on_destroy())
+ self.destroyed.connect(self.on_destroy)
QEConfig.instance.freezeReusedAddressUtxosChanged.connect(lambda: self.setDirty())
@@ -131,6 +131,7 @@ class QEAddressCoinListModel(QAbstractListModel, QtEventListener):
self.initModel()
def on_destroy(self):
+ print(f"heyheyhey. QEAddressCoinListModel.on_destroy() running")
self.unregister_callbacks()
@qt_event_listenerThe print statement does not seem to run.
(2) I also tested without changing the logic, on master, and the print statement is visible on stdout:
diff --git a/electrum/gui/qml/qeaddresslistmodel.py b/electrum/gui/qml/qeaddresslistmodel.py
index fa7a546a6..1415683f7 100644
--- a/electrum/gui/qml/qeaddresslistmodel.py
+++ b/electrum/gui/qml/qeaddresslistmodel.py
@@ -131,6 +131,7 @@ class QEAddressCoinListModel(QAbstractListModel, QtEventListener):
self.initModel()
def on_destroy(self):
+ print(f"heyheyhey. QEAddressCoinListModel.on_destroy() running")
self.unregister_callbacks()
@qt_event_listenerThere was a problem hiding this comment.
The lambdas were basically everywhere, as (years ago) I noticed without the lambda the on_destroy method would not be called in all cases, so using lambdas as default solution seemed to solve that issue.
While working on the boilerplate to test QObjects, the strong-ref issue with lambdas became apparent.
What I understood to be the case when diving deeper, is that the python destroyed signal handler gets called when the C++ object is deleted before the python wrapper (e.g. due to a Qt parent deleting its children on destruction) , and it will fail if the wrapper is deleted first (e.g. no strong refs left on the wrapper)
So the lambda style signal handler will
- always trigger
on_destroy - prohibit the wrapper from being garbage collected
And the class method signal handler will
- trigger
on_destroyonly if C++ object gets deleted first - garbage collect the wrapper
| # slot alive -- keeps this QEWallet (and via self.wallet etc. everything | ||
| # it references) alive even after the underlying C++ object is deleted, | ||
| # leaking the wallet. on_destroy() disconnects it to break that cycle. | ||
| self._destroyed_connection = self.destroyed.connect(lambda: self.on_destroy()) |
There was a problem hiding this comment.
Why is a lambda used here for QEWallet.destroyed, but not for QEAddressCoinListModel.destroyed?
I don't understand what the relevant difference between the two classes is.
There was a problem hiding this comment.
basically, when using a lambda, the object must take care of its own destruction obligations, like QEWallet needs to do as the member QObjects it has are not parented to QEWallet, and the strong-ref signal handler must be explicitly disconnected.
Note that in this PR I did not look at at QEWallet-parenting these contained objects like historymodel and addresscoinmodel etc as I remember there were issues with that in Qt5 times.
- move QEWallet.getInstanceFor to QEDaemon.getQEWalletInstanceFor - parent QEWallet instances to QEDaemon, parent listmodels to QEWallet - introduce QEDaemon.unloadWallet method for clean teardown of QEWallet instances - destroyed signal handler on QEWallet is now plain bound method style
bbbca17 to
6ba3a50
Compare