From 9cca56fbedb69b218b97cefc3822a5e8f21eb5b7 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 17 May 2026 03:51:06 +0000 Subject: [PATCH 01/15] test: reproduce custom holder shared_ptr cast regression --- tests/test_smart_ptr.cpp | 52 ++++++++++++++++++++++++++++++++++++++++ tests/test_smart_ptr.py | 10 ++++++++ 2 files changed, 62 insertions(+) diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index 3617fa3e85..ba1da574a6 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -83,6 +83,26 @@ class const_only_shared_ptr { // for demonstration purpose only, this imitates smart pointer with a const-only pointer }; +template +class shared_ptr_as_custom_holder { + std::shared_ptr ptr_; + +public: + using element_type = T; + + shared_ptr_as_custom_holder() = default; + + explicit shared_ptr_as_custom_holder(T *) { + throw std::runtime_error("invalid shared_ptr_as_custom_holder constructor call"); + } + + explicit shared_ptr_as_custom_holder(std::shared_ptr ptr) : ptr_(std::move(ptr)) {} + + T *get() const { return ptr_.get(); } + + operator std::shared_ptr() const { return ptr_; } +}; + // Custom object with builtin reference counting (see 'object.h' for the implementation) class MyObject1 : public Object { public: @@ -239,6 +259,25 @@ struct SharedFromThisRef { std::shared_ptr shared = std::make_shared(); }; +class PrivateDtorWithCustomHolder { +public: + static std::shared_ptr create(int value) { + return {new PrivateDtorWithCustomHolder(value), + [](PrivateDtorWithCustomHolder *ptr) { delete ptr; }}; + } + + int value = 0; + +private: + explicit PrivateDtorWithCustomHolder(int value_) : value(value_) {} + ~PrivateDtorWithCustomHolder() = default; +}; + +std::shared_ptr &private_dtor_with_custom_holder_singleton() { + static auto singleton = PrivateDtorWithCustomHolder::create(17); + return singleton; +} + // Issue #865: shared_from_this doesn't work with virtual inheritance struct SharedFromThisVBase : std::enable_shared_from_this { SharedFromThisVBase() = default; @@ -341,6 +380,7 @@ struct holder_helper> { // Make pybind aware of the ref-counted wrapper type (s): PYBIND11_DECLARE_HOLDER_TYPE(T, ref, true) PYBIND11_DECLARE_HOLDER_TYPE(T, const_only_shared_ptr, true) +PYBIND11_DECLARE_HOLDER_TYPE(T, shared_ptr_as_custom_holder) // The following is not required anymore for std::shared_ptr, but it should compile without error: PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr) PYBIND11_DECLARE_HOLDER_TYPE(T, huge_unique_ptr) @@ -501,6 +541,18 @@ TEST_SUBMODULE(smart_ptr, m) { .def(py::init([](const std::string &value) { return MyObject6::createObject(value); })) .def_property_readonly("value", &MyObject6::value); + py::class_>( + m, "PrivateDtorWithCustomHolder") + .def_property("value", + [](const PrivateDtorWithCustomHolder &self) { return self.value; }, + [](PrivateDtorWithCustomHolder &self, int value) { self.value = value; }) + .def_static("get_singleton_holder", []() { + return shared_ptr_as_custom_holder( + private_dtor_with_custom_holder_singleton()); + }); + m.def("get_private_dtor_with_custom_holder_shared_ptr", + []() { return private_dtor_with_custom_holder_singleton(); }); + // test_shared_ptr_and_references using A = SharedPtrRef::A; py::class_>(m, "A"); diff --git a/tests/test_smart_ptr.py b/tests/test_smart_ptr.py index 76ebd8cf20..9c34a1b9ed 100644 --- a/tests/test_smart_ptr.py +++ b/tests/test_smart_ptr.py @@ -368,3 +368,13 @@ def test_move_only_holder_caster_shared_ptr_with_smart_holder_support_enabled(): def test_const_only_holder(): o = m.MyObject6("my_data") assert o.value == "my_data" + + +def test_shared_ptr_cast_for_custom_holder_with_private_dtor(): + holder_obj = m.PrivateDtorWithCustomHolder.get_singleton_holder() + shared_ptr_obj = m.get_private_dtor_with_custom_holder_shared_ptr() + + holder_obj.value = 23 + + assert shared_ptr_obj.value == 23 + assert m.get_private_dtor_with_custom_holder_shared_ptr().value == 23 From fc535cc0b3b68cccd53c26870996f4bf605950ad Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sun, 17 May 2026 05:38:14 +0000 Subject: [PATCH 02/15] fix: support shared_ptr casts for compatible custom holders Add internal callback plumbing so class_ registrations can expose when a custom holder is constructible from std::shared_ptr. When py::cast() sees a returned std::shared_ptr for such a bound type, it now creates the Python instance by reconstructing the bound holder from an erased aliasing shared_ptr instead of rejecting the conversion. This preserves the #6008 safety check for incompatible holders while restoring support for private-destructor/shared_ptr patterns that use a custom holder wrapper. --- include/pybind11/attr.h | 3 ++ include/pybind11/cast.h | 4 ++ include/pybind11/detail/internals.h | 1 + include/pybind11/detail/type_caster_base.h | 57 ++++++++++++++++++++++ include/pybind11/pybind11.h | 33 +++++++++++++ 5 files changed, 98 insertions(+) diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index d337595a0b..e00704f546 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -313,6 +313,9 @@ struct type_record { /// Function pointer to class_<..>::dealloc void (*dealloc)(detail::value_and_holder &) = nullptr; + /// Function pointer to construct a bound holder from an erased std::shared_ptr. + void (*init_instance_from_shared_ptr)(instance *, const std::shared_ptr *) = nullptr; + /// Function pointer for casting alias class (aka trampoline) pointer to /// trampoline_self_life_support pointer. Sidesteps cross-DSO RTTI issues /// on platforms like macOS (see PR #5728 for details). diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index b7a4c2b0ce..86b9682045 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1025,6 +1025,10 @@ struct copyable_holder_caster< if (tinfo != nullptr && tinfo->holder_enum_v == holder_enum_t::std_shared_ptr) { return type_caster_base::cast_holder(srcs, &src); } + if (tinfo != nullptr && tinfo->init_instance_from_shared_ptr != nullptr) { + return smart_holder_type_caster_support::custom_holder_from_shared_ptr( + src, policy, parent, srcs.result); + } if (parent) { return type_caster_generic::cast_non_owning( diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 4175d20b21..e3865f17a8 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -403,6 +403,7 @@ struct type_info { void *(*operator_new)(size_t); void (*init_instance)(instance *, const void *); void (*dealloc)(value_and_holder &v_h); + void (*init_instance_from_shared_ptr)(instance *, const std::shared_ptr *) = nullptr; // Cross-DSO-safe function pointers, to sidestep cross-DSO RTTI issues // on platforms like macOS (see PR #5728 for details): diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 8fbf700e12..b95b0bdb0e 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -803,6 +803,63 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, cs); } +template +handle custom_holder_from_shared_ptr(const std::shared_ptr &src, + return_value_policy policy, + handle parent, + const cast_sources::resolved_source &cs) { + switch (policy) { + case return_value_policy::automatic: + case return_value_policy::automatic_reference: + break; + case return_value_policy::take_ownership: + throw cast_error("Invalid return_value_policy for shared_ptr (take_ownership)."); + case return_value_policy::copy: + case return_value_policy::move: + break; + case return_value_policy::reference: + throw cast_error("Invalid return_value_policy for shared_ptr (reference)."); + case return_value_policy::reference_internal: + break; + } + if (!src) { + return none().release(); + } + + void *src_raw_void_ptr = const_cast(cs.cppobj); + assert(cs.tinfo != nullptr); + const detail::type_info *tinfo = cs.tinfo; + if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { + return existing_inst; + } + + auto inst = reinterpret_steal(make_new_instance(tinfo->type)); + auto *inst_raw_ptr = reinterpret_cast(inst.ptr()); + inst_raw_ptr->owned = true; + void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr(); + valueptr = src_raw_void_ptr; + + auto erased_shared_ptr = std::shared_ptr(src, src_raw_void_ptr); + tinfo->init_instance_from_shared_ptr(inst_raw_ptr, &erased_shared_ptr); + + if (policy == return_value_policy::reference_internal) { + keep_alive_impl(inst, parent); + } + + return inst.release(); +} + +template +handle custom_holder_from_shared_ptr(const std::shared_ptr &src, + return_value_policy policy, + handle parent, + const cast_sources::resolved_source &cs) { + return custom_holder_from_shared_ptr(std::const_pointer_cast(src), // Const2Mutbl + policy, + parent, + cs); +} + struct shared_ptr_parent_life_support { PyObject *parent; explicit shared_ptr_parent_life_support(PyObject *parent) : parent{parent} { diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 9cc45bdbdc..c0e6f554de 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1825,6 +1825,7 @@ class generic_type : public object { tinfo->holder_size_in_ptrs = size_in_ptrs(rec.holder_size); tinfo->init_instance = rec.init_instance; tinfo->dealloc = rec.dealloc; + tinfo->init_instance_from_shared_ptr = rec.init_instance_from_shared_ptr; tinfo->get_trampoline_self_life_support = rec.get_trampoline_self_life_support; tinfo->simple_type = true; tinfo->simple_ancestors = true; @@ -2361,6 +2362,7 @@ class class_ : public detail::generic_type { record.type_align = alignof(conditional_t &); record.holder_size = sizeof(holder_type); record.init_instance = init_instance; + record.init_instance_from_shared_ptr = get_init_instance_from_shared_ptr(); if (detail::is_instantiation::value) { record.holder_enum_v = detail::holder_enum_t::std_unique_ptr; @@ -2783,6 +2785,37 @@ class class_ : public detail::generic_type { } } + template >::value, int> + = 0> + static void init_instance_from_shared_ptr(detail::instance *inst, + const std::shared_ptr *shared_ptr_void_ptr) { + auto v_h = inst->get_value_and_holder(detail::get_type_info(typeid(type))); + if (!v_h.instance_registered()) { + register_instance(inst, v_h.value_ptr(), v_h.type); + v_h.set_instance_registered(); + } + auto shared_ptr = std::shared_ptr(*shared_ptr_void_ptr, v_h.value_ptr()); + new (std::addressof(v_h.holder())) holder_type(std::move(shared_ptr)); + v_h.set_holder_constructed(); + } + + template >::value, int> + = 0> + static constexpr auto get_init_instance_from_shared_ptr() + -> void (*)(detail::instance *, const std::shared_ptr *) { + return &init_instance_from_shared_ptr<>; + } + + template >::value, int> + = 0> + static constexpr auto get_init_instance_from_shared_ptr() + -> void (*)(detail::instance *, const std::shared_ptr *) { + return nullptr; + } + /// Performs instance initialization including constructing a holder and registering the known /// instance. Should be called as soon as the `type` value_ptr is set for an instance. Takes /// an optional pointer to an existing holder to use; if not specified and the instance is From e56dc0318c827c6fd23459bbade22419292ea7ff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 05:44:42 +0000 Subject: [PATCH 03/15] style: pre-commit fixes --- include/pybind11/pybind11.h | 6 ++---- tests/test_smart_ptr.cpp | 10 ++++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index c0e6f554de..0f4a7e69c8 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2786,8 +2786,7 @@ class class_ : public detail::generic_type { } template >::value, int> - = 0> + detail::enable_if_t>::value, int> = 0> static void init_instance_from_shared_ptr(detail::instance *inst, const std::shared_ptr *shared_ptr_void_ptr) { auto v_h = inst->get_value_and_holder(detail::get_type_info(typeid(type))); @@ -2801,8 +2800,7 @@ class class_ : public detail::generic_type { } template >::value, int> - = 0> + detail::enable_if_t>::value, int> = 0> static constexpr auto get_init_instance_from_shared_ptr() -> void (*)(detail::instance *, const std::shared_ptr *) { return &init_instance_from_shared_ptr<>; diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index ba1da574a6..68110f77fe 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -541,11 +541,13 @@ TEST_SUBMODULE(smart_ptr, m) { .def(py::init([](const std::string &value) { return MyObject6::createObject(value); })) .def_property_readonly("value", &MyObject6::value); - py::class_>( + py::class_>( m, "PrivateDtorWithCustomHolder") - .def_property("value", - [](const PrivateDtorWithCustomHolder &self) { return self.value; }, - [](PrivateDtorWithCustomHolder &self, int value) { self.value = value; }) + .def_property( + "value", + [](const PrivateDtorWithCustomHolder &self) { return self.value; }, + [](PrivateDtorWithCustomHolder &self, int value) { self.value = value; }) .def_static("get_singleton_holder", []() { return shared_ptr_as_custom_holder( private_dtor_with_custom_holder_singleton()); From 74320514197b627e3f7a42c142a8e6bec4478840 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 16:13:34 -0700 Subject: [PATCH 04/15] ci: pin Ubuntu PyPy 3.11 to 7.3.21 --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6556ebdcd0..8a4e971545 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,8 @@ jobs: python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_TEST_SMART_HOLDER=ON - runs-on: ubuntu-latest - python-version: 'pypy3.11' + # Temporarily pinned pending investigation in issue #6049. + python-version: 'pypy-3.11-v7.3.21' cmake-args: -DCMAKE_CXX_STANDARD=17 - runs-on: ubuntu-latest python-version: 'graalpy-24.2' From d818bd75b5f056a0c447dbbf2943954a28ed1583 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 16:16:49 -0700 Subject: [PATCH 05/15] test: mark custom holder conversion explicit --- tests/test_smart_ptr.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index 68110f77fe..aef7923831 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -100,7 +100,7 @@ class shared_ptr_as_custom_holder { T *get() const { return ptr_.get(); } - operator std::shared_ptr() const { return ptr_; } + explicit operator std::shared_ptr() const { return ptr_; } }; // Custom object with builtin reference counting (see 'object.h' for the implementation) From 432dcaca399a337570333c77178ecebf9bc2e88c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 16:23:24 -0700 Subject: [PATCH 06/15] test: cover incompatible custom holder shared_ptr casts --- tests/test_smart_ptr.cpp | 1 + tests/test_smart_ptr.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index aef7923831..f771d4ae07 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -613,6 +613,7 @@ TEST_SUBMODULE(smart_ptr, m) { py::class_>(m, "TypeWithMoveOnlyHolder") .def_static("make", []() { return custom_unique_ptr(new C); }) .def_static("make_as_object", []() { return py::cast(custom_unique_ptr(new C)); }); + m.def("get_type_with_move_only_holder_shared_ptr", []() { return std::make_shared(); }); // test_holder_with_addressof_operator using HolderWithAddressOf = shared_ptr_with_addressof_operator; diff --git a/tests/test_smart_ptr.py b/tests/test_smart_ptr.py index 9c34a1b9ed..35b6a79e7d 100644 --- a/tests/test_smart_ptr.py +++ b/tests/test_smart_ptr.py @@ -378,3 +378,14 @@ def test_shared_ptr_cast_for_custom_holder_with_private_dtor(): assert shared_ptr_obj.value == 23 assert m.get_private_dtor_with_custom_holder_shared_ptr().value == 23 + + +def test_shared_ptr_cast_for_incompatible_custom_holder_throws(): + with pytest.raises( + RuntimeError, + match=( + "Unable to convert std::shared_ptr to Python when the bound type does not use" + " std::shared_ptr or py::smart_holder as its holder type" + ), + ): + m.get_type_with_move_only_holder_shared_ptr() From 2cddf042f47db922e330994b91d02df5475d2c00 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 16:31:57 -0700 Subject: [PATCH 07/15] refactor: share std::shared_ptr cast setup --- include/pybind11/detail/type_caster_base.h | 98 +++++++++++----------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index b95b0bdb0e..0fe699d47e 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -742,11 +742,29 @@ handle smart_holder_from_unique_ptr(std::unique_ptr &&src, cs); } +struct shared_ptr_cast_data { + handle result; + object inst; + instance *inst_raw_ptr; + void *src_raw_void_ptr; + const detail::type_info *tinfo; + + explicit shared_ptr_cast_data(handle result_) + : result(result_), inst(), inst_raw_ptr(nullptr), src_raw_void_ptr(nullptr), + tinfo(nullptr) {} + + shared_ptr_cast_data(object &&inst_, + instance *inst_raw_ptr_, + void *src_raw_void_ptr_, + const detail::type_info *tinfo_) + : result(), inst(std::move(inst_)), inst_raw_ptr(inst_raw_ptr_), + src_raw_void_ptr(src_raw_void_ptr_), tinfo(tinfo_) {} +}; + template -handle smart_holder_from_shared_ptr(const std::shared_ptr &src, - return_value_policy policy, - handle parent, - const cast_sources::resolved_source &cs) { +shared_ptr_cast_data prepare_shared_ptr_cast(const std::shared_ptr &src, + return_value_policy policy, + const cast_sources::resolved_source &cs) { switch (policy) { case return_value_policy::automatic: case return_value_policy::automatic_reference: @@ -762,7 +780,7 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, break; } if (!src) { - return none().release(); + return shared_ptr_cast_data(none().release()); } // cs.cppobj is the subobject pointer appropriate for tinfo (may differ from src.get() @@ -771,9 +789,9 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, assert(cs.tinfo != nullptr); const detail::type_info *tinfo = cs.tinfo; if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { - // PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing smart_holder. + // PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing holder. // PYBIND11:REMINDER: MISSING: keep_alive. - return existing_inst; + return shared_ptr_cast_data(existing_inst); } auto inst = reinterpret_steal(make_new_instance(tinfo->type)); @@ -781,17 +799,33 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, inst_raw_ptr->owned = true; void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr(); valueptr = src_raw_void_ptr; + return shared_ptr_cast_data(std::move(inst), inst_raw_ptr, src_raw_void_ptr, tinfo); +} - auto smhldr = smart_holder::from_shared_ptr(std::shared_ptr(src, src_raw_void_ptr)); - tinfo->init_instance(inst_raw_ptr, static_cast(&smhldr)); - +inline handle finish_shared_ptr_cast(object &&inst, return_value_policy policy, handle parent) { if (policy == return_value_policy::reference_internal) { keep_alive_impl(inst, parent); } - return inst.release(); } +template +handle smart_holder_from_shared_ptr(const std::shared_ptr &src, + return_value_policy policy, + handle parent, + const cast_sources::resolved_source &cs) { + auto cast_data = prepare_shared_ptr_cast(src, policy, cs); + if (cast_data.result) { + return cast_data.result; + } + + auto smhldr + = smart_holder::from_shared_ptr(std::shared_ptr(src, cast_data.src_raw_void_ptr)); + cast_data.tinfo->init_instance(cast_data.inst_raw_ptr, static_cast(&smhldr)); + + return finish_shared_ptr_cast(std::move(cast_data.inst), policy, parent); +} + template handle smart_holder_from_shared_ptr(const std::shared_ptr &src, return_value_policy policy, @@ -808,45 +842,15 @@ handle custom_holder_from_shared_ptr(const std::shared_ptr &src, return_value_policy policy, handle parent, const cast_sources::resolved_source &cs) { - switch (policy) { - case return_value_policy::automatic: - case return_value_policy::automatic_reference: - break; - case return_value_policy::take_ownership: - throw cast_error("Invalid return_value_policy for shared_ptr (take_ownership)."); - case return_value_policy::copy: - case return_value_policy::move: - break; - case return_value_policy::reference: - throw cast_error("Invalid return_value_policy for shared_ptr (reference)."); - case return_value_policy::reference_internal: - break; - } - if (!src) { - return none().release(); + auto cast_data = prepare_shared_ptr_cast(src, policy, cs); + if (cast_data.result) { + return cast_data.result; } - void *src_raw_void_ptr = const_cast(cs.cppobj); - assert(cs.tinfo != nullptr); - const detail::type_info *tinfo = cs.tinfo; - if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { - return existing_inst; - } - - auto inst = reinterpret_steal(make_new_instance(tinfo->type)); - auto *inst_raw_ptr = reinterpret_cast(inst.ptr()); - inst_raw_ptr->owned = true; - void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr(); - valueptr = src_raw_void_ptr; + auto erased_shared_ptr = std::shared_ptr(src, cast_data.src_raw_void_ptr); + cast_data.tinfo->init_instance_from_shared_ptr(cast_data.inst_raw_ptr, &erased_shared_ptr); - auto erased_shared_ptr = std::shared_ptr(src, src_raw_void_ptr); - tinfo->init_instance_from_shared_ptr(inst_raw_ptr, &erased_shared_ptr); - - if (policy == return_value_policy::reference_internal) { - keep_alive_impl(inst, parent); - } - - return inst.release(); + return finish_shared_ptr_cast(std::move(cast_data.inst), policy, parent); } template From 58118a088811ad4debf1674cbe4571e3f2b706e3 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 20:56:00 -0700 Subject: [PATCH 08/15] remove redundant shared_ptr cast initializers to resolve clang-tidy errors --- include/pybind11/detail/type_caster_base.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 0fe699d47e..ce30937ed8 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -750,15 +750,14 @@ struct shared_ptr_cast_data { const detail::type_info *tinfo; explicit shared_ptr_cast_data(handle result_) - : result(result_), inst(), inst_raw_ptr(nullptr), src_raw_void_ptr(nullptr), - tinfo(nullptr) {} + : result(result_), inst_raw_ptr(nullptr), src_raw_void_ptr(nullptr), tinfo(nullptr) {} shared_ptr_cast_data(object &&inst_, instance *inst_raw_ptr_, void *src_raw_void_ptr_, const detail::type_info *tinfo_) - : result(), inst(std::move(inst_)), inst_raw_ptr(inst_raw_ptr_), - src_raw_void_ptr(src_raw_void_ptr_), tinfo(tinfo_) {} + : inst(std::move(inst_)), inst_raw_ptr(inst_raw_ptr_), src_raw_void_ptr(src_raw_void_ptr_), + tinfo(tinfo_) {} }; template From 747c094bcddc91d933a5abf8859d6166be36629f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 21:46:37 -0700 Subject: [PATCH 09/15] refactor: group type_info init callbacks (for slightly improved readability) --- include/pybind11/detail/internals.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index e3865f17a8..ae5e6121e3 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -402,8 +402,8 @@ struct type_info { size_t type_size, type_align, holder_size_in_ptrs; void *(*operator_new)(size_t); void (*init_instance)(instance *, const void *); - void (*dealloc)(value_and_holder &v_h); void (*init_instance_from_shared_ptr)(instance *, const std::shared_ptr *) = nullptr; + void (*dealloc)(value_and_holder &v_h); // Cross-DSO-safe function pointers, to sidestep cross-DSO RTTI issues // on platforms like macOS (see PR #5728 for details): From 3ecd0bf60745d6a192709abf201ffbbacaefd762 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 21:52:26 -0700 Subject: [PATCH 10/15] chore: bump pybind11 internals version to 13 --- include/pybind11/detail/internals.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index ae5e6121e3..f8632baf20 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -39,11 +39,11 @@ /// further ABI-incompatible changes may be made before the ABI is officially /// changed to the new version. #ifndef PYBIND11_INTERNALS_VERSION -# define PYBIND11_INTERNALS_VERSION 12 +# define PYBIND11_INTERNALS_VERSION 13 #endif -#if PYBIND11_INTERNALS_VERSION < 12 -# error "PYBIND11_INTERNALS_VERSION 12 is the minimum for all platforms for pybind11 v3.1.0" +#if PYBIND11_INTERNALS_VERSION < 13 +# error "PYBIND11_INTERNALS_VERSION 13 is the minimum for all platforms for pybind11 v3.1.0" #endif PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) From 42954cf6ace7998e66ba06cb9a569bc0b64cd466 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 22:00:34 -0700 Subject: [PATCH 11/15] fix: keep shared_ptr cast early results owned --- include/pybind11/detail/type_caster_base.h | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index ce30937ed8..4d1d995c83 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -743,14 +743,15 @@ handle smart_holder_from_unique_ptr(std::unique_ptr &&src, } struct shared_ptr_cast_data { - handle result; + object result; object inst; instance *inst_raw_ptr; void *src_raw_void_ptr; const detail::type_info *tinfo; - explicit shared_ptr_cast_data(handle result_) - : result(result_), inst_raw_ptr(nullptr), src_raw_void_ptr(nullptr), tinfo(nullptr) {} + explicit shared_ptr_cast_data(object &&result_) + : result(std::move(result_)), inst_raw_ptr(nullptr), src_raw_void_ptr(nullptr), + tinfo(nullptr) {} shared_ptr_cast_data(object &&inst_, instance *inst_raw_ptr_, @@ -779,7 +780,7 @@ shared_ptr_cast_data prepare_shared_ptr_cast(const std::shared_ptr &src, break; } if (!src) { - return shared_ptr_cast_data(none().release()); + return shared_ptr_cast_data(reinterpret_steal(none().release())); } // cs.cppobj is the subobject pointer appropriate for tinfo (may differ from src.get() @@ -790,7 +791,7 @@ shared_ptr_cast_data prepare_shared_ptr_cast(const std::shared_ptr &src, if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { // PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing holder. // PYBIND11:REMINDER: MISSING: keep_alive. - return shared_ptr_cast_data(existing_inst); + return shared_ptr_cast_data(reinterpret_steal(existing_inst)); } auto inst = reinterpret_steal(make_new_instance(tinfo->type)); @@ -815,7 +816,7 @@ handle smart_holder_from_shared_ptr(const std::shared_ptr &src, const cast_sources::resolved_source &cs) { auto cast_data = prepare_shared_ptr_cast(src, policy, cs); if (cast_data.result) { - return cast_data.result; + return cast_data.result.release(); } auto smhldr @@ -843,7 +844,7 @@ handle custom_holder_from_shared_ptr(const std::shared_ptr &src, const cast_sources::resolved_source &cs) { auto cast_data = prepare_shared_ptr_cast(src, policy, cs); if (cast_data.result) { - return cast_data.result; + return cast_data.result.release(); } auto erased_shared_ptr = std::shared_ptr(src, cast_data.src_raw_void_ptr); From fdbfbd2634542fd14286cf7354bf8e00617ff4a3 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 22:07:31 -0700 Subject: [PATCH 12/15] refactor: group type_record init callbacks --- include/pybind11/attr.h | 6 +++--- include/pybind11/pybind11.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index e00704f546..f709b4e320 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -310,12 +310,12 @@ struct type_record { /// Function pointer to class_<..>::init_instance void (*init_instance)(instance *, const void *) = nullptr; - /// Function pointer to class_<..>::dealloc - void (*dealloc)(detail::value_and_holder &) = nullptr; - /// Function pointer to construct a bound holder from an erased std::shared_ptr. void (*init_instance_from_shared_ptr)(instance *, const std::shared_ptr *) = nullptr; + /// Function pointer to class_<..>::dealloc + void (*dealloc)(detail::value_and_holder &) = nullptr; + /// Function pointer for casting alias class (aka trampoline) pointer to /// trampoline_self_life_support pointer. Sidesteps cross-DSO RTTI issues /// on platforms like macOS (see PR #5728 for details). diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 0f4a7e69c8..bf7a49d9de 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -1824,8 +1824,8 @@ class generic_type : public object { tinfo->operator_new = rec.operator_new; tinfo->holder_size_in_ptrs = size_in_ptrs(rec.holder_size); tinfo->init_instance = rec.init_instance; - tinfo->dealloc = rec.dealloc; tinfo->init_instance_from_shared_ptr = rec.init_instance_from_shared_ptr; + tinfo->dealloc = rec.dealloc; tinfo->get_trampoline_self_life_support = rec.get_trampoline_self_life_support; tinfo->simple_type = true; tinfo->simple_ancestors = true; From 9adc072942b80ebab6dcf9eca9a9c6fb0d0ca257 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 22:11:07 -0700 Subject: [PATCH 13/15] Add // Issue #6064 to new code in tests/test_smart_ptr.cpp --- tests/test_smart_ptr.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index f771d4ae07..f7da8908f1 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -84,7 +84,7 @@ class const_only_shared_ptr { }; template -class shared_ptr_as_custom_holder { +class shared_ptr_as_custom_holder { // Issue #6064 std::shared_ptr ptr_; public: @@ -259,7 +259,7 @@ struct SharedFromThisRef { std::shared_ptr shared = std::make_shared(); }; -class PrivateDtorWithCustomHolder { +class PrivateDtorWithCustomHolder { // Issue #6064 public: static std::shared_ptr create(int value) { return {new PrivateDtorWithCustomHolder(value), From 48d1d3001327f81185bb9d600b6d81e09ceaf04b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 22:20:16 -0700 Subject: [PATCH 14/15] test: cover const shared_ptr custom holder casts --- tests/test_smart_ptr.cpp | 4 ++++ tests/test_smart_ptr.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index f7da8908f1..a920354a5a 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -554,6 +554,10 @@ TEST_SUBMODULE(smart_ptr, m) { }); m.def("get_private_dtor_with_custom_holder_shared_ptr", []() { return private_dtor_with_custom_holder_singleton(); }); + m.def("get_private_dtor_with_custom_holder_const_shared_ptr", []() { + return std::shared_ptr( + private_dtor_with_custom_holder_singleton()); + }); // test_shared_ptr_and_references using A = SharedPtrRef::A; diff --git a/tests/test_smart_ptr.py b/tests/test_smart_ptr.py index 35b6a79e7d..b058aec0ce 100644 --- a/tests/test_smart_ptr.py +++ b/tests/test_smart_ptr.py @@ -373,11 +373,14 @@ def test_const_only_holder(): def test_shared_ptr_cast_for_custom_holder_with_private_dtor(): holder_obj = m.PrivateDtorWithCustomHolder.get_singleton_holder() shared_ptr_obj = m.get_private_dtor_with_custom_holder_shared_ptr() + const_shared_ptr_obj = m.get_private_dtor_with_custom_holder_const_shared_ptr() holder_obj.value = 23 assert shared_ptr_obj.value == 23 + assert const_shared_ptr_obj.value == 23 assert m.get_private_dtor_with_custom_holder_shared_ptr().value == 23 + assert m.get_private_dtor_with_custom_holder_const_shared_ptr().value == 23 def test_shared_ptr_cast_for_incompatible_custom_holder_throws(): From 6fe83184d2b2675c80e37fe329b9b34c025d79c6 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 17 May 2026 22:31:25 -0700 Subject: [PATCH 15/15] test: cover fresh shared_ptr custom holder casts Ensure custom holders construct from shared_ptr before an existing Python instance can satisfy the cast. --- tests/test_smart_ptr.cpp | 6 ++++++ tests/test_smart_ptr.py | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/tests/test_smart_ptr.cpp b/tests/test_smart_ptr.cpp index a920354a5a..70386c25de 100644 --- a/tests/test_smart_ptr.cpp +++ b/tests/test_smart_ptr.cpp @@ -558,6 +558,12 @@ TEST_SUBMODULE(smart_ptr, m) { return std::shared_ptr( private_dtor_with_custom_holder_singleton()); }); + m.def("make_private_dtor_with_custom_holder_shared_ptr", + []() { return PrivateDtorWithCustomHolder::create(41); }); + m.def("make_private_dtor_with_custom_holder_const_shared_ptr", []() { + return std::shared_ptr( + PrivateDtorWithCustomHolder::create(43)); + }); // test_shared_ptr_and_references using A = SharedPtrRef::A; diff --git a/tests/test_smart_ptr.py b/tests/test_smart_ptr.py index b058aec0ce..9181fe5e6c 100644 --- a/tests/test_smart_ptr.py +++ b/tests/test_smart_ptr.py @@ -371,6 +371,14 @@ def test_const_only_holder(): def test_shared_ptr_cast_for_custom_holder_with_private_dtor(): + fresh_shared_ptr_obj = m.make_private_dtor_with_custom_holder_shared_ptr() + fresh_const_shared_ptr_obj = ( + m.make_private_dtor_with_custom_holder_const_shared_ptr() + ) + + assert fresh_shared_ptr_obj.value == 41 + assert fresh_const_shared_ptr_obj.value == 43 + holder_obj = m.PrivateDtorWithCustomHolder.get_singleton_holder() shared_ptr_obj = m.get_private_dtor_with_custom_holder_shared_ptr() const_shared_ptr_obj = m.get_private_dtor_with_custom_holder_const_shared_ptr()