Skip to content

Commit 90312a6

Browse files
authored
Add type_caster<PyObject> (#4601)
* Add `type_caster<PyObject>` (tests are still incomplete). * Fix oversight (`const PyObject *`). * Ensure `type_caster<PyObject>` only works for `PyObject *` * Move `is_same_ignoring_cvref` into `detail` namespace. * Add test_cast_nullptr * Change is_same_ignoring_cvref from variable template to using. ``` test_type_caster_pyobject_ptr.cpp:8:23: error: variable templates only available with ‘-std=c++14’ or ‘-std=gnu++14’ [-Werror] 8 | static constexpr bool is_same_ignoring_cvref = std::is_same<detail::remove_cvref_t<T>, U>::value; | ^~~~~~~~~~~~~~~~~~~~~~ ``` * Remove `return_value_policy::reference_internal` `keep_alive` feature (because of doubts about it actually being useful). * Add missing test, fix bug (missing `throw error_already_set();`), various cosmetic changes. * Move `type_caster<PyObject>` from test to new include (pybind11/type_caster_pyobject_ptr.h) * Add new header file to CMakeLists.txt and tests/extra_python_package/test_files.py * Backport changes from google/pybind11clif#30021 to #4601 * 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. * Change `type_caster<PyObject>::name` to `object`, as suggested by @Skylion007 * Expand comment for the new `T cast(const handle &handle)` [`T` = `PyObject *`] * Add `T cast(object &&obj)` overload as suggested by @Skylion007 The original suggestion leads to `error: call to 'cast' is ambiguous` (full error message below), therefore SFINAE guarding is needed. ``` clang++ -o pybind11/tests/test_type_caster_pyobject_ptr.os -c -std=c++17 -fPIC -fvisibility=hidden -O0 -g -Wall -Wextra -Wconversion -Wcast-qual -Wdeprecated -Wundef -Wnon-virtual-dtor -Wunused-result -Werror -isystem /usr/include/python3.10 -isystem /usr/include/eigen3 -DPYBIND11_STRICT_ASSERTS_CLASS_HOLDER_VS_TYPE_CASTER_MIX -DPYBIND11_ENABLE_TYPE_CASTER_ODR_GUARD_IF_AVAILABLE -DPYBIND11_TEST_BOOST -Ipybind11/include -I/usr/local/google/home/rwgk/forked/pybind11/include -I/usr/local/google/home/rwgk/clone/pybind11/include /usr/local/google/home/rwgk/forked/pybind11/tests/test_type_caster_pyobject_ptr.cpp In file included from /usr/local/google/home/rwgk/forked/pybind11/tests/test_type_caster_pyobject_ptr.cpp:1: In file included from /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/functional.h:12: In file included from /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/pybind11.h:13: In file included from /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/detail/class.h:12: In file included from /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/attr.h:14: /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/cast.h:1165:12: error: call to 'cast' is ambiguous return pybind11::cast<T>(std::move(*this)); ^~~~~~~~~~~~~~~~~ /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/functional.h:109:70: note: in instantiation of function template specialization 'pybind11::object::cast<_object *>' requested here return hfunc.f(std::forward<Args>(args)...).template cast<Return>(); ^ /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/functional.h:103:16: note: in instantiation of member function 'pybind11::detail::type_caster<std::function<_object *(int)>>::load(pybind11::handle, bool)::func_wrapper::operator()' requested here struct func_wrapper { ^ /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/cast.h:1456:47: note: in instantiation of member function 'pybind11::detail::type_caster<std::function<_object *(int)>>::load' requested here if ((... || !std::get<Is>(argcasters).load(call.args[Is], call.args_convert[Is]))) { ^ /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/cast.h:1434:50: note: in instantiation of function template specialization 'pybind11::detail::argument_loader<const std::function<_object *(int)> &, int>::load_impl_sequence<0UL, 1UL>' requested here bool load_args(function_call &call) { return load_impl_sequence(call, indices{}); } ^ /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/pybind11.h:227:33: note: in instantiation of member function 'pybind11::detail::argument_loader<const std::function<_object *(int)> &, int>::load_args' requested here if (!args_converter.load_args(call)) { ^ /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/pybind11.h:101:9: note: in instantiation of function template specialization 'pybind11::cpp_function::initialize<(lambda at /usr/local/google/home/rwgk/forked/pybind11/tests/test_type_caster_pyobject_ptr.cpp:50:9), _object *, const std::function<_object *(int)> &, int, pybind11::name, pybind11::scope, pybind11::sibling, pybind11::return_value_policy>' requested here initialize( ^ /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/pybind11.h:1163:22: note: in instantiation of function template specialization 'pybind11::cpp_function::cpp_function<(lambda at /usr/local/google/home/rwgk/forked/pybind11/tests/test_type_caster_pyobject_ptr.cpp:50:9), pybind11::name, pybind11::scope, pybind11::sibling, pybind11::return_value_policy, void>' requested here cpp_function func(std::forward<Func>(f), ^ /usr/local/google/home/rwgk/forked/pybind11/tests/test_type_caster_pyobject_ptr.cpp:48:7: note: in instantiation of function template specialization 'pybind11::module_::def<(lambda at /usr/local/google/home/rwgk/forked/pybind11/tests/test_type_caster_pyobject_ptr.cpp:50:9), pybind11::return_value_policy>' requested here m.def( ^ /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/cast.h:1077:3: note: candidate function [with T = _object *, $1 = 0] T cast(object &&obj) { ^ /usr/local/google/home/rwgk/forked/pybind11/include/pybind11/cast.h:1149:1: note: candidate function [with T = _object *] cast(object &&object) { ^ 1 error generated. ```
1 parent f701654 commit 90312a6

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
@@ -139,7 +139,8 @@ set(PYBIND11_HEADERS
139139
include/pybind11/pytypes.h
140140
include/pybind11/stl.h
141141
include/pybind11/stl_bind.h
142-
include/pybind11/stl/filesystem.h)
142+
include/pybind11/stl/filesystem.h
143+
include/pybind11/type_caster_pyobject_ptr.h)
143144

