Skip to content

Commit af2dda3

Browse files
committed
Add a life support system for type_caster temporaries
1 parent 6b442ff commit af2dda3

File tree

5 files changed

+97
-7
lines changed

5 files changed

+97
-7
lines changed

include/pybind11/cast.h

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,53 @@ PYBIND11_NOINLINE inline internals &get_internals() {
111111
return *internals_ptr;
112112
}
113113

114+
/// A life support system for temporary objects created by `type_caster::load()`.
115+
/// Adding a patient will keep it alive up until the enclosing function returns.
116+
class loader_life_support {
117+
public:
118+
/// A new patient frame is created when a function is entered
119+
loader_life_support() {
120+
get_internals().loader_patient_stack.push_back(nullptr);
121+
}
122+
123+
/// ... and destroyed after it returns
124+
~loader_life_support() {
125+
auto &stack = get_internals().loader_patient_stack;
126+
if (stack.empty())
127+
pybind11_fail("loader_life_support: internal error");
128+
129+
auto ptr = stack.back();
130+
stack.pop_back();
131+
Py_CLEAR(ptr);
132+
133+
// A heuristic to reduce the stack's capacity (e.g. after long recursive calls)
134+
if (stack.capacity() > 16 && stack.size() != 0 && stack.capacity() / stack.size() > 2)
135+
stack.shrink_to_fit();
136+
}
137+
138+
/// This can only be used inside a pybind11-bound function, either by `argument_loader`
139+
/// at argument preparation time or by `py::cast()` at execution time.
140+
PYBIND11_NOINLINE static void add_patient(handle h) {
141+
auto &stack = get_internals().loader_patient_stack;
142+
if (stack.empty())
143+
throw cast_error("When called outside a bound function, py::cast() cannot "
144+
"do Python -> C++ conversions which require the creation "
145+
"of temporary values");
146+
147+
auto &list_ptr = stack.back();
148+
if (list_ptr == nullptr) {
149+
list_ptr = PyList_New(1);
150+
if (!list_ptr)
151+
pybind11_fail("loader_life_support: error allocating list");
152+
PyList_SET_ITEM(list_ptr, 0, h.inc_ref().ptr());
153+
} else {
154+
auto result = PyList_Append(list_ptr, h.ptr());
155+
if (result == -1)
156+
pybind11_fail("loader_life_support: error adding patient");
157+
}
158+
}
159+
};
160+
114161
// Gets the cache entry for the given type, creating it if necessary. The return value is the pair
115162
// returned by emplace, i.e. an iterator for the entry and a bool set to `true` if the entry was
116163
// just created.
@@ -643,9 +690,11 @@ class type_caster_generic {
643690
// Perform an implicit conversion
644691
if (convert) {
645692
for (auto &converter : typeinfo->implicit_conversions) {
646-
temp = reinterpret_steal<object>(converter(src.ptr(), typeinfo->type));
647-
if (load_impl<ThisT>(temp, false))
693+
auto temp = reinterpret_steal<object>(converter(src.ptr(), typeinfo->type));
694+
if (load_impl<ThisT>(temp, false)) {
695+
loader_life_support::add_patient(temp);
648696
return true;
697+
}
649698
}
650699
if (this_.try_direct_conversions(src))
651700
return true;
@@ -674,7 +723,6 @@ class type_caster_generic {
674723

675724
const type_info *typeinfo = nullptr;
676725
void *value = nullptr;
677-
object temp;
678726
};
679727

680728
/**
@@ -1064,7 +1112,7 @@ template <typename StringType, bool IsView = false> struct string_caster {
10641112

10651113
// If we're loading a string_view we need to keep the encoded Python object alive:
10661114
if (IsView)
1067-
view_into = std::move(utfNbytes);
1115+
loader_life_support::add_patient(utfNbytes);
10681116

10691117
return true;
10701118
}
@@ -1080,8 +1128,6 @@ template <typename StringType, bool IsView = false> struct string_caster {
10801128
PYBIND11_TYPE_CASTER(StringType, _(PYBIND11_STRING_NAME));
10811129

10821130
private:
1083-
object view_into;
1084-
10851131
static handle decode_utfN(const char *buffer, ssize_t nbytes) {
10861132
#if !defined(PYPY_VERSION)
10871133
return
@@ -1336,7 +1382,6 @@ struct copyable_holder_caster : public type_caster_base<type> {
13361382
using base::cast;
13371383
using base::typeinfo;
13381384
using base::value;
1339-
using base::temp;
13401385

13411386
bool load(handle src, bool convert) {
13421387
return base::template load_impl<copyable_holder_caster<type, holder_type>>(src, convert);

include/pybind11/common.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ struct internals {
474474
std::unordered_map<const PyObject *, std::vector<PyObject *>> patients;
475475
std::forward_list<void (*) (std::exception_ptr)> registered_exception_translators;
476476
std::unordered_map<std::string, void *> shared_data; // Custom data to be shared across extensions
477+
std::vector<PyObject *> loader_patient_stack; // Used by `loader_life_support`
477478
PyTypeObject *static_property_type;
478479
PyTypeObject *default_metaclass;
479480
PyObject *instance_base;

include/pybind11/pybind11.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ class cpp_function : public function {
573573

574574
// 6. Call the function.
575575
try {
576+
loader_life_support guard{};
576577
result = func.impl(call);
577578
} catch (reference_cast_error &) {
578579
result = PYBIND11_TRY_NEXT_OVERLOAD;
@@ -601,6 +602,7 @@ class cpp_function : public function {
601602
// The no-conversion pass finished without success, try again with conversion allowed
602603
for (auto &call : second_pass) {
603604
try {
605+
loader_life_support guard{};
604606
result = call.func.impl(call);
605607
} catch (reference_cast_error &) {
606608
result = PYBIND11_TRY_NEXT_OVERLOAD;

tests/test_class.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,40 @@ TEST_SUBMODULE(class_, m) {
150150
py::class_<MyDerived, MyBase>(m, "MyDerived")
151151
.def_static("make", &MyDerived::make)
152152
.def_static("make2", &MyDerived::make);
153+
154+
// test_implicit_conversion_life_support
155+
struct ConvertibleFromUserType {
156+
int i;
157+
158+
ConvertibleFromUserType(UserType u) : i(u.value()) { }
159+
};
160+
161+
py::class_<ConvertibleFromUserType>(m, "AcceptsUserType")
162+
.def(py::init<UserType>());
163+
py::implicitly_convertible<UserType, ConvertibleFromUserType>();
164+
165+
m.def("implicitly_convert_argument", [](const ConvertibleFromUserType &r) { return r.i; });
166+
m.def("implicitly_convert_variable", [](py::object o) {
167+
// `o` is `UserType` and `r` is a reference to a temporary created by implicit
168+
// conversion. This is valid when called inside a bound function because the temp
169+
// object is attached to the same life support system as the arguments.
170+
const auto &r = o.cast<const ConvertibleFromUserType &>();
171+
return r.i;
172+
});
173+
m.add_object("implicitly_convert_variable_fail", [&] {
174+
auto f = [](PyObject *, PyObject *args) -> PyObject * {
175+
auto o = py::reinterpret_borrow<py::tuple>(args)[0];
176+
try { // It should fail here because there is no life support.
177+
o.cast<const ConvertibleFromUserType &>();
178+
} catch (const py::cast_error &e) {
179+
return py::str(e.what()).release().ptr();
180+
}
181+
return py::str().release().ptr();
182+
};
183+
184+
auto def = new PyMethodDef{"f", f, METH_VARARGS, nullptr};
185+
return py::reinterpret_steal<py::object>(PyCFunction_NewEx(def, nullptr, m.ptr()));
186+
}());
153187
}
154188

155189
template <int N> class BreaksBase {};

tests/test_class.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,11 @@ def test_override_static():
119119
assert isinstance(b, m.MyBase)
120120
assert isinstance(d1, m.MyDerived)
121121
assert isinstance(d2, m.MyDerived)
122+
123+
124+
def test_implicit_conversion_life_support():
125+
"""Ensure the lifetime of temporary objects created for implicit conversions"""
126+
assert m.implicitly_convert_argument(UserType(5)) == 5
127+
assert m.implicitly_convert_variable(UserType(5)) == 5
128+
129+
assert "outside a bound function" in m.implicitly_convert_variable_fail(UserType(5))

0 commit comments

Comments
 (0)