Skip to content

Commit 9d698f7

Browse files
bmerryjagerman
authored andcommitted
Hold strong references to keep_alive patients
This fixes #856. Instead of the weakref trick, the internals structure holds an unordered_map from PyObject* to a vector of references. To avoid the cost of the unordered_map lookup for objects that don't have any keep_alive patients, a flag is added to each instance to indicate whether there is anything to do.
1 parent 2196696 commit 9d698f7

File tree

5 files changed

+138
-21
lines changed

5 files changed

+138
-21
lines changed

include/pybind11/class_support.h

+26
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,29 @@ extern "C" inline int pybind11_object_init(PyObject *self, PyObject *, PyObject
280280
return -1;
281281
}
282282

283+
inline void add_patient(PyObject *nurse, PyObject *patient) {
284+
auto &internals = get_internals();
285+
auto instance = reinterpret_cast<detail::instance *>(nurse);
286+
instance->has_patients = true;
287+
Py_INCREF(patient);
288+
internals.patients[nurse].push_back(patient);
289+
}
290+
291+
inline void clear_patients(PyObject *self) {
292+
auto instance = reinterpret_cast<detail::instance *>(self);
293+
auto &internals = get_internals();
294+
auto pos = internals.patients.find(self);
295+
assert(pos != internals.patients.end());
296+
// Clearing the patients can cause more Python code to run, which
297+
// can invalidate the iterator. Extract the vector of patients
298+
// from the unordered_map first.
299+
auto patients = std::move(pos->second);
300+
internals.patients.erase(pos);
301+
instance->has_patients = false;
302+
for (PyObject *&patient : patients)
303+
Py_CLEAR(patient);
304+
}
305+
283306
/// Clears all internal data from the instance and removes it from registered instances in
284307
/// preparation for deallocation.
285308
inline void clear_instance(PyObject *self) {
@@ -304,6 +327,9 @@ inline void clear_instance(PyObject *self) {
304327
PyObject **dict_ptr = _PyObject_GetDictPtr(self);
305328
if (dict_ptr)
306329
Py_CLEAR(*dict_ptr);
330+
331+
if (instance->has_patients)
332+
clear_patients(self);
307333
}
308334

309335
/// Instance destructor function for all pybind11 types. It calls `type_info.dealloc`

include/pybind11/common.h

+3
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,8 @@ struct instance {
405405
bool simple_layout : 1;
406406
/// For simple layout, tracks whether the holder has been constructed
407407
bool simple_holder_constructed : 1;
408+
/// If true, get_internals().patients has an entry for this object
409+
bool has_patients : 1;
408410

409411
/// Initializes all of the above type/values/holders data
410412
void allocate_layout();
@@ -469,6 +471,7 @@ struct internals {
469471
std::unordered_multimap<const void *, instance*> registered_instances; // void * -> instance*
470472
std::unordered_set<std::pair<const PyObject *, const char *>, overload_hash> inactive_overload_cache;
471473
type_map<std::vector<bool (*)(PyObject *, void *&)>> direct_conversions;
474+
std::unordered_map<const PyObject *, std::vector<PyObject *>> patients;
472475
std::forward_list<void (*) (std::exception_ptr)> registered_exception_translators;
473476
std::unordered_map<std::string, void *> shared_data; // Custom data to be shared across extensions
474477
PyTypeObject *static_property_type;

include/pybind11/pybind11.h

+16-6
Original file line numberDiff line numberDiff line change
@@ -1328,20 +1328,30 @@ template <typename... Args> struct init_alias {
13281328

13291329

13301330
inline void keep_alive_impl(handle nurse, handle patient) {
1331-
/* Clever approach based on weak references taken from Boost.Python */
13321331
if (!nurse || !patient)
13331332
pybind11_fail("Could not activate keep_alive!");
13341333

13351334
if (patient.is_none() || nurse.is_none())
13361335
return; /* Nothing to keep alive or nothing to be kept alive by */
13371336

1338-
cpp_function disable_lifesupport(
1339-
[patient](handle weakref) { patient.dec_ref(); weakref.dec_ref(); });
1337+
auto tinfo = all_type_info(Py_TYPE(nurse.ptr()));
1338+
if (!tinfo.empty()) {
1339+
/* It's a pybind-registered type, so we can store the patient in the
1340+
* internal list. */
1341+
add_patient(nurse.ptr(), patient.ptr());
1342+
}
1343+
else {
1344+
/* Fall back to clever approach based on weak references taken from
1345+
* Boost.Python. This is not used for pybind-registered types because
1346+
* the objects can be destroyed out-of-order in a GC pass. */
1347+
cpp_function disable_lifesupport(
1348+
[patient](handle weakref) { patient.dec_ref(); weakref.dec_ref(); });
13401349

1341-
weakref wr(nurse, disable_lifesupport);
1350+
weakref wr(nurse, disable_lifesupport);
13421351

1343-
patient.inc_ref(); /* reference patient and leak the weak reference */
1344-
(void) wr.release();
1352+
patient.inc_ref(); /* reference patient and leak the weak reference */
1353+
(void) wr.release();
1354+
}
13451355
}
13461356

13471357
PYBIND11_NOINLINE inline void keep_alive_impl(size_t Nurse, size_t Patient, function_call &call, handle ret) {

tests/test_call_policies.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ class Parent {
2424
Child *returnNullChild() { return nullptr; }
2525
};
2626

27+
#if !defined(PYPY_VERSION)
28+
class ParentGC : public Parent {
29+
public:
30+
using Parent::Parent;
31+
};
32+
#endif
33+
2734
test_initializer keep_alive([](py::module &m) {
2835
py::class_<Parent>(m, "Parent")
2936
.def(py::init<>())
@@ -34,6 +41,11 @@ test_initializer keep_alive([](py::module &m) {
3441
.def("returnNullChildKeepAliveChild", &Parent::returnNullChild, py::keep_alive<1, 0>())
3542
.def("returnNullChildKeepAliveParent", &Parent::returnNullChild, py::keep_alive<0, 1>());
3643

44+
#if !defined(PYPY_VERSION)
45+
py::class_<ParentGC, Parent>(m, "ParentGC", py::dynamic_attr())
46+
.def(py::init<>());
47+
#endif
48+
3749
py::class_<Child>(m, "Child")
3850
.def(py::init<>());
3951
});

tests/test_call_policies.py

+81-15
Original file line numberDiff line numberDiff line change
@@ -2,98 +2,164 @@
22

33

44
def test_keep_alive_argument(capture):
5-
from pybind11_tests import Parent, Child
5+
from pybind11_tests import Parent, Child, ConstructorStats
66

7+
n_inst = ConstructorStats.detail_reg_inst()
78
with capture:
89
p = Parent()
910
assert capture == "Allocating parent."
1011
with capture:
1112
p.addChild(Child())
12-
pytest.gc_collect()
13+
assert ConstructorStats.detail_reg_inst() == n_inst + 1
1314
assert capture == """
1415
Allocating child.
1516
Releasing child.
1617
"""
1718
with capture:
1819
del p
19-
pytest.gc_collect()
20+
assert ConstructorStats.detail_reg_inst() == n_inst
2021
assert capture == "Releasing parent."
2122

2223
with capture:
2324
p = Parent()
2425
assert capture == "Allocating parent."
2526
with capture:
2627
p.addChildKeepAlive(Child())
27-
pytest.gc_collect()
28+
assert ConstructorStats.detail_reg_inst() == n_inst + 2
2829
assert capture == "Allocating child."
2930
with capture:
3031
del p
31-
pytest.gc_collect()
32+
assert ConstructorStats.detail_reg_inst() == n_inst
3233
assert capture == """
3334
Releasing parent.
3435
Releasing child.
3536
"""
3637

3738

3839
def test_keep_alive_return_value(capture):
39-
from pybind11_tests import Parent
40+
from pybind11_tests import Parent, ConstructorStats
4041

42+
n_inst = ConstructorStats.detail_reg_inst()
4143
with capture:
4244
p = Parent()
4345
assert capture == "Allocating parent."
4446
with capture:
4547
p.returnChild()
46-
pytest.gc_collect()
48+
assert ConstructorStats.detail_reg_inst() == n_inst + 1
4749
assert capture == """
4850
Allocating child.
4951
Releasing child.
5052
"""
5153
with capture:
5254
del p
53-
pytest.gc_collect()
55+
assert ConstructorStats.detail_reg_inst() == n_inst
5456
assert capture == "Releasing parent."
5557

5658
with capture:
5759
p = Parent()
5860
assert capture == "Allocating parent."
5961
with capture:
6062
p.returnChildKeepAlive()
61-
pytest.gc_collect()
63+
assert ConstructorStats.detail_reg_inst() == n_inst + 2
6264
assert capture == "Allocating child."
6365
with capture:
6466
del p
65-
pytest.gc_collect()
67+
assert ConstructorStats.detail_reg_inst() == n_inst
68+
assert capture == """
69+
Releasing parent.
70+
Releasing child.
71+
"""
72+
73+
74+
# https://bitbucket.org/pypy/pypy/issues/2447
75+
@pytest.unsupported_on_pypy
76+
def test_alive_gc(capture):
77+
from pybind11_tests import ParentGC, Child, ConstructorStats
78+
79+
n_inst = ConstructorStats.detail_reg_inst()
80+
p = ParentGC()
81+
p.addChildKeepAlive(Child())
82+
assert ConstructorStats.detail_reg_inst() == n_inst + 2
83+
lst = [p]
84+
lst.append(lst) # creates a circular reference
85+
with capture:
86+
del p, lst
87+
assert ConstructorStats.detail_reg_inst() == n_inst
88+
assert capture == """
89+
Releasing parent.
90+
Releasing child.
91+
"""
92+
93+
94+
def test_alive_gc_derived(capture):
95+
from pybind11_tests import Parent, Child, ConstructorStats
96+
97+
class Derived(Parent):
98+
pass
99+
100+
n_inst = ConstructorStats.detail_reg_inst()
101+
p = Derived()
102+
p.addChildKeepAlive(Child())
103+
assert ConstructorStats.detail_reg_inst() == n_inst + 2
104+
lst = [p]
105+
lst.append(lst) # creates a circular reference
106+
with capture:
107+
del p, lst
108+
assert ConstructorStats.detail_reg_inst() == n_inst
109+
assert capture == """
110+
Releasing parent.
111+
Releasing child.
112+
"""
113+
114+
115+
def test_alive_gc_multi_derived(capture):
116+
from pybind11_tests import Parent, Child, ConstructorStats
117+
118+
class Derived(Parent, Child):
119+
pass
120+
121+
n_inst = ConstructorStats.detail_reg_inst()
122+
p = Derived()
123+
p.addChildKeepAlive(Child())
124+
# +3 rather than +2 because Derived corresponds to two registered instances
125+
assert ConstructorStats.detail_reg_inst() == n_inst + 3
126+
lst = [p]
127+
lst.append(lst) # creates a circular reference
128+
with capture:
129+
del p, lst
130+
assert ConstructorStats.detail_reg_inst() == n_inst
66131
assert capture == """
67132
Releasing parent.
68133
Releasing child.
69134
"""
70135

71136

72137
def test_return_none(capture):
73-
from pybind11_tests import Parent
138+
from pybind11_tests import Parent, ConstructorStats
74139

140+
n_inst = ConstructorStats.detail_reg_inst()
75141
with capture:
76142
p = Parent()
77143
assert capture == "Allocating parent."
78144
with capture:
79145
p.returnNullChildKeepAliveChild()
80-
pytest.gc_collect()
146+
assert ConstructorStats.detail_reg_inst() == n_inst + 1
81147
assert capture == ""
82148
with capture:
83149
del p
84-
pytest.gc_collect()
150+
assert ConstructorStats.detail_reg_inst() == n_inst
85151
assert capture == "Releasing parent."
86152

87153
with capture:
88154
p = Parent()
89155
assert capture == "Allocating parent."
90156
with capture:
91157
p.returnNullChildKeepAliveParent()
92-
pytest.gc_collect()
158+
assert ConstructorStats.detail_reg_inst() == n_inst + 1
93159
assert capture == ""
94160
with capture:
95161
del p
96-
pytest.gc_collect()
162+
assert ConstructorStats.detail_reg_inst() == n_inst
97163
assert capture == "Releasing parent."
98164

99165

0 commit comments

Comments
 (0)