144145
# Compare with grep and warn if mismatched
145146
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
@@ -1041,7 +1041,11 @@ make_caster<T> load_type(const handle &handle) {
10411041
PYBIND11_NAMESPACE_END(detail)
10421042

10431043
// pytype -> C++ type
1044-
template <typename T, detail::enable_if_t<!detail::is_pyobject<T>::value, int> = 0>
1044+
template <typename T,
1045+
detail::enable_if_t<!detail::is_pyobject<T>::value
1046+
&& !detail::is_same_ignoring_cvref<T, PyObject *>::value,
1047+
int>
1048+
= 0>
10451049
T cast(const handle &handle) {
10461050
using namespace detail;
10471051
static_assert(!cast_is_temporary_value_reference<T>::value,
@@ -1055,6 +1059,34 @@ T cast(const handle &handle) {
10551059
return T(reinterpret_borrow<object>(handle));
10561060
}
10571061

1062+
// Note that `cast<PyObject *>(obj)` increments the reference count of `obj`.
1063+
// This is necessary for the case that `obj` is a temporary, and could
1064+
// not possibly be different, given
1065+
// 1. the established convention that the passed `handle` is borrowed, and
1066+
// 2. we don't want to force all generic code using `cast<T>()` to special-case
1067+
// handling of `T` = `PyObject *` (to increment the reference count there).
1068+
// It is the responsibility of the caller to ensure that the reference count
1069+
// is decremented.
1070+
template <typename T,
1071+
typename Handle,
1072+
detail::enable_if_t<detail::is_same_ignoring_cvref<T, PyObject *>::value
1073+
&& detail::is_same_ignoring_cvref<Handle, handle>::value,
1074+
int>
1075+
= 0>
1076+
T cast(Handle &&handle) {
1077+
return handle.inc_ref().ptr();
1078+
}
1079+
// To optimize way an inc_ref/dec_ref cycle:
1080+
template <typename T,
1081+
typename Object,
1082+
detail::enable_if_t<detail::is_same_ignoring_cvref<T, PyObject *>::value
1083+
&& detail::is_same_ignoring_cvref<Object, object>::value,
1084+
int>
1085+
= 0>
1086+
T cast(Object &&obj) {
1087+
return obj.release().ptr();
1088+
}
1089+
10581090
// C++ type -> py::object
10591091
template <typename T, detail::enable_if_t<!detail::is_pyobject<T>::value, int> = 0>
10601092
object cast(T &&value,

include/pybind11/detail/common.h

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

664+
/// Example usage: is_same_ignoring_cvref<T, PyObject *>::value
665+
template <typename T, typename U>
666+
using is_same_ignoring_cvref = std::is_same<detail::remove_cvref_t<T>, U>;
667+
664668
/// Index sequences
665669
#if defined(PYBIND11_CPP14)
666670
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
@@ -154,6 +154,7 @@ set(PYBIND11_TEST_FILES
154154
test_stl_binders
155155
test_tagbased_polymorphic
156156
test_thread
157+
test_type_caster_pyobject_ptr
157158
test_union
158159
test_unnamed_namespace_a
159160
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
@@ -43,6 +43,7 @@
4343
"include/pybind11/pytypes.h",
4444
"include/pybind11/stl.h",
4545
"include/pybind11/stl_bind.h",
46+
"include/pybind11/type_caster_pyobject_ptr.h",
4647
}
4748

4849
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)