[PyQt] Re: SIP bug with object lifetime

Phil Thompson phil at riverbankcomputing.co.uk
Mon May 14 10:01:12 BST 2007


On Monday 14 May 2007 9:33 am, Giovanni Bajo wrote:
> On 14/05/2007 9.33, Phil Thompson wrote:
> >>>> =========================================
> >>>> from qt import *
> >>>> import weakref
> >>>>
> >>>> app = QApplication([])
> >>>>
> >>>> ql = QListView(None)
> >>>> viewport = ql.viewport()
> >>>>
> >>>> o = QObject(viewport)
> >>>> o.xxx = viewport  # bug-trigger!
> >>>>
> >>>> destroyed = []
> >>>> def cb(wr):
> >>>>      destroyed.append(1)
> >>>> wr = weakref.ref(o, cb)
> >>>>
> >>>> del o
> >>>> del viewport
> >>>> assert not destroyed, "object destroyed too early #1!"
> >>>>
> >>>> import gc
> >>>> gc.collect()
> >>>> assert not destroyed, "object destroyed too early #2!"
> >>>>
> >>>> del ql
> >>>> import gc
> >>>> gc.collect()
> >>>> assert destroyed, "object never destroyed!"
> >>>> =========================================
> >>>> Traceback (most recent call last):
> >>>>    File "pyqtbug19.py", line 25, in ?
> >>>>      assert not destroyed, "object destroyed too early #2!"
> >>>> AssertionError: object destroyed too early #2!
> >>>>
> >>>>
> >>>> This happens with latest PyQt and SIP official releases (3.17.1 and
> >>>> 4.6). The line that seems to trigger the bug is the one marked with a
> >>>> comment.
> >>>
> >>> This behaves as I would expect - ie. it's a missing feature rather than
> >>> a bug.
> >>>
> >>> Although ql is the parent of viewport, Python doesn't know that and
> >>> there is no hidden extra reference to viewport to keep it alive when
> >>> collect() is run.
> >>
> >> But the problem here is that "o" is collected. o is a QObject whose
> >> lifetime should be transferred to C++ (given that it has a non-NULL
> >> parent, right?).
> >
> > But that parent is owned by Python. When viewpoint (Python) goes,
> > viewpoint (C++) goes, which takes o (C++), which takes o (Python).
>
> Are you saying that "viewport" is owned by Python?

I was but I was just in the process of replying to myself to say I was talking 
rubbish.

Ownership these days covers a couple of things that are actually applied 
separately. One thing is the responsibility to call the C++ dtor, the other 
thing is the associations maintained for the cyclic garbage collector.

When the result of viewport() is wrapped (assuming this is the first call) 
then C++ has the responsibility for calling the dtor. However as far as 
Python is concerned there is no gc association with any other Python object.

"Association" is a parent/child relationship and the parent has an (internally 
maintained) Python reference to the child.

When o is wrapped C++ is responsible for the dtor. A gc association is also 
made with viewport, ie. the viewport Python object has a reference to the o 
Python object. Obviously o is given a reference to viewport via the xxx 
attribute, so creating the cycle.

When o and viewport are del'ed and collect() is run the cycle is broken and 
the o and viewport Python objects garbage collected. The o and viewport C++ 
instances are left untouched.

If the new /Transfer/ function annotation were to be applied to viewport() 
then a gc association would be established between viewport and ql with the 
ql Python object having a reference to the viewport Python object. This would 
prevent viewport (and also o) from being garbage collected until ql was.

> (PS: what about a debugging function in sip which tells who owns who?
> sip.owner(foo) which returns a sip.voidptr() to the owner if it's C++, or
> None if it's owned by Python. It would make debugging of situations like
> this a little easier... using weakref/SIGNAL(destroyed) can bring other
> bugs into the table and makes things more confusing)

Would you want to use the value? Or would something like sip.debug(foo) that 
displayed lots of stuff to stdout be better?

Phil


More information about the PyQt mailing list