Description
Issue description
The py::keep_alive weakref trick doesn't always work. In some cases (at least in Python 2.7, haven't tried 3), the weakref callback can run before the referenced object is cleared. This happens in the cyclic garbage collector: first all weakref callbacks are handled (handle_weakrefs
function), then the garbage is cleared. If the nurse is being destroyed on this path, then it can lead to the patient destructor running before the nurse's (leading to undefined behaviour if the destructor of the nurse references the patient). Note that the nurse doesn't itself have to be part of a reference cycle, it just has to be reachable from one. However, it needs to have Py_TPFLAGS_HAVE_GC
. In the example code I've ensured that by specifying py::dynamic_attr
, but I think when I originally encountered the problem it was set due to having a declared base.
I've had similar issues with Boost.Python before. I suspect the weakref approach is fundamentally untenable; it may be necessary to store a list of handles (with strong references) in the PyObject and decref them when the object is cleared.
Reproducible example code
keepalive.cpp:
#include <pybind11/pybind11.h>
namespace py = pybind11;
class B;
class A {
private:
B &b;
public:
A(B &b) : b(b) {}
~A() { py::print("In A::~A()"); }
};
class B {
public:
~B() { py::print("In B::~B()"); }
};
PYBIND11_PLUGIN(keepalive) {
py::module m("keepalive");
py::class_<A>(m, "A", py::dynamic_attr())
.def(py::init<B &>(), py::keep_alive<1, 2>());
py::class_<B>(m, "B").def(py::init<>());
return m.ptr();
}
test.py:
from keepalive import A, B
lst = [A(B())]
lst.append(lst)
del lst
Output is
In B::~B()
In A::~A()
Expected output is the other way around.