Skip to content

qml: apply fixes for strongly held references in PyQt python wrappers#10701

Open
accumulator wants to merge 2 commits into
spesmilo:masterfrom
accumulator:qml_qobject_lifecycle_fixes
Open

qml: apply fixes for strongly held references in PyQt python wrappers#10701
accumulator wants to merge 2 commits into
spesmilo:masterfrom
accumulator:qml_qobject_lifecycle_fixes

Conversation

@accumulator

Copy link
Copy Markdown
Member
  • 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

@accumulator

Copy link
Copy Markdown
Member Author

fixes issues found by #10699

@accumulator accumulator force-pushed the qml_qobject_lifecycle_fixes branch from 9984484 to ab08004 Compare June 12, 2026 10:43
- 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
@accumulator accumulator force-pushed the qml_qobject_lifecycle_fixes branch from ab08004 to f62096f Compare June 12, 2026 13:37

self.register_callbacks()
self.destroyed.connect(lambda: self.on_destroy())
self.destroyed.connect(self.on_destroy)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why were the lambdas there before?
are you sure they are not needed?

ref https://stackoverflow.com/questions/16842955/widgets-destroyed-signal-is-not-fired-pyqt#comment58357066_35304400 :

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_listener

The 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_listener

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_destroy only if C++ object gets deleted first
  • garbage collect the wrapper

Comment thread electrum/gui/qml/qewallet.py Outdated
# 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())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@accumulator accumulator Jun 24, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@accumulator accumulator force-pushed the qml_qobject_lifecycle_fixes branch from bbbca17 to 6ba3a50 Compare June 24, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants