Skip to content

Commit ba3d14d

Browse files
committed
Merge branch 'master' into sh_merge_master
2 parents 9651d3b + 90312a6 commit ba3d14d

File tree

8 files changed

+336
-2
lines changed

8 files changed

+336
-2
lines changed

CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ set(PYBIND11_HEADERS
146146
include/pybind11/stl.h
147147
include/pybind11/stl_bind.h
148148
include/pybind11/stl/filesystem.h
149-
include/pybind11/trampoline_self_life_support.h)
149+
include/pybind11/trampoline_self_life_support.h
150+
include/pybind11/type_caster_pyobject_ptr.h)
150151

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

include/pybind11/cast.h

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,11 @@ make_caster<T> load_type(const handle &handle) {
10831083
PYBIND11_NAMESPACE_END(detail)
10841084

10851085
// pytype -> C++ type
1086-
template <typename T, detail::enable_if_t<!detail::is_pyobject<T>::value, int> = 0>
1086+
template <typename T,
1087+
detail::enable_if_t<!detail::is_pyobject<T>::value
1088+
&& !detail::is_same_ignoring_cvref<T, PyObject *>::value,
1089+
int>
1090+
= 0>
10871091
T cast(const handle &handle) {
10881092
using namespace detail;
10891093
static_assert(!cast_is_temporary_value_reference<T>::value,
@@ -1097,6 +1101,34 @@ T cast(const handle &handle) {
10971101
return T(reinterpret_borrow<object>(handle));
10981102
}
10991103

1104+
// Note that `cast<PyObject *>(obj)` increments the reference count of `obj`.
1105+
// This is necessary for the case that `obj` is a temporary, and could
1106+
// not possibly be different, given
1107+
// 1. the established convention that the passed `handle` is borrowed, and
1108+
// 2. we don't want to force all generic code using `cast<T>()` to special-case
1109+
// handling of `T` = `PyObject *` (to increment the reference count there).
1110+
// It is the responsibility of the caller to ensure that the reference count
1111+
// is decremented.
1112+
template <typename T,
1113+
typename Handle,
1114+
detail::enable_if_t<detail::is_same_ignoring_cvref<T, PyObject *>::value
1115+
&& detail::is_same_ignoring_cvref<Handle, handle>::value,
1116+
int>
1117+
= 0>
1118+
T cast(Handle &&handle) {
1119+
return handle.inc_ref().ptr();
1120+
}
1121+
// To optimize way an inc_ref/dec_ref cycle:
1122+
template <typename T,
1123+
typename Object,
1124+
detail::enable_if_t<detail::is_same_ignoring_cvref<T, PyObject *>::value
1125+
&& detail::is_same_ignoring_cvref<Object, object>::value,
1126+
int>
1127+
= 0>
1128+
T cast(Object &&obj) {
1129+
return obj.release().ptr();
1130+
}
1131+
11001132
// C++ type -> py::object
11011133
template <typename T, detail::enable_if_t<!detail::is_pyobject<T>::value, int> = 0>
11021134
object cast(T &&value,

include/pybind11/detail/common.h

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

688+
/// Example usage: is_same_ignoring_cvref<T, PyObject *>::value
689+
template <typename T, typename U>
690+
using is_same_ignoring_cvref = std::is_same<detail::remove_cvref_t<T>, U>;
691+
688692
/// Index sequences
689693
#if defined(PYBIND11_CPP14)
690694
using std::index_sequence;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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("object"); // See discussion under PR #4601.
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+
return src;
37+
}
38+
if (policy == return_value_policy::reference
39+
|| policy == return_value_policy::automatic_reference) {
40+
return handle(src).inc_ref();
41+
}
42+
pybind11_fail("type_caster<PyObject>::cast(): unsupported return_value_policy: "
43+
+ std::to_string(static_cast<int>(policy)));
44+
}
45+
46+
bool load(handle src, bool) {
47+
value = reinterpret_borrow<object>(src);
48+
return true;
49+
}
50+
51+
template <typename T>
52+
using cast_op_type = PyObject *;
53+
54+
explicit operator PyObject *() { return value.ptr(); }
55+
56+
private:
57+
object value;
58+
};
59+
60+
PYBIND11_NAMESPACE_END(detail)
61+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ set(PYBIND11_TEST_FILES
177177
test_thread
178178
test_type_caster_odr_guard_1
179179
test_type_caster_odr_guard_2
180+
test_type_caster_pyobject_ptr
180181
test_union
181182
test_unnamed_namespace_a
182183
test_unnamed_namespace_b

tests/extra_python_package/test_files.py

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

5051
detail_headers = {
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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_handle_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+
m.def("cast_object_to_pyobject_ptr", [](py::object obj) {
38+
py::handle hdl = obj;
39+
auto rc1 = hdl.ref_count();
40+
auto *ptr = py::cast<PyObject *>(std::move(obj));
41+
auto rc2 = hdl.ref_count();
42+
if (rc2 != rc1) {
43+
return -1;
44+
}
45+
return 300 - py::reinterpret_steal<py::object>(ptr).attr("value").cast<int>();
46+
});
47+
m.def("cast_list_to_pyobject_ptr", [](py::list lst) {
48+
// This is to cover types implicitly convertible to object.
49+
py::handle hdl = lst;
50+
auto rc1 = hdl.ref_count();
51+
auto *ptr = py::cast<PyObject *>(std::move(lst));
52+
auto rc2 = hdl.ref_count();
53+
if (rc2 != rc1) {
54+
return -1;
55+
}
56+
return 400 - static_cast<int>(py::len(py::reinterpret_steal<py::list>(ptr)));
57+
});
58+
59+
m.def(
60+
"return_pyobject_ptr",
61+
[]() { return PyLong_FromLongLong(2314L); },
62+
py::return_value_policy::take_ownership);
63+
m.def("pass_pyobject_ptr", [](PyObject *ptr) {
64+
return 200 - py::reinterpret_borrow<py::object>(ptr).attr("value").cast<int>();
65+
});
66+
67+
m.def("call_callback_with_object_return",
68+
[](const std::function<py::object(int)> &cb, int value) { return cb(value); });
69+
m.def(
70+
"call_callback_with_pyobject_ptr_return",
71+
[](const std::function<PyObject *(int)> &cb, int value) { return cb(value); },
72+
py::return_value_policy::take_ownership);
73+
m.def(
74+
"call_callback_with_pyobject_ptr_arg",
75+
[](const std::function<int(PyObject *)> &cb, py::handle obj) { return cb(obj.ptr()); },
76+
py::arg("cb"), // This triggers return_value_policy::automatic_reference
77+
py::arg("obj"));
78+
79+
m.def("cast_to_pyobject_ptr_nullptr", [](bool set_error) {
80+
if (set_error) {
81+
PyErr_SetString(PyExc_RuntimeError, "Reflective of healthy error handling.");
82+
}
83+
PyObject *ptr = nullptr;
84+
py::cast(ptr);
85+
});
86+
87+
m.def("cast_to_pyobject_ptr_non_nullptr_with_error_set", []() {
88+
PyErr_SetString(PyExc_RuntimeError, "Reflective of unhealthy error handling.");
89+
py::cast(Py_None);
90+
});
91+
92+
m.def("pass_list_pyobject_ptr", [](const std::vector<PyObject *> &vec_obj) {
93+
int acc = 0;
94+
for (const auto &ptr : vec_obj) {
95+
acc = acc * 1000 + py::reinterpret_borrow<py::object>(ptr).attr("value").cast<int>();
96+
}
97+
return acc;
98+
});
99+
100+
m.def("return_list_pyobject_ptr_take_ownership",
101+
make_vector_pyobject_ptr,
102+
// Ownership is transferred one-by-one when the vector is converted to a Python list.
103+
py::return_value_policy::take_ownership);
104+
105+
m.def("return_list_pyobject_ptr_reference",
106+
make_vector_pyobject_ptr,
107+
// Ownership is not transferred.
108+
py::return_value_policy::reference);
109+
110+
m.def("dec_ref_each_pyobject_ptr", [](const std::vector<PyObject *> &vec_obj) {
111+
std::size_t i = 0;
112+
for (; i < vec_obj.size(); i++) {
113+
py::handle h(vec_obj[i]);
114+
if (static_cast<std::size_t>(h.ref_count()) < 2) {
115+
break; // Something is badly wrong.
116+
}
117+
h.dec_ref();
118+
}
119+
return i;
120+
});
121+
122+
m.def("pass_pyobject_ptr_and_int", [](PyObject *, int) {});
123+
124+
#ifdef PYBIND11_NO_COMPILE_SECTION // Change to ifndef for manual testing.
125+
{
126+
PyObject *ptr = nullptr;
127+
(void) py::cast(*ptr);
128+
}
129+
#endif
130+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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_handle_to_pyobject_ptr():
17+
assert m.cast_handle_to_pyobject_ptr(ValueHolder(24)) == 76
18+
19+
20+
def test_cast_object_to_pyobject_ptr():
21+
assert m.cast_object_to_pyobject_ptr(ValueHolder(43)) == 257
22+
23+
24+
def test_cast_list_to_pyobject_ptr():
25+
assert m.cast_list_to_pyobject_ptr([1, 2, 3, 4, 5]) == 395
26+
27+
28+
def test_return_pyobject_ptr():
29+
assert m.return_pyobject_ptr() == 2314
30+
31+
32+
def test_pass_pyobject_ptr():
33+
assert m.pass_pyobject_ptr(ValueHolder(82)) == 118
34+
35+
36+
@pytest.mark.parametrize(
37+
"call_callback",
38+
[
39+
m.call_callback_with_object_return,
40+
m.call_callback_with_pyobject_ptr_return,
41+
],
42+
)
43+
def test_call_callback_with_object_return(call_callback):
44+
def cb(value):
45+
if value < 0:
46+
raise ValueError("Raised from cb")
47+
return ValueHolder(1000 - value)
48+
49+
assert call_callback(cb, 287).value == 713
50+
51+
with pytest.raises(ValueError, match="^Raised from cb$"):
52+
call_callback(cb, -1)
53+
54+
55+
def test_call_callback_with_pyobject_ptr_arg():
56+
def cb(obj):
57+
return 300 - obj.value
58+
59+
assert m.call_callback_with_pyobject_ptr_arg(cb, ValueHolder(39)) == 261
60+
61+
62+
@pytest.mark.parametrize("set_error", [True, False])
63+
def test_cast_to_python_nullptr(set_error):
64+
expected = {
65+
True: r"^Reflective of healthy error handling\.$",
66+
False: (
67+
r"^Internal error: pybind11::error_already_set called "
68+
r"while Python error indicator not set\.$"
69+
),
70+
}[set_error]
71+
with pytest.raises(RuntimeError, match=expected):
72+
m.cast_to_pyobject_ptr_nullptr(set_error)
73+
74+
75+
def test_cast_to_python_non_nullptr_with_error_set():
76+
with pytest.raises(SystemError) as excinfo:
77+
m.cast_to_pyobject_ptr_non_nullptr_with_error_set()
78+
assert str(excinfo.value) == "src != nullptr but PyErr_Occurred()"
79+
assert str(excinfo.value.__cause__) == "Reflective of unhealthy error handling."
80+
81+
82+
def test_pass_list_pyobject_ptr():
83+
acc = m.pass_list_pyobject_ptr([ValueHolder(842), ValueHolder(452)])
84+
assert acc == 842452
85+
86+
87+
def test_return_list_pyobject_ptr_take_ownership():
88+
vec_obj = m.return_list_pyobject_ptr_take_ownership(ValueHolder)
89+
assert [e.value for e in vec_obj] == [93, 186]
90+
91+
92+
def test_return_list_pyobject_ptr_reference():
93+
vec_obj = m.return_list_pyobject_ptr_reference(ValueHolder)
94+
assert [e.value for e in vec_obj] == [93, 186]
95+
# Commenting out the next `assert` will leak the Python references.
96+
# An easy way to see evidence of the leaks:
97+
# Insert `while True:` as the first line of this function and monitor the
98+
# process RES (Resident Memory Size) with the Unix top command.
99+
assert m.dec_ref_each_pyobject_ptr(vec_obj) == 2
100+
101+
102+
def test_type_caster_name_via_incompatible_function_arguments_type_error():
103+
with pytest.raises(TypeError, match=r"1\. \(arg0: object, arg1: int\) -> None"):
104+
m.pass_pyobject_ptr_and_int(ValueHolder(101), ValueHolder(202))

0 commit comments

Comments
 (0)