Skip to content

Commit da058a2

Browse files
[Smart holder] Support void pointer capsules (#3633)
* Make smart holder type casters support void pointer capsules. * Fix warnings * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix checks * Fix check failures under CentOS * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Remove unused regex module * Resolve comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Resolve comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix clangtidy * Resolve comments Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 152bb10 commit da058a2

File tree

3 files changed

+227
-1
lines changed

3 files changed

+227
-1
lines changed

include/pybind11/detail/smart_holder_type_casters.h

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ struct is_smart_holder_type<smart_holder> : std::true_type {};
3737
inline void register_instance(instance *self, void *valptr, const type_info *tinfo);
3838
inline bool deregister_instance(instance *self, void *valptr, const type_info *tinfo);
3939

40+
// Replace all occurrences of a character in string.
41+
inline void replace_all(std::string& str, const std::string& from, char to) {
42+
size_t pos = str.find(from);
43+
while (pos != std::string::npos) {
44+
str.replace(pos, from.length(), 1, to);
45+
pos = str.find(from, pos);
46+
}
47+
}
48+
4049
// The modified_type_caster_generic_load_impl could replace type_caster_generic::load_impl but not
4150
// vice versa. The main difference is that the original code only propagates a reference to the
4251
// held value, while the modified implementation propagates value_and_holder.
@@ -106,6 +115,28 @@ class modified_type_caster_generic_load_impl {
106115
return false;
107116
}
108117

118+
bool try_as_void_ptr_capsule(handle src) {
119+
std::string type_name = cpptype->name();
120+
detail::clean_type_id(type_name);
121+
122+
// Convert `a::b::c` to `a_b_c`
123+
replace_all(type_name, "::", '_');
124+
125+
std::string as_void_ptr_function_name("as_");
126+
as_void_ptr_function_name += type_name;
127+
if (hasattr(src, as_void_ptr_function_name.c_str())) {
128+
auto as_void_ptr_function = function(
129+
src.attr(as_void_ptr_function_name.c_str()));
130+
auto void_ptr_capsule = as_void_ptr_function();
131+
if (isinstance<capsule>(void_ptr_capsule)) {
132+
unowned_void_ptr_from_void_ptr_capsule = reinterpret_borrow<capsule>(
133+
void_ptr_capsule).get_pointer();
134+
return true;
135+
}
136+
}
137+
return false;
138+
}
139+
109140
PYBIND11_NOINLINE static void *local_load(PyObject *src, const type_info *ti) {
110141
std::unique_ptr<modified_type_caster_generic_load_impl> loader(
111142
new modified_type_caster_generic_load_impl(ti));
@@ -161,6 +192,9 @@ class modified_type_caster_generic_load_impl {
161192
template <typename ThisT>
162193
PYBIND11_NOINLINE bool load_impl(handle src, bool convert) {
163194
if (!src) return false;
195+
if (cpptype && try_as_void_ptr_capsule(src)) {
196+
return true;
197+
}
164198
if (!typeinfo) return try_load_foreign_module_local(src);
165199

166200
auto &this_ = static_cast<ThisT &>(*this);
@@ -250,6 +284,7 @@ class modified_type_caster_generic_load_impl {
250284
const type_info *typeinfo = nullptr;
251285
const std::type_info *cpptype = nullptr;
252286
void *unowned_void_ptr_from_direct_conversion = nullptr;
287+
void *unowned_void_ptr_from_void_ptr_capsule = nullptr;
253288
const std::type_info *loaded_v_h_cpptype = nullptr;
254289
void *(*implicit_cast)(void *) = nullptr;
255290
value_and_holder loaded_v_h;
@@ -366,7 +401,10 @@ struct smart_holder_type_caster_load {
366401
}
367402

368403
T *loaded_as_raw_ptr_unowned() const {
369-
void *void_ptr = load_impl.unowned_void_ptr_from_direct_conversion;
404+
void *void_ptr = load_impl.unowned_void_ptr_from_void_ptr_capsule;
405+
if (void_ptr == nullptr) {
406+
void_ptr = load_impl.unowned_void_ptr_from_direct_conversion;
407+
}
370408
if (void_ptr == nullptr) {
371409
if (have_holder()) {
372410
throw_if_uninitialized_or_disowned_holder();
@@ -387,6 +425,10 @@ struct smart_holder_type_caster_load {
387425
}
388426

389427
std::shared_ptr<T> loaded_as_shared_ptr() const {
428+
if (load_impl.unowned_void_ptr_from_void_ptr_capsule) {
429+
throw cast_error("Unowned pointer from a void pointer capsule cannot be converted to a"
430+
" std::shared_ptr.");
431+
}
390432
if (load_impl.unowned_void_ptr_from_direct_conversion != nullptr)
391433
throw cast_error("Unowned pointer from direct conversion cannot be converted to a"
392434
" std::shared_ptr.");
@@ -441,6 +483,10 @@ struct smart_holder_type_caster_load {
441483

442484
template <typename D>
443485
std::unique_ptr<T, D> loaded_as_unique_ptr(const char *context = "loaded_as_unique_ptr") {
486+
if (load_impl.unowned_void_ptr_from_void_ptr_capsule) {
487+
throw cast_error("Unowned pointer from a void pointer capsule cannot be converted to a"
488+
" std::unique_ptr.");
489+
}
444490
if (load_impl.unowned_void_ptr_from_direct_conversion != nullptr)
445491
throw cast_error("Unowned pointer from direct conversion cannot be converted to a"
446492
" std::unique_ptr.");
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#include "pybind11_tests.h"
2+
3+
#include <pybind11/smart_holder.h>
4+
5+
#include <memory>
6+
7+
namespace pybind11_tests {
8+
namespace class_sh_void_ptr_capsule {
9+
10+
// Without the helper we will run into a type_caster::load recursion.
11+
// This is because whenever the type_caster::load is called, it checks
12+
// whether the object defines an `as_` method that returns the void pointer
13+
// capsule. If yes, it calls the method. But in the following testcases, those
14+
// `as_` methods are defined with pybind11, which implicitly takes the object
15+
// itself as the first parameter. Therefore calling those methods causes loading
16+
// the object again, which causes infinite recursion.
17+
// This test is unusual in the sense that the void pointer capsules are meant to
18+
// be provided by objects wrapped with systems other than pybind11
19+
// (i.e. having to avoid the recursion is an artificial problem, not the norm).
20+
// Conveniently, the helper also serves to keep track of `capsule_generated`.
21+
struct HelperBase {
22+
HelperBase() = default;
23+
virtual ~HelperBase() = default;
24+
25+
bool capsule_generated = false;
26+
virtual int get() const { return 100; }
27+
};
28+
29+
struct Valid: public HelperBase {
30+
int get() const override { return 101; }
31+
32+
PyObject* as_pybind11_tests_class_sh_void_ptr_capsule_Valid() {
33+
void* vptr = dynamic_cast<void*>(this);
34+
capsule_generated = true;
35+
// We assume vptr out lives the capsule, so we use nullptr for the
36+
// destructor.
37+
return PyCapsule_New(
38+
vptr, "::pybind11_tests::class_sh_void_ptr_capsule::Valid",
39+
nullptr);
40+
}
41+
};
42+
43+
struct NoConversion: public HelperBase {
44+
int get() const override { return 102; }
45+
};
46+
47+
struct NoCapsuleReturned: public HelperBase {
48+
int get() const { return 103; }
49+
50+
PyObject* as_pybind11_tests_class_sh_void_ptr_capsule_NoCapsuleReturned() {
51+
capsule_generated = true;
52+
Py_XINCREF(Py_None);
53+
return Py_None;
54+
}
55+
};
56+
57+
struct AsAnotherObject: public HelperBase {
58+
int get() const override { return 104; }
59+
60+
PyObject* as_pybind11_tests_class_sh_void_ptr_capsule_Valid() {
61+
void* vptr = dynamic_cast<void*>(this);
62+
capsule_generated = true;
63+
// We assume vptr out lives the capsule, so we use nullptr for the
64+
// destructor.
65+
return PyCapsule_New(
66+
vptr, "::pybind11_tests::class_sh_void_ptr_capsule::Valid",
67+
nullptr);
68+
}
69+
};
70+
71+
int get_from_valid_capsule(const Valid* c) {
72+
return c->get();
73+
}
74+
75+
int get_from_shared_ptr_valid_capsule(std::shared_ptr<Valid> c) {
76+
return c->get();
77+
}
78+
79+
int get_from_unique_ptr_valid_capsule(std::unique_ptr<Valid> c) {
80+
return c->get();
81+
}
82+
83+
int get_from_no_conversion_capsule(const NoConversion* c) {
84+
return c->get();
85+
}
86+
87+
int get_from_no_capsule_returned(const NoCapsuleReturned* c) {
88+
return c->get();
89+
}
90+
91+
} // namespace class_sh_void_ptr_capsule
92+
} // namespace pybind11_tests
93+
94+
PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_void_ptr_capsule::HelperBase)
95+
PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_void_ptr_capsule::Valid)
96+
PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_void_ptr_capsule::NoConversion)
97+
PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_void_ptr_capsule::NoCapsuleReturned)
98+
PYBIND11_SMART_HOLDER_TYPE_CASTERS(pybind11_tests::class_sh_void_ptr_capsule::AsAnotherObject)
99+
100+
TEST_SUBMODULE(class_sh_void_ptr_capsule, m) {
101+
using namespace pybind11_tests::class_sh_void_ptr_capsule;
102+
103+
py::classh<HelperBase>(m, "HelperBase")
104+
.def(py::init<>())
105+
.def("get", &HelperBase::get)
106+
.def_readonly("capsule_generated", &HelperBase::capsule_generated);
107+
108+
py::classh<Valid, HelperBase>(m, "Valid")
109+
.def(py::init<>())
110+
.def("as_pybind11_tests_class_sh_void_ptr_capsule_Valid",
111+
[](HelperBase* self) {
112+
Valid *obj = dynamic_cast<Valid *>(self);
113+
assert(obj != nullptr);
114+
PyObject* capsule = obj->as_pybind11_tests_class_sh_void_ptr_capsule_Valid();
115+
return pybind11::reinterpret_steal<py::capsule>(capsule);
116+
});
117+
118+
py::classh<NoConversion, HelperBase>(m, "NoConversion")
119+
.def(py::init<>());
120+
121+
py::classh<NoCapsuleReturned, HelperBase>(m, "NoCapsuleReturned")
122+
.def(py::init<>())
123+
.def("as_pybind11_tests_class_sh_void_ptr_capsule_NoCapsuleReturned",
124+
[](HelperBase* self) {
125+
NoCapsuleReturned *obj = dynamic_cast<NoCapsuleReturned *>(self);
126+
assert(obj != nullptr);
127+
PyObject* capsule = obj->as_pybind11_tests_class_sh_void_ptr_capsule_NoCapsuleReturned();
128+
return pybind11::reinterpret_steal<py::capsule>(capsule);
129+
});
130+
131+
py::classh<AsAnotherObject, HelperBase>(m, "AsAnotherObject")
132+
.def(py::init<>())
133+
.def("as_pybind11_tests_class_sh_void_ptr_capsule_Valid",
134+
[](HelperBase* self) {
135+
AsAnotherObject *obj = dynamic_cast<AsAnotherObject *>(self);
136+
assert(obj != nullptr);
137+
PyObject* capsule = obj->as_pybind11_tests_class_sh_void_ptr_capsule_Valid();
138+
return pybind11::reinterpret_steal<py::capsule>(capsule);
139+
});
140+
141+
m.def("get_from_valid_capsule", &get_from_valid_capsule);
142+
m.def("get_from_shared_ptr_valid_capsule", &get_from_shared_ptr_valid_capsule);
143+
m.def("get_from_unique_ptr_valid_capsule", &get_from_unique_ptr_valid_capsule);
144+
m.def("get_from_no_conversion_capsule", &get_from_no_conversion_capsule);
145+
m.def("get_from_no_capsule_returned", &get_from_no_capsule_returned);
146+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# -*- coding: utf-8 -*-
2+
import pytest
3+
4+
from pybind11_tests import class_sh_void_ptr_capsule as m
5+
6+
7+
@pytest.mark.parametrize(
8+
"ctor, caller, expected, capsule_generated",
9+
[
10+
(m.Valid, m.get_from_valid_capsule, 101, True),
11+
(m.NoConversion, m.get_from_no_conversion_capsule, 102, False),
12+
(m.NoCapsuleReturned, m.get_from_no_capsule_returned, 103, True),
13+
(m.AsAnotherObject, m.get_from_valid_capsule, 104, True),
14+
],
15+
)
16+
def test_as_void_ptr_capsule(ctor, caller, expected, capsule_generated):
17+
obj = ctor()
18+
assert caller(obj) == expected
19+
assert obj.capsule_generated == capsule_generated
20+
21+
22+
@pytest.mark.parametrize(
23+
"ctor, caller, pointer_type, capsule_generated",
24+
[
25+
(m.AsAnotherObject, m.get_from_shared_ptr_valid_capsule, "shared_ptr", True),
26+
(m.AsAnotherObject, m.get_from_unique_ptr_valid_capsule, "unique_ptr", True),
27+
],
28+
)
29+
def test_as_void_ptr_capsule_unsupported(ctor, caller, pointer_type, capsule_generated):
30+
obj = ctor()
31+
with pytest.raises(RuntimeError) as excinfo:
32+
caller(obj)
33+
assert pointer_type in str(excinfo.value)
34+
assert obj.capsule_generated == capsule_generated

0 commit comments

Comments
 (0)