Skip to content

Commit f7a8136

Browse files
committed
Track patients with unordered_set rather than vector
The stored vector of pybind-registered-type patients can grow without bound if a function is called with the same patient multiple times. This commit changes the pybind internal patient storage to an `unordered_set` to avoid the issue. Fixes pybind#1251
1 parent add56cc commit f7a8136

File tree

4 files changed

+51
-7
lines changed

4 files changed

+51
-7
lines changed

include/pybind11/detail/class.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,8 @@ inline void add_patient(PyObject *nurse, PyObject *patient) {
290290
auto &internals = get_internals();
291291
auto instance = reinterpret_cast<detail::instance *>(nurse);
292292
instance->has_patients = true;
293-
Py_INCREF(patient);
294-
internals.patients[nurse].push_back(patient);
293+
auto it = internals.patients[nurse].insert(patient);
294+
if (it.second) Py_INCREF(patient);
295295
}
296296

297297
inline void clear_patients(PyObject *self) {
@@ -300,12 +300,12 @@ inline void clear_patients(PyObject *self) {
300300
auto pos = internals.patients.find(self);
301301
assert(pos != internals.patients.end());
302302
// Clearing the patients can cause more Python code to run, which
303-
// can invalidate the iterator. Extract the vector of patients
303+
// can invalidate the iterator. Extract the set of patients
304304
// from the unordered_map first.
305305
auto patients = std::move(pos->second);
306306
internals.patients.erase(pos);
307307
instance->has_patients = false;
308-
for (PyObject *&patient : patients)
308+
for (PyObject *patient : patients)
309309
Py_CLEAR(patient);
310310
}
311311

include/pybind11/detail/internals.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ struct internals {
7070
std::unordered_multimap<const void *, instance*> registered_instances; // void * -> instance*
7171
std::unordered_set<std::pair<const PyObject *, const char *>, overload_hash> inactive_overload_cache;
7272
type_map<std::vector<bool (*)(PyObject *, void *&)>> direct_conversions;
73-
std::unordered_map<const PyObject *, std::vector<PyObject *>> patients;
73+
std::unordered_map<const PyObject *, std::unordered_set<PyObject *>> patients;
7474
std::forward_list<void (*) (std::exception_ptr)> registered_exception_translators;
7575
std::unordered_map<std::string, void *> shared_data; // Custom data to be shared across extensions
7676
std::vector<PyObject *> loader_patient_stack; // Used by `loader_life_support`
@@ -111,7 +111,7 @@ struct type_info {
111111
};
112112

113113
/// Tracks the `internals` and `type_info` ABI version independent of the main library version
114-
#define PYBIND11_INTERNALS_VERSION 1
114+
#define PYBIND11_INTERNALS_VERSION 2
115115

116116
#if defined(WITH_THREAD)
117117
# define PYBIND11_INTERNALS_KIND ""

tests/test_call_policies.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
#include "pybind11_tests.h"
11+
#include "constructor_stats.h"
1112

1213
struct CustomGuard {
1314
static bool enabled;
@@ -61,6 +62,21 @@ TEST_SUBMODULE(call_policies, m) {
6162
.def("returnNullChildKeepAliveChild", &Parent::returnNullChild, py::keep_alive<1, 0>())
6263
.def("returnNullChildKeepAliveParent", &Parent::returnNullChild, py::keep_alive<0, 1>());
6364

65+
// test_keep_alive_single
66+
m.def("add_patient", [](py::object /*nurse*/, py::object /*patient*/) { }, py::keep_alive<1, 2>());
67+
m.def("get_patients", [](py::object nurse) {
68+
py::set patients;
69+
for (PyObject *p : pybind11::detail::get_internals().patients[nurse.ptr()])
70+
patients.add(py::reinterpret_borrow<py::object>(p));
71+
return patients;
72+
});
73+
m.def("refcount", [](py::handle h) {
74+
#ifdef PYPY_VERSION
75+
ConstructorStats::gc(); // PyPy doesn't update ref counts until GC occurs
76+
#endif
77+
return h.ref_count();
78+
});
79+
6480
#if !defined(PYPY_VERSION)
6581
// test_alive_gc
6682
class ParentGC : public Parent {

tests/test_call_policies.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
from pybind11_tests import call_policies as m
3-
from pybind11_tests import ConstructorStats
3+
from pybind11_tests import ConstructorStats, UserType
44

55

66
def test_keep_alive_argument(capture):
@@ -69,6 +69,34 @@ def test_keep_alive_return_value(capture):
6969
"""
7070

7171

72+
def test_keep_alive_single():
73+
"""Issue #1251 - patients are stored multiple times when given to the same nurse"""
74+
75+
nurse, p1, p2 = UserType(), UserType(), UserType()
76+
assert [m.refcount(nurse), m.refcount(p1), m.refcount(p2)] ==[3, 3, 3]
77+
m.add_patient(nurse, p1)
78+
assert m.get_patients(nurse) == {p1, }
79+
assert [m.refcount(nurse), m.refcount(p1), m.refcount(p2)] ==[3, 4, 3]
80+
m.add_patient(nurse, p1)
81+
assert m.get_patients(nurse) == {p1, }
82+
assert [m.refcount(nurse), m.refcount(p1), m.refcount(p2)] ==[3, 4, 3]
83+
m.add_patient(nurse, p1)
84+
assert m.get_patients(nurse) == {p1, }
85+
assert [m.refcount(nurse), m.refcount(p1), m.refcount(p2)] ==[3, 4, 3]
86+
m.add_patient(nurse, p2)
87+
assert m.get_patients(nurse) == {p1, p2}
88+
assert [m.refcount(nurse), m.refcount(p1), m.refcount(p2)] ==[3, 4, 4]
89+
m.add_patient(nurse, p2)
90+
assert m.get_patients(nurse) == {p1, p2}
91+
assert [m.refcount(nurse), m.refcount(p1), m.refcount(p2)] ==[3, 4, 4]
92+
m.add_patient(nurse, p2)
93+
m.add_patient(nurse, p1)
94+
assert m.get_patients(nurse) == {p1, p2}
95+
assert [m.refcount(nurse), m.refcount(p1), m.refcount(p2)] ==[3, 4, 4]
96+
del nurse
97+
assert [m.refcount(p1), m.refcount(p2)] == [3, 3]
98+
99+
72100
# https://bitbucket.org/pypy/pypy/issues/2447
73101
@pytest.unsupported_on_pypy
74102
def test_alive_gc(capture):

0 commit comments

Comments
 (0)