fix(smart_holder): keep void-cast semantics in from_unique_ptr (#6163)
* fix(smart_holder): keep void-cast semantics in from_unique_ptr
Fixes item 4 of #6159.
* test: cover base unique_ptr factories and trampoline lifetime
* test: limit prompt trampoline destruction check to CPython
* docs: clarify smart_holder void-cast ownership safeguard
---------
Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
diff --git a/include/pybind11/detail/init.h b/include/pybind11/detail/init.h
index 5f9e925..cce3eb5 100644
--- a/include/pybind11/detail/init.h
+++ b/include/pybind11/detail/init.h
@@ -206,8 +206,8 @@
template <typename T, typename D>
smart_holder init_smart_holder_from_unique_ptr(std::unique_ptr<T, D> &&unq_ptr,
bool void_cast_raw_ptr) {
- void *void_ptr = void_cast_raw_ptr ? static_cast<void *>(unq_ptr.get()) : nullptr;
- return smart_holder::from_unique_ptr(std::move(unq_ptr), void_ptr);
+ return smart_holder::from_unique_ptr(
+ std::move(unq_ptr), /*mi_subobject_ptr*/ nullptr, void_cast_raw_ptr);
}
template <typename Class,
diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h
index 57cd843..27e8591 100644
--- a/include/pybind11/detail/struct_smart_holder.h
+++ b/include/pybind11/detail/struct_smart_holder.h
@@ -43,7 +43,10 @@
* The `void_cast_raw_ptr` option is needed to make the `smart_holder` `vptr`
member invisible to the `shared_from_this` mechanism, in case the lifetime
- of a `PyObject` is tied to the pointee.
+ of a `PyObject` is tied to the pointee. This control block cannot itself keep
+ the Python object alive: that would create a reference cycle through the
+ object's own holder that Python's garbage collector cannot detect.
+ See https://github.com/pybind/pybind11/pull/3023 for the original rationale.
*/
#pragma once
@@ -333,7 +336,8 @@
template <typename T, typename D>
static smart_holder from_unique_ptr(std::unique_ptr<T, D> &&unq_ptr,
- void *mi_subobject_ptr = nullptr) {
+ void *mi_subobject_ptr = nullptr,
+ bool void_cast_raw_ptr = false) {
smart_holder hld;
hld.rtti_uqp_del = &typeid(D);
hld.vptr_is_using_std_default_delete = uqp_del_is_std_default_delete<T, D>();
@@ -344,7 +348,19 @@
? make_guarded_std_default_delete<T>(true)
: make_guarded_custom_deleter<T, D>(std::move(unq_ptr.get_deleter()), true);
// Critical: construct owner with pointer we intend to delete
- std::shared_ptr<T> owner(unq_ptr.get(), std::move(gd));
+ std::shared_ptr<void> owner;
+ if (void_cast_raw_ptr) {
+ // Passing a `T *` to the `shared_ptr` constructor would connect the
+ // `std::enable_shared_from_this` machinery to this control block, even for
+ // a `shared_ptr<void>`. For trampolines, this control block must stay invisible
+ // (see the `void_cast_raw_ptr` comment near the top of this file).
+ // Cast the raw pointer to `void *` before construction; converting the resulting
+ // `shared_ptr` to `shared_ptr<void>` afterwards would be too late.
+ owner = std::shared_ptr<void>(static_cast<void *>(unq_ptr.get()), std::move(gd));
+ } else {
+ owner
+ = std::static_pointer_cast<void>(std::shared_ptr<T>(unq_ptr.get(), std::move(gd)));
+ }
// Relinquish ownership only after successful construction of owner
(void) unq_ptr.release();
@@ -366,7 +382,7 @@
if (mi_subobject_ptr) {
hld.vptr = std::shared_ptr<void>(owner, mi_subobject_ptr);
} else {
- hld.vptr = std::static_pointer_cast<void>(owner);
+ hld.vptr = std::move(owner);
}
hld.is_populated = true;
diff --git a/tests/test_class_sh_trampoline_shared_from_this.cpp b/tests/test_class_sh_trampoline_shared_from_this.cpp
index dc6bf1c..f21bd5d 100644
--- a/tests/test_class_sh_trampoline_shared_from_this.cpp
+++ b/tests/test_class_sh_trampoline_shared_from_this.cpp
@@ -114,6 +114,13 @@
.def(py::init([](const std::string &history, int) {
return std::make_shared<SftTrampoline>(history);
}))
+ // The second argument is only used to make this overload unambiguous.
+ .def(py::init([](const std::string &history, const std::string &) {
+ return std::unique_ptr<SftTrampoline>(new SftTrampoline(history));
+ }))
+ .def(py::init([](const std::string &history, const std::string &, bool) {
+ return std::unique_ptr<Sft>(new SftTrampoline(history));
+ }))
.def_readonly("history", &Sft::history)
// This leads to multiple entries in registered_instances:
.def(py::init([](const std::shared_ptr<Sft> &existing) { return existing; }));
diff --git a/tests/test_class_sh_trampoline_shared_from_this.py b/tests/test_class_sh_trampoline_shared_from_this.py
index c59d0d1..965ee20 100644
--- a/tests/test_class_sh_trampoline_shared_from_this.py
+++ b/tests/test_class_sh_trampoline_shared_from_this.py
@@ -162,6 +162,40 @@
assert obj.history == "PureCppSft_Stash1AddSharedFromThis"
+@pytest.mark.parametrize("factory_args", [("unique_ptr",), ("unique_ptr", True)])
+def test_unique_ptr_factory_and_stash_via_shared_from_this(factory_args):
+ # Exercises that the smart_holder vptr stays invisible to the shared_from_this
+ # mechanism, also for a trampoline made by a unique_ptr factory.
+ class PySftUniquePtr(m.Sft):
+ def __init__(self, history):
+ super().__init__(history, *factory_args)
+
+ obj = PySftUniquePtr("PySftUniquePtr")
+ assert obj.history == "PySftUniquePtr"
+ stash1 = m.SftSharedPtrStash(1)
+ with pytest.raises(RuntimeError) as exc_info:
+ stash1.AddSharedFromThis(obj)
+ assert str(exc_info.value) == "bad_weak_ptr"
+ stash1.Add(obj)
+ assert obj.history == "PySftUniquePtr_Stash1Add"
+ assert stash1.use_count(0) == 1
+ stash1.AddSharedFromThis(obj)
+ assert obj.history == "PySftUniquePtr_Stash1Add_Stash1AddSharedFromThis"
+ assert stash1.use_count(0) == 2
+ assert stash1.use_count(1) == 2
+
+ obj_ref = weakref.ref(obj)
+ del obj
+ pytest.gc_collect()
+ assert obj_ref() is not None
+ assert obj_ref().history == "PySftUniquePtr_Stash1Add_Stash1AddSharedFromThis"
+ stash1.Clear()
+ pytest.gc_collect()
+ # As in the lifetime tests below, only CPython guarantees prompt destruction.
+ if not env.PYPY and not env.GRAALPY:
+ assert obj_ref() is None
+
+
def test_multiple_registered_instances_for_same_pointee():
obj0 = PySft("PySft")
obj0.attachment_in_dict = "Obj0"