Skip to content

Commit ff158e9

Browse files
authored
Add type_caster<PyObject> (#30021)
* Snapshot of pybind/pybind11#4601 (squashed). * Add new header file to CMakeLists.txt and tests/extra_python_package/test_files.py * to-Pyton-cast: `return_value_policy::_clif_automatic` => take ownership * `handle.inc_ref()` in `T cast(const handle &handle)` specialization for `PyObject *` * Guard against accidental leaking by requiring `return_value_policy::reference` or `automatic_reference` * Fix oversight in test (to resolve a valgrind leak detection error) and add a related comment in cast.h. No production code changes. Make tests more sensitive by using `ValueHolder` instead of empty tuples and dicts. Manual leak checks with `while True:` & top command repeated for all tests. * Add tests for interop with stl.h `list_caster` (No production code changes.) * Bug fix in test. Minor comment enhancements.
1 parent d1b83fd commit ff158e9

File tree

8 files changed

+283
-2
lines changed

8 files changed

+283
-2
lines changed

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ set(PYBIND11_HEADERS
151151
include/pybind11/stl.h
152152
include/pybind11/stl_bind.h
153153
include/pybind11/stl/filesystem.h
154-
include/pybind11/trampoline_self_life_support.h)
154+
include/pybind11/trampoline_self_life_support.h
155+
include/pybind11/type_caster_pyobject_ptr.h)
155156

156157
# Compare with grep and warn if mismatched
157158
if(PYBIND11_MASTER_PROJECT AND NOT CMAKE_VERSION VERSION_LESS 3.12)

include/pybind11/cast.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1207,7 +1207,11 @@ make_caster<T> load_type(const handle &handle) {
12071207
PYBIND11_NAMESPACE_END(detail)
12081208

12091209
// pytype -> C++ type
1210-
template <typename T, detail::enable_if_t<!detail::is_pyobject<T>::value, int> = 0>
1210+
template <typename T,
1211+
detail::enable_if_t<!detail::is_pyobject<T>::value
1212+
&& !detail::is_same_ignoring_cvref<T, PyObject *>::value,
1213+
int>
1214+
= 0>
12111215
T cast(const handle &handle) {
12121216
using namespace detail;
12131217
constexpr bool is_enum_cast = type_uses_type_caster_enum_type<intrinsic_t<T>>::value;
@@ -1231,6 +1235,16 @@ T cast(const handle &handle) {
12311235
return T(reinterpret_borrow<object>(handle));
12321236
}
12331237

1238+
// Note that `cast<PyObject *>(obj)` increments the reference count of `obj`.
1239+
// This is necessary for the case that `obj` is a temporary.
1240+
// It is the responsibility of the caller to ensure that the reference count
1241+
// is decremented.
1242+
template <typename T,
1243+
detail::enable_if_t<detail::is_same_ignoring_cvref<T, PyObject *>::value, int> = 0>
1244+
T cast(const handle &handle) {
1245+
return handle.inc_ref().ptr();
1246+
}
1247+
12341248
// C++ type -> py::object
12351249
template <typename T, detail::enable_if_t<!detail::is_pyobject<T>::value, int> = 0>
12361250
object cast(T &&value,

include/pybind11/detail/common.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,10 @@ template <class T>
751751
using remove_cvref_t = typename remove_cvref<T>::type;
752752
#endif
753753

754+
/// Example usage: is_same_ignoring_cvref<T, PyObject *>::value
755+
template <typename T, typename U>
756+
using is_same_ignoring_cvref = std::is_same<detail::remove_cvref_t<T>, U>;
757+
754758
/// Index sequences
755759
#if defined(PYBIND11_CPP14)
756760
using std::index_sequence;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2023 The pybind Community.
2+
3+
#pragma once
4+
5+
#include "detail/common.h"
6+
#include "detail/descr.h"
7+
#include "cast.h"
8+
#include "pytypes.h"
9+
10+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
11+
PYBIND11_NAMESPACE_BEGIN(detail)
12+
13+
template <>
14+
class type_caster<PyObject> {
15+
public:
16+
static constexpr auto name = const_name("PyObject *");
17+
18+
// This overload is purely to guard against accidents.
19+
template <typename T,
20+
detail::enable_if_t<!is_same_ignoring_cvref<T, PyObject *>::value, int> = 0>
21+
static handle cast(T &&, return_value_policy, handle /*parent*/) {
22+
static_assert(is_same_ignoring_cvref<T, PyObject *>::value,
23+
"Invalid C++ type T for to-Python conversion (type_caster<PyObject>).");
24+
return nullptr; // Unreachable.
25+
}
26+
27+
static handle cast(PyObject *src, return_value_policy policy, handle /*parent*/) {
28+
if (src == nullptr) {
29+
throw error_already_set();
30+
}
31+
if (PyErr_Occurred()) {
32+
raise_from(PyExc_SystemError, "src != nullptr but PyErr_Occurred()");
33+
throw error_already_set();
34+
}
35+
if (policy == return_value_policy::take_ownership
36+
|| policy == return_value_policy::_clif_automatic) {
37+
return src;
38+
}
39+
if (policy == return_value_policy::reference
40+
|| policy == return_value_policy::automatic_reference) {
41+
return handle(src).inc_ref();
42+
}
43+
pybind11_fail("type_caster<PyObject>::cast(): unsupported return_value_policy: "
44+
+ std::to_string(static_cast<int>(policy)));
45+
}
46+
47+
bool load(handle src, bool) {
48+
value = reinterpret_borrow<object>(src);
49+
return true;
50+
}
51+
52+
template <typename T>
53+
using cast_op_type = PyObject *;
54+
55+
explicit operator PyObject *() { return value.ptr(); }
56+
57+
private:
58+
object value;
59+
};
60+
61+
PYBIND11_NAMESPACE_END(detail)
62+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ set(PYBIND11_TEST_FILES
180180
test_thread
181181
test_type_caster_odr_guard_1
182182
test_type_caster_odr_guard_2
183+
test_type_caster_pyobject_ptr
183184
test_type_caster_std_function_specializations
184185
test_union
185186
test_virtual_functions)

tests/extra_python_package/test_files.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"include/pybind11/stl.h",
4747
"include/pybind11/stl_bind.h",
4848
"include/pybind11/trampoline_self_life_support.h",
49+
"include/pybind11/type_caster_pyobject_ptr.h",
4950
}
5051

5152
detail_headers = {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#include <pybind11/functional.h>
2+
#include <pybind11/stl.h>
3+
#include <pybind11/type_caster_pyobject_ptr.h>
4+
5+
#include "pybind11_tests.h"
6+
7+
#include <cstddef>
8+
#include <vector>
9+
10+
namespace {
11+
12+
std::vector<PyObject *> make_vector_pyobject_ptr(const py::object &ValueHolder) {
13+
std::vector<PyObject *> vec_obj;
14+
for (int i = 1; i < 3; i++) {
15+
vec_obj.push_back(ValueHolder(i * 93).release().ptr());
16+
}
17+
// This vector now owns the refcounts.
18+
return vec_obj;
19+
}
20+
21+
} // namespace
22+
23+
TEST_SUBMODULE(type_caster_pyobject_ptr, m) {
24+
m.def("cast_from_pyobject_ptr", []() {
25+
PyObject *ptr = PyLong_FromLongLong(6758L);
26+
return py::cast(ptr, py::return_value_policy::take_ownership);
27+
});
28+
m.def("cast_to_pyobject_ptr", [](py::handle obj) {
29+
auto rc1 = obj.ref_count();
30+
auto *ptr = py::cast<PyObject *>(obj);
31+
auto rc2 = obj.ref_count();
32+
if (rc2 != rc1 + 1) {
33+
return -1;
34+
}
35+
return 100 - py::reinterpret_steal<py::object>(ptr).attr("value").cast<int>();
36+
});
37+
38+
m.def(
39+
"return_pyobject_ptr",
40+
[]() { return PyLong_FromLongLong(2314L); },
41+
py::return_value_policy::take_ownership);
42+
m.def("pass_pyobject_ptr", [](PyObject *ptr) {
43+
return 200 - py::reinterpret_borrow<py::object>(ptr).attr("value").cast<int>();
44+
});
45+
46+
m.def("call_callback_with_object_return",
47+
[](const std::function<py::object(int)> &cb, int value) { return cb(value); });
48+
m.def(
49+
"call_callback_with_pyobject_ptr_return",
50+
[](const std::function<PyObject *(int)> &cb, int value) { return cb(value); },
51+
py::return_value_policy::take_ownership);
52+
m.def(
53+
"call_callback_with_pyobject_ptr_arg",
54+
[](const std::function<int(PyObject *)> &cb, py::handle obj) { return cb(obj.ptr()); },
55+
py::arg("cb"), // This triggers return_value_policy::automatic_reference
56+
py::arg("obj"));
57+
58+
m.def("cast_to_pyobject_ptr_nullptr", [](bool set_error) {
59+
if (set_error) {
60+
PyErr_SetString(PyExc_RuntimeError, "Reflective of healthy error handling.");
61+
}
62+
PyObject *ptr = nullptr;
63+
py::cast(ptr);
64+
});
65+
66+
m.def("cast_to_pyobject_ptr_non_nullptr_with_error_set", []() {
67+
PyErr_SetString(PyExc_RuntimeError, "Reflective of unhealthy error handling.");
68+
py::cast(Py_None);
69+
});
70+
71+
m.def("pass_list_pyobject_ptr", [](const std::vector<PyObject *> &vec_obj) {
72+
int acc = 0;
73+
for (const auto &ptr : vec_obj) {
74+
acc = acc * 1000 + py::reinterpret_borrow<py::object>(ptr).attr("value").cast<int>();
75+
}
76+
return acc;
77+
});
78+
79+
m.def("return_list_pyobject_ptr_take_ownership",
80+
make_vector_pyobject_ptr,
81+
// Ownership is transferred one-by-one when the vector is converted to a Python list.
82+
py::return_value_policy::take_ownership);
83+
84+
m.def("return_list_pyobject_ptr_reference",
85+
make_vector_pyobject_ptr,
86+
// Ownership is not transferred.
87+
py::return_value_policy::reference);
88+
89+
m.def("dec_ref_each_pyobject_ptr", [](const std::vector<PyObject *> &vec_obj) {
90+
std::size_t i = 0;
91+
for (; i < vec_obj.size(); i++) {
92+
py::handle h(vec_obj[i]);
93+
if (static_cast<std::size_t>(h.ref_count()) < 2) {
94+
break; // Something is badly wrong.
95+
}
96+
h.dec_ref();
97+
}
98+
return i;
99+
});
100+
101+
#ifdef PYBIND11_NO_COMPILE_SECTION // Change to ifndef for manual testing.
102+
{
103+
PyObject *ptr = nullptr;
104+
(void) py::cast(*ptr);
105+
}
106+
#endif
107+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import pytest
2+
3+
from pybind11_tests import type_caster_pyobject_ptr as m
4+
5+
6+
# For use as a temporary user-defined object, to maximize sensitivity of the tests below.
7+
class ValueHolder:
8+
def __init__(self, value):
9+
self.value = value
10+
11+
12+
def test_cast_from_pyobject_ptr():
13+
assert m.cast_from_pyobject_ptr() == 6758
14+
15+
16+
def test_cast_to_pyobject_ptr():
17+
assert m.cast_to_pyobject_ptr(ValueHolder(24)) == 76
18+
19+
20+
def test_return_pyobject_ptr():
21+
assert m.return_pyobject_ptr() == 2314
22+
23+
24+
def test_pass_pyobject_ptr():
25+
assert m.pass_pyobject_ptr(ValueHolder(82)) == 118
26+
27+
28+
@pytest.mark.parametrize(
29+
"call_callback",
30+
[
31+
m.call_callback_with_object_return,
32+
m.call_callback_with_pyobject_ptr_return,
33+
],
34+
)
35+
def test_call_callback_with_object_return(call_callback):
36+
def cb(value):
37+
if value < 0:
38+
raise ValueError("Raised from cb")
39+
return ValueHolder(1000 - value)
40+
41+
assert call_callback(cb, 287).value == 713
42+
43+
with pytest.raises(ValueError, match="^Raised from cb$"):
44+
call_callback(cb, -1)
45+
46+
47+
def test_call_callback_with_pyobject_ptr_arg():
48+
def cb(obj):
49+
return 300 - obj.value
50+
51+
assert m.call_callback_with_pyobject_ptr_arg(cb, ValueHolder(39)) == 261
52+
53+
54+
@pytest.mark.parametrize("set_error", [True, False])
55+
def test_cast_to_python_nullptr(set_error):
56+
expected = {
57+
True: r"^Reflective of healthy error handling\.$",
58+
False: (
59+
r"^Internal error: pybind11::error_already_set called "
60+
r"while Python error indicator not set\.$"
61+
),
62+
}[set_error]
63+
with pytest.raises(RuntimeError, match=expected):
64+
m.cast_to_pyobject_ptr_nullptr(set_error)
65+
66+
67+
def test_cast_to_python_non_nullptr_with_error_set():
68+
with pytest.raises(SystemError) as excinfo:
69+
m.cast_to_pyobject_ptr_non_nullptr_with_error_set()
70+
assert str(excinfo.value) == "src != nullptr but PyErr_Occurred()"
71+
assert str(excinfo.value.__cause__) == "Reflective of unhealthy error handling."
72+
73+
74+
def test_pass_list_pyobject_ptr():
75+
acc = m.pass_list_pyobject_ptr([ValueHolder(842), ValueHolder(452)])
76+
assert acc == 842452
77+
78+
79+
def test_return_list_pyobject_ptr_take_ownership():
80+
vec_obj = m.return_list_pyobject_ptr_take_ownership(ValueHolder)
81+
assert [e.value for e in vec_obj] == [93, 186]
82+
83+
84+
def test_return_list_pyobject_ptr_reference():
85+
vec_obj = m.return_list_pyobject_ptr_reference(ValueHolder)
86+
assert [e.value for e in vec_obj] == [93, 186]
87+
# Commenting out the next `assert` will leak the Python references.
88+
# An easy way to see evidence of the leaks:
89+
# Insert `while True:` as the first line of this function and monitor the
90+
# process RES (Resident Memory Size) with the Unix top command.
91+
assert m.dec_ref_each_pyobject_ptr(vec_obj) == 2

0 commit comments

Comments
 (0)