diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index bef0f2cb8f..b7a4c2b0ce 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -1027,7 +1027,7 @@ struct copyable_holder_caster< } if (parent) { - return type_caster_base::cast( + return type_caster_generic::cast_non_owning( srcs, return_value_policy::reference_internal, parent); } diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index b0c59e1138..8fbf700e12 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -1004,6 +1004,18 @@ class type_caster_generic { return cast(srcs, policy, parent, copy_constructor, move_constructor, existing_holder); } + static handle cast_non_owning(const cast_sources &srcs, + return_value_policy policy, + handle parent, + const void *existing_holder = nullptr) { + // Reference-like policies alias an existing C++ object instead of creating + // a new one, so copy/move constructor callbacks must remain null here. + assert(policy == return_value_policy::reference + || policy == return_value_policy::reference_internal + || policy == return_value_policy::automatic_reference); + return cast(srcs, policy, parent, nullptr, nullptr, existing_holder); + } + PYBIND11_NOINLINE static handle cast(const cast_sources &srcs, return_value_policy policy, handle parent, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e87b1e93b3..d5d597cdff 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -167,6 +167,7 @@ set(PYBIND11_TEST_FILES test_operator_overloading test_pickling test_potentially_slicing_weak_ptr + test_pytorch_shared_ptr_cast_regression test_python_multiple_inheritance test_pytypes test_scoped_critical_section diff --git a/tests/test_class_sh_property.py b/tests/test_class_sh_property.py index 4a7b77c69a..b8a5933e5f 100644 --- a/tests/test_class_sh_property.py +++ b/tests/test_class_sh_property.py @@ -204,3 +204,15 @@ def test_non_smart_holder_member_type_with_smart_holder_owner_aliases_member(): legacy = obj.legacy legacy.value = 13 assert obj.legacy.value == 13 + + +def test_non_smart_holder_member_type_with_smart_holder_owner_aliases_member_multiple_reads(): + obj = m.ShWithSimpleStructMember() + + a = obj.legacy + b = obj.legacy + + a.value = 13 + + assert b.value == 13 + assert obj.legacy.value == 13 diff --git a/tests/test_pytorch_shared_ptr_cast_regression.cpp b/tests/test_pytorch_shared_ptr_cast_regression.cpp new file mode 100644 index 0000000000..1425962242 --- /dev/null +++ b/tests/test_pytorch_shared_ptr_cast_regression.cpp @@ -0,0 +1,62 @@ +#include "pybind11_tests.h" + +#include +#include + +#if defined(__clang__) +# if __has_warning("-Wdeprecated-copy-with-user-provided-dtor") +# pragma clang diagnostic error "-Wdeprecated-copy-with-user-provided-dtor" +# endif +# if __has_warning("-Wdeprecated-copy-with-dtor") +# pragma clang diagnostic error "-Wdeprecated-copy-with-dtor" +# endif +#endif + +namespace test_pytorch_regressions { + +// Directly extracted from PyTorch patterns that regressed in CI. +struct TracingState : std::enable_shared_from_this { + TracingState() = default; + ~TracingState() = default; + int value = 0; +}; + +const std::shared_ptr &get_tracing_state() { + static std::shared_ptr state = std::make_shared(); + return state; +} + +struct InterfaceType { + ~InterfaceType() = default; + int value = 0; +}; +using InterfaceTypePtr = std::shared_ptr; + +struct CompilationUnit { + InterfaceTypePtr iface = std::make_shared(); + + InterfaceTypePtr get_interface(const std::string &) const { return iface; } +}; + +} // namespace test_pytorch_regressions + +TEST_SUBMODULE(pybind11_pytorch_regressions, m) { + using namespace test_pytorch_regressions; + + py::class_>(m, "TracingState") + .def(py::init<>()) + .def_readwrite("value", &TracingState::value); + + m.def("_get_tracing_state", []() { return get_tracing_state(); }); + + py::class_(m, "InterfaceType") + .def(py::init<>()) + .def_readwrite("value", &InterfaceType::value); + + py::class_>(m, "CompilationUnit") + .def(py::init<>()) + .def("get_interface", + [](const std::shared_ptr &self, const std::string &name) { + return self->get_interface(name); + }); +} diff --git a/tests/test_pytorch_shared_ptr_cast_regression.py b/tests/test_pytorch_shared_ptr_cast_regression.py new file mode 100644 index 0000000000..b7c393b325 --- /dev/null +++ b/tests/test_pytorch_shared_ptr_cast_regression.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from pybind11_tests import pybind11_pytorch_regressions as m + + +def test_pytorch_like_get_tracing_state_aliases_singleton_shared_ptr(): + a = m._get_tracing_state() + b = m._get_tracing_state() + + a.value = 17 + + assert b.value == 17 + assert m._get_tracing_state().value == 17 + + +def test_pytorch_like_compilation_unit_get_interface_aliases_member_shared_ptr(): + cu = m.CompilationUnit() + + a = cu.get_interface("iface") + b = cu.get_interface("iface") + + a.value = 23 + + assert b.value == 23 + assert cu.get_interface("iface").value == 23