Skip to content

Commit c7b4f66

Browse files
type_caster_generic: add set_foreign_holder method for subclasses to implement (#5862)
* type_caster_generic: add set_foreign_holder method for subclasses to implement * style: pre-commit fixes * Rename try_shared_from_this -> set_via_shared_from_this to avoid confusion against try_get_shared_from_this * Add comment explaining the limits of the test * CI * style: pre-commit fixes * Fixes from code review * style: pre-commit fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 9f75202 commit c7b4f66

File tree

10 files changed

+186
-16
lines changed

10 files changed

+186
-16
lines changed

.codespell-ignore-lines

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ template <op_id id, op_type ot, typename L = undefined_t, typename R = undefined
22
template <typename ThisT>
33
auto &this_ = static_cast<ThisT &>(*this);
44
if (load_impl<ThisT>(temp, false)) {
5+
return load_impl<ThisT>(src, false);
56
ssize_t nd = 0;
67
auto trivial = broadcast(buffers, nd, shape);
78
auto ndim = (size_t) nd;

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ set(PYBIND11_HEADERS
188188
include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h
189189
include/pybind11/detail/exception_translation.h
190190
include/pybind11/detail/function_record_pyobject.h
191+
include/pybind11/detail/holder_caster_foreign_helpers.h
191192
include/pybind11/detail/init.h
192193
include/pybind11/detail/internals.h
193194
include/pybind11/detail/native_enum_data.h

include/pybind11/cast.h

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include "detail/argument_vector.h"
1414
#include "detail/common.h"
1515
#include "detail/descr.h"
16+
#include "detail/holder_caster_foreign_helpers.h"
1617
#include "detail/native_enum_data.h"
1718
#include "detail/type_caster_base.h"
1819
#include "detail/typeid.h"
@@ -907,6 +908,10 @@ struct copyable_holder_caster : public type_caster_base<type> {
907908
}
908909
}
909910

911+
bool set_foreign_holder(handle src) {
912+
return holder_caster_foreign_helpers::set_foreign_holder(src, (type *) value, &holder);
913+
}
914+
910915
void load_value(value_and_holder &&v_h) {
911916
if (v_h.holder_constructed()) {
912917
value = v_h.value_ptr();
@@ -977,22 +982,22 @@ struct copyable_holder_caster<
977982
}
978983

979984
explicit operator std::shared_ptr<type> *() {
980-
if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) {
985+
if (sh_load_helper.was_populated) {
981986
pybind11_fail("Passing `std::shared_ptr<T> *` from Python to C++ is not supported "
982987
"(inherently unsafe).");
983988
}
984989
return std::addressof(shared_ptr_storage);
985990
}
986991

987992
explicit operator std::shared_ptr<type> &() {
988-
if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) {
993+
if (sh_load_helper.was_populated) {
989994
shared_ptr_storage = sh_load_helper.load_as_shared_ptr(typeinfo, value);
990995
}
991996
return shared_ptr_storage;
992997
}
993998

994999
std::weak_ptr<type> potentially_slicing_weak_ptr() {
995-
if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) {
1000+
if (sh_load_helper.was_populated) {
9961001
// Reusing shared_ptr code to minimize code complexity.
9971002
shared_ptr_storage
9981003
= sh_load_helper.load_as_shared_ptr(typeinfo,
@@ -1041,6 +1046,11 @@ struct copyable_holder_caster<
10411046
}
10421047
}
10431048

1049+
bool set_foreign_holder(handle src) {
1050+
return holder_caster_foreign_helpers::set_foreign_holder(
1051+
src, (type *) value, &shared_ptr_storage);
1052+
}
1053+
10441054
void load_value(value_and_holder &&v_h) {
10451055
if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) {
10461056
sh_load_helper.loaded_v_h = v_h;
@@ -1078,6 +1088,7 @@ struct copyable_holder_caster<
10781088
value = cast.second(sub_caster.value);
10791089
if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) {
10801090
sh_load_helper.loaded_v_h = sub_caster.sh_load_helper.loaded_v_h;
1091+
sh_load_helper.was_populated = true;
10811092
} else {
10821093
shared_ptr_storage
10831094
= std::shared_ptr<type>(sub_caster.shared_ptr_storage, (type *) value);
@@ -1224,6 +1235,12 @@ struct move_only_holder_caster<
12241235
return false;
12251236
}
12261237

1238+
bool set_foreign_holder(handle) {
1239+
throw cast_error("Foreign instance cannot be converted to std::unique_ptr "
1240+
"because we don't know how to make it relinquish "
1241+
"ownership");
1242+
}
1243+
12271244
void load_value(value_and_holder &&v_h) {
12281245
if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) {
12291246
sh_load_helper.loaded_v_h = v_h;
@@ -1282,6 +1299,7 @@ struct move_only_holder_caster<
12821299
value = cast.second(sub_caster.value);
12831300
if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) {
12841301
sh_load_helper.loaded_v_h = sub_caster.sh_load_helper.loaded_v_h;
1302+
sh_load_helper.was_populated = true;
12851303
} else {
12861304
pybind11_fail("Expected to be UNREACHABLE: " __FILE__
12871305
":" PYBIND11_TOSTRING(__LINE__));
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
pybind11/detail/holder_caster_foreign_helpers.h: Logic to implement
3+
set_foreign_holder() in copyable_ and movable_holder_caster.
4+
5+
Copyright (c) 2025 Hudson River Trading LLC <opensource@hudson-trading.com>
6+
7+
All rights reserved. Use of this source code is governed by a
8+
BSD-style license that can be found in the LICENSE file.
9+
*/
10+
11+
#pragma once
12+
13+
#include <pybind11/gil.h>
14+
15+
#include "common.h"
16+
17+
PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE)
18+
PYBIND11_NAMESPACE_BEGIN(detail)
19+
20+
struct holder_caster_foreign_helpers {
21+
struct py_deleter {
22+
void operator()(const void *) const noexcept {
23+
// Don't run the deleter if the interpreter has been shut down
24+
if (Py_IsInitialized() == 0) {
25+
return;
26+
}
27+
gil_scoped_acquire guard;
28+
Py_DECREF(o);
29+
}
30+
31+
PyObject *o;
32+
};
33+
34+
template <typename type>
35+
static auto set_via_shared_from_this(type *value, std::shared_ptr<type> *holder_out)
36+
-> decltype(value->shared_from_this(), bool()) {
37+
// object derives from enable_shared_from_this;
38+
// try to reuse an existing shared_ptr if one is known
39+
if (auto existing = try_get_shared_from_this(value)) {
40+
*holder_out = std::static_pointer_cast<type>(existing);
41+
return true;
42+
}
43+
return false;
44+
}
45+
46+
template <typename type>
47+
static bool set_via_shared_from_this(void *, std::shared_ptr<type> *) {
48+
return false;
49+
}
50+
51+
template <typename type>
52+
static bool set_foreign_holder(handle src, type *value, std::shared_ptr<type> *holder_out) {
53+
// We only support using std::shared_ptr<T> for foreign T, and
54+
// it's done by creating a new shared_ptr control block that
55+
// owns a reference to the original Python object.
56+
if (value == nullptr) {
57+
*holder_out = {};
58+
return true;
59+
}
60+
if (set_via_shared_from_this(value, holder_out)) {
61+
return true;
62+
}
63+
*holder_out = std::shared_ptr<type>(value, py_deleter{src.inc_ref().ptr()});
64+
return true;
65+
}
66+
67+
template <typename type>
68+
static bool
69+
set_foreign_holder(handle src, const type *value, std::shared_ptr<const type> *holder_out) {
70+
std::shared_ptr<type> holder_mut;
71+
if (set_foreign_holder(src, const_cast<type *>(value), &holder_mut)) {
72+
*holder_out = holder_mut;
73+
return true;
74+
}
75+
return false;
76+
}
77+
78+
template <typename type>
79+
static bool set_foreign_holder(handle, type *, ...) {
80+
throw cast_error("Unable to cast foreign type to held instance -- "
81+
"only std::shared_ptr<T> is supported in this case");
82+
}
83+
};
84+
85+
PYBIND11_NAMESPACE_END(detail)
86+
PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE)

include/pybind11/detail/type_caster_base.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,7 @@ class type_caster_generic {
10691069
return false;
10701070
}
10711071
void check_holder_compat() {}
1072+
bool set_foreign_holder(handle) { return true; }
10721073

10731074
PYBIND11_NOINLINE static void *local_load(PyObject *src, const type_info *ti) {
10741075
auto caster = type_caster_generic(ti);
@@ -1107,14 +1108,14 @@ class type_caster_generic {
11071108
// logic (without having to resort to virtual inheritance).
11081109
template <typename ThisT>
11091110
PYBIND11_NOINLINE bool load_impl(handle src, bool convert) {
1111+
auto &this_ = static_cast<ThisT &>(*this);
11101112
if (!src) {
11111113
return false;
11121114
}
11131115
if (!typeinfo) {
1114-
return try_load_foreign_module_local(src);
1116+
return try_load_foreign_module_local(src) && this_.set_foreign_holder(src);
11151117
}
11161118

1117-
auto &this_ = static_cast<ThisT &>(*this);
11181119
this_.check_holder_compat();
11191120

11201121
PyTypeObject *srctype = Py_TYPE(src.ptr());
@@ -1180,13 +1181,13 @@ class type_caster_generic {
11801181
if (typeinfo->module_local) {
11811182
if (auto *gtype = get_global_type_info(*typeinfo->cpptype)) {
11821183
typeinfo = gtype;
1183-
return load(src, false);
1184+
return load_impl<ThisT>(src, false);
11841185
}
11851186
}
11861187

11871188
// Global typeinfo has precedence over foreign module_local
11881189
if (try_load_foreign_module_local(src)) {
1189-
return true;
1190+
return this_.set_foreign_holder(src);
11901191
}
11911192

11921193
// Custom converters didn't take None, now we convert None to nullptr.
@@ -1200,7 +1201,7 @@ class type_caster_generic {
12001201
}
12011202

12021203
if (convert && cpptype && this_.try_cpp_conduit(src)) {
1203-
return true;
1204+
return this_.set_foreign_holder(src);
12041205
}
12051206

12061207
return false;

tests/extra_python_package/test_files.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"include/pybind11/detail/descr.h",
8484
"include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h",
8585
"include/pybind11/detail/function_record_pyobject.h",
86+
"include/pybind11/detail/holder_caster_foreign_helpers.h",
8687
"include/pybind11/detail/init.h",
8788
"include/pybind11/detail/internals.h",
8889
"include/pybind11/detail/native_enum_data.h",

tests/local_bindings.h

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ using MixedLocalGlobal = LocalBase<4>;
2525
using MixedGlobalLocal = LocalBase<5>;
2626

2727
/// Registered with py::module_local only in the secondary module:
28-
using ExternalType1 = LocalBase<6>;
29-
using ExternalType2 = LocalBase<7>;
28+
using ExternalType1 = LocalBase<6>; // default holder
29+
using ExternalType2 = LocalBase<7>; // held by std::shared_ptr
30+
using ExternalType3 = LocalBase<8>; // held by smart_holder
3031

3132
using LocalVec = std::vector<LocalType>;
3233
using LocalVec2 = std::vector<NonLocal2>;
@@ -65,11 +66,11 @@ PYBIND11_MAKE_OPAQUE(NonLocalMap)
6566
PYBIND11_MAKE_OPAQUE(NonLocalMap2)
6667

6768
// Simple bindings (used with the above):
68-
template <typename T, int Adjust = 0, typename... Args>
69-
py::class_<T> bind_local(Args &&...args) {
70-
return py::class_<T>(std::forward<Args>(args)...).def(py::init<int>()).def("get", [](T &i) {
71-
return i.i + Adjust;
72-
});
69+
template <typename T, int Adjust = 0, typename Holder = std::unique_ptr<T>, typename... Args>
70+
py::class_<T, Holder> bind_local(Args &&...args) {
71+
return py::class_<T, Holder>(std::forward<Args>(args)...)
72+
.def(py::init<int>())
73+
.def("get", [](T &i) { return i.i + Adjust; });
7374
}
7475

7576
// Simulate a foreign library base class (to match the example in the docs):

tests/pybind11_cross_module_tests.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m, py::mod_gil_not_used()) {
2626

2727
// test_load_external
2828
bind_local<ExternalType1>(m, "ExternalType1", py::module_local());
29-
bind_local<ExternalType2>(m, "ExternalType2", py::module_local());
29+
bind_local<ExternalType2, 0, std::shared_ptr<ExternalType2>>(
30+
m, "ExternalType2", py::module_local());
31+
bind_local<ExternalType3, 0, py::smart_holder>(m, "ExternalType3", py::module_local());
3032

3133
// test_exceptions.py
3234
py::register_local_exception<LocalSimpleException>(m, "LocalSimpleException");

tests/test_local_bindings.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,31 @@ TEST_SUBMODULE(local_bindings, m) {
2121
// test_load_external
2222
m.def("load_external1", [](ExternalType1 &e) { return e.i; });
2323
m.def("load_external2", [](ExternalType2 &e) { return e.i; });
24+
m.def("load_external3", [](ExternalType3 &e) { return e.i; });
25+
26+
struct SharedKeepAlive {
27+
std::shared_ptr<int> contents;
28+
int value() const { return contents ? *contents : -20251012; }
29+
long use_count() const { return contents.use_count(); }
30+
};
31+
py::class_<SharedKeepAlive>(m, "SharedKeepAlive")
32+
.def_property_readonly("value", &SharedKeepAlive::value)
33+
.def_property_readonly("use_count", &SharedKeepAlive::use_count);
34+
m.def("load_external2_shared", [](const std::shared_ptr<ExternalType2> &p) {
35+
return SharedKeepAlive{std::shared_ptr<int>(p, &p->i)};
36+
});
37+
m.def("load_external3_shared", [](const std::shared_ptr<ExternalType3> &p) {
38+
return SharedKeepAlive{std::shared_ptr<int>(p, &p->i)};
39+
});
40+
m.def("load_external1_unique", [](std::unique_ptr<ExternalType1> p) { return p->i; });
41+
m.def("load_external3_unique", [](std::unique_ptr<ExternalType3> p) { return p->i; });
42+
43+
// Aspects of set_foreign_holder that are not covered:
44+
// - loading a foreign instance into a custom holder should fail
45+
// - we're only covering the case where the local module doesn't know
46+
// about the type; the paths where it does (e.g., if both global and
47+
// foreign-module-local bindings exist for the same type) should work
48+
// the same way (they use the same code so they very likely do)
2449

2550
// test_local_bindings
2651
// Register a class with py::module_local:

tests/test_local_bindings.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from __future__ import annotations
22

3+
import sys
4+
from contextlib import suppress
5+
36
import pytest
47

58
from pybind11_tests import local_bindings as m
@@ -11,6 +14,7 @@ def test_load_external():
1114

1215
assert m.load_external1(cm.ExternalType1(11)) == 11
1316
assert m.load_external2(cm.ExternalType2(22)) == 22
17+
assert m.load_external3(cm.ExternalType3(33)) == 33
1418

1519
with pytest.raises(TypeError) as excinfo:
1620
assert m.load_external2(cm.ExternalType1(21)) == 21
@@ -20,6 +24,36 @@ def test_load_external():
2024
assert m.load_external1(cm.ExternalType2(12)) == 12
2125
assert "incompatible function arguments" in str(excinfo.value)
2226

27+
def test_shared(val, ctor, loader):
28+
obj = ctor(val)
29+
with suppress(AttributeError): # non-cpython VMs don't have getrefcount
30+
rc_before = sys.getrefcount(obj)
31+
wrapper = loader(obj)
32+
# wrapper holds a shared_ptr that keeps obj alive
33+
assert wrapper.use_count == 1
34+
assert wrapper.value == val
35+
with suppress(AttributeError):
36+
rc_after = sys.getrefcount(obj)
37+
assert rc_after > rc_before
38+
39+
test_shared(220, cm.ExternalType2, m.load_external2_shared)
40+
test_shared(330, cm.ExternalType3, m.load_external3_shared)
41+
42+
with pytest.raises(TypeError, match="incompatible function arguments"):
43+
test_shared(320, cm.ExternalType2, m.load_external3_shared)
44+
with pytest.raises(TypeError, match="incompatible function arguments"):
45+
test_shared(230, cm.ExternalType3, m.load_external2_shared)
46+
47+
with pytest.raises(
48+
RuntimeError, match="Foreign instance cannot be converted to std::unique_ptr"
49+
):
50+
m.load_external1_unique(cm.ExternalType1(2200))
51+
52+
with pytest.raises(
53+
RuntimeError, match="Foreign instance cannot be converted to std::unique_ptr"
54+
):
55+
m.load_external3_unique(cm.ExternalType3(3300))
56+
2357

2458
def test_local_bindings():
2559
"""Tests that duplicate `py::module_local` class bindings work across modules"""

0 commit comments

Comments
 (0)