Skip to content

Commit 97a7fb7

Browse files
authored
Porting/adapting Dustin's PR #2839 to smart_holder branch (#2886)
* WIP: test setup complete, AddInCppUniquePtr failing (reproduces PyCLIF smart_ptrs_test failure). * Fully tested locally. * Adding new tests to cmake file.
1 parent 6a7e9f4 commit 97a7fb7

8 files changed

+360
-7
lines changed

include/pybind11/detail/smart_holder_poc.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ struct smart_holder {
102102
bool vptr_is_using_builtin_delete : 1;
103103
bool vptr_is_external_shared_ptr : 1;
104104
bool is_populated : 1;
105+
bool pointee_depends_on_holder_owner : 1; // SMART_HOLDER_WIP: See PR #2839.
105106

106107
// Design choice: smart_holder is movable but not copyable.
107108
smart_holder(smart_holder &&) = default;
@@ -111,13 +112,14 @@ struct smart_holder {
111112

112113
smart_holder()
113114
: rtti_uqp_del{nullptr}, vptr_is_using_noop_deleter{false},
114-
vptr_is_using_builtin_delete{false}, vptr_is_external_shared_ptr{false}, is_populated{
115-
false} {}
115+
vptr_is_using_builtin_delete{false}, vptr_is_external_shared_ptr{false},
116+
is_populated{false}, pointee_depends_on_holder_owner{false} {}
116117

117118
explicit smart_holder(bool vptr_deleter_armed_flag)
118119
: rtti_uqp_del{nullptr}, vptr_deleter_armed_flag_ptr{new bool{vptr_deleter_armed_flag}},
119120
vptr_is_using_noop_deleter{false}, vptr_is_using_builtin_delete{false},
120-
vptr_is_external_shared_ptr{false}, is_populated{false} {}
121+
vptr_is_external_shared_ptr{false}, is_populated{false}, pointee_depends_on_holder_owner{
122+
false} {}
121123

122124
bool has_pointee() const { return vptr.get() != nullptr; }
123125

include/pybind11/detail/smart_holder_type_casters.h

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#pragma once
66

7+
#include "../gil.h"
78
#include "../pytypes.h"
89
#include "common.h"
910
#include "descr.h"
@@ -262,7 +263,9 @@ struct smart_holder_type_caster_class_hooks : smart_holder_type_caster_base_tag
262263
}
263264

264265
template <typename T>
265-
static void init_instance_for_type(detail::instance *inst, const void *holder_const_void_ptr) {
266+
static void init_instance_for_type(detail::instance *inst,
267+
const void *holder_const_void_ptr,
268+
bool has_alias) {
266269
// Need for const_cast is a consequence of the type_info::init_instance type:
267270
// void (*init_instance)(instance *, const void *);
268271
auto holder_void_ptr = const_cast<void *>(holder_const_void_ptr);
@@ -284,6 +287,7 @@ struct smart_holder_type_caster_class_hooks : smart_holder_type_caster_base_tag
284287
new (std::addressof(v_h.holder<holder_type>()))
285288
holder_type(holder_type::from_raw_ptr_unowned(v_h.value_ptr<T>()));
286289
}
290+
v_h.holder<holder_type>().pointee_depends_on_holder_owner = has_alias;
287291
v_h.set_holder_constructed();
288292
}
289293

@@ -331,15 +335,34 @@ struct smart_holder_type_caster_load {
331335
return *raw_ptr;
332336
}
333337

338+
struct shared_ptr_dec_ref_deleter {
339+
// Note: deleter destructor fails on MSVC 2015 and GCC 4.8, so we manually call dec_ref
340+
// here instead.
341+
handle ref;
342+
void operator()(void *) {
343+
gil_scoped_acquire gil;
344+
ref.dec_ref();
345+
}
346+
};
347+
334348
std::shared_ptr<T> loaded_as_shared_ptr() const {
335349
if (load_impl.unowned_void_ptr_from_direct_conversion != nullptr)
336350
throw cast_error("Unowned pointer from direct conversion cannot be converted to a"
337351
" std::shared_ptr.");
338352
if (!have_holder())
339353
return nullptr;
340354
throw_if_uninitialized_or_disowned_holder();
341-
std::shared_ptr<void> void_ptr = holder().template as_shared_ptr<void>();
342-
return std::shared_ptr<T>(void_ptr, convert_type(void_ptr.get()));
355+
auto void_raw_ptr = holder().template as_raw_ptr_unowned<void>();
356+
auto type_raw_ptr = convert_type(void_raw_ptr);
357+
if (holder().pointee_depends_on_holder_owner) {
358+
// Tie lifetime of trampoline Python part to C++ part (PR #2839).
359+
return std::shared_ptr<T>(
360+
type_raw_ptr,
361+
shared_ptr_dec_ref_deleter{
362+
handle((PyObject *) load_impl.loaded_v_h.inst).inc_ref()});
363+
}
364+
std::shared_ptr<void> void_shd_ptr = holder().template as_shared_ptr<void>();
365+
return std::shared_ptr<T>(void_shd_ptr, type_raw_ptr);
343366
}
344367

345368
template <typename D>
@@ -352,6 +375,10 @@ struct smart_holder_type_caster_load {
352375
throw_if_uninitialized_or_disowned_holder();
353376
holder().template ensure_compatible_rtti_uqp_del<T, D>(context);
354377
holder().ensure_use_count_1(context);
378+
if (holder().pointee_depends_on_holder_owner) {
379+
throw value_error("Ownership of instance with virtual overrides in Python cannot be "
380+
"transferred to C++.");
381+
}
355382
auto raw_void_ptr = holder().template as_raw_ptr_unowned<void>();
356383
// SMART_HOLDER_WIP: MISSING: Safety checks for type conversions
357384
// (T must be polymorphic or meet certain other conditions).

include/pybind11/pybind11.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1616,7 +1616,7 @@ class class_ : public detail::generic_type {
16161616
template <typename T = type,
16171617
detail::enable_if_t<detail::type_uses_smart_holder_type_caster<T>::value, int> = 0>
16181618
static void init_instance(detail::instance *inst, const void *holder_ptr) {
1619-
detail::type_caster<T>::template init_instance_for_type<type>(inst, holder_ptr);
1619+
detail::type_caster<T>::template init_instance_for_type<type>(inst, holder_ptr, has_alias);
16201620
}
16211621
// clang-format off
16221622

tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ set(PYBIND11_TEST_FILES
104104
test_class_sh_basic.cpp
105105
test_class_sh_factory_constructors.cpp
106106
test_class_sh_inheritance.cpp
107+
test_class_sh_trampoline_shared_ptr_cpp_arg.cpp
107108
test_class_sh_unique_ptr_member.cpp
109+
test_class_sh_with_alias.cpp
108110
test_constants_and_functions.cpp
109111
test_copy_move.cpp
110112
test_custom_type_casters.cpp
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) 2021 The Pybind Development Team.
2+
// All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
#include "pybind11_tests.h"
6+
#include "pybind11/smart_holder.h"
7+
8+
namespace {
9+
10+
// For testing whether a python subclass of a C++ object dies when the
11+
// last python reference is lost
12+
struct SpBase {
13+
// returns true if the base virtual function is called
14+
virtual bool is_base_used() { return true; }
15+
16+
// returns true if there's an associated python instance
17+
bool has_python_instance() {
18+
auto tinfo = py::detail::get_type_info(typeid(SpBase));
19+
return (bool)py::detail::get_object_handle(this, tinfo);
20+
}
21+
22+
SpBase() = default;
23+
SpBase(const SpBase &) = delete;
24+
virtual ~SpBase() = default;
25+
};
26+
27+
struct PySpBase : SpBase {
28+
bool is_base_used() override { PYBIND11_OVERRIDE(bool, SpBase, is_base_used); }
29+
};
30+
31+
struct SpBaseTester {
32+
std::shared_ptr<SpBase> get_object() { return m_obj; }
33+
void set_object(std::shared_ptr<SpBase> obj) { m_obj = obj; }
34+
bool is_base_used() { return m_obj->is_base_used(); }
35+
bool has_instance() { return (bool)m_obj; }
36+
bool has_python_instance() { return m_obj && m_obj->has_python_instance(); }
37+
void set_nonpython_instance() {
38+
m_obj = std::make_shared<SpBase>();
39+
}
40+
std::shared_ptr<SpBase> m_obj;
41+
};
42+
43+
// For testing that a C++ class without an alias does not retain the python
44+
// portion of the object
45+
struct SpGoAway {};
46+
47+
struct SpGoAwayTester {
48+
std::shared_ptr<SpGoAway> m_obj;
49+
};
50+
51+
} // namespace
52+
53+
PYBIND11_SMART_HOLDER_TYPE_CASTERS(SpBase)
54+
PYBIND11_SMART_HOLDER_TYPE_CASTERS(SpBaseTester)
55+
PYBIND11_SMART_HOLDER_TYPE_CASTERS(SpGoAway)
56+
PYBIND11_SMART_HOLDER_TYPE_CASTERS(SpGoAwayTester)
57+
58+
TEST_SUBMODULE(class_sh_trampoline_shared_ptr_cpp_arg, m) {
59+
// For testing whether a python subclass of a C++ object dies when the
60+
// last python reference is lost
61+
62+
py::classh<SpBase, PySpBase>(m, "SpBase")
63+
.def(py::init<>())
64+
.def("is_base_used", &SpBase::is_base_used)
65+
.def("has_python_instance", &SpBase::has_python_instance);
66+
67+
py::classh<SpBaseTester>(m, "SpBaseTester")
68+
.def(py::init<>())
69+
.def("get_object", &SpBaseTester::get_object)
70+
.def("set_object", &SpBaseTester::set_object)
71+
.def("is_base_used", &SpBaseTester::is_base_used)
72+
.def("has_instance", &SpBaseTester::has_instance)
73+
.def("has_python_instance", &SpBaseTester::has_python_instance)
74+
.def("set_nonpython_instance", &SpBaseTester::set_nonpython_instance)
75+
.def_readwrite("obj", &SpBaseTester::m_obj);
76+
77+
// For testing that a C++ class without an alias does not retain the python
78+
// portion of the object
79+
80+
py::classh<SpGoAway>(m, "SpGoAway").def(py::init<>());
81+
82+
py::classh<SpGoAwayTester>(m, "SpGoAwayTester")
83+
.def(py::init<>())
84+
.def_readwrite("obj", &SpGoAwayTester::m_obj);
85+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# -*- coding: utf-8 -*-
2+
import pytest
3+
4+
import pybind11_tests.class_sh_trampoline_shared_ptr_cpp_arg as m
5+
6+
7+
def test_shared_ptr_cpp_arg():
8+
import weakref
9+
10+
class PyChild(m.SpBase):
11+
def is_base_used(self):
12+
return False
13+
14+
tester = m.SpBaseTester()
15+
16+
obj = PyChild()
17+
objref = weakref.ref(obj)
18+
19+
# Pass the last python reference to the C++ function
20+
tester.set_object(obj)
21+
del obj
22+
pytest.gc_collect()
23+
24+
# python reference is still around since C++ has it now
25+
assert objref() is not None
26+
assert tester.is_base_used() is False
27+
assert tester.obj.is_base_used() is False
28+
assert tester.get_object() is objref()
29+
30+
31+
def test_shared_ptr_cpp_prop():
32+
class PyChild(m.SpBase):
33+
def is_base_used(self):
34+
return False
35+
36+
tester = m.SpBaseTester()
37+
38+
# Set the last python reference as a property of the C++ object
39+
tester.obj = PyChild()
40+
pytest.gc_collect()
41+
42+
# python reference is still around since C++ has it now
43+
assert tester.is_base_used() is False
44+
assert tester.has_python_instance() is True
45+
assert tester.obj.is_base_used() is False
46+
assert tester.obj.has_python_instance() is True
47+
48+
49+
def test_shared_ptr_arg_identity():
50+
import weakref
51+
52+
tester = m.SpBaseTester()
53+
54+
obj = m.SpBase()
55+
objref = weakref.ref(obj)
56+
57+
tester.set_object(obj)
58+
del obj
59+
pytest.gc_collect()
60+
61+
# python reference is still around since C++ has it
62+
assert objref() is not None
63+
assert tester.get_object() is objref()
64+
assert tester.has_python_instance() is True
65+
66+
# python reference disappears once the C++ object releases it
67+
tester.set_object(None)
68+
pytest.gc_collect()
69+
assert objref() is None
70+
71+
72+
def test_shared_ptr_alias_nonpython():
73+
tester = m.SpBaseTester()
74+
75+
# C++ creates the object, a python instance shouldn't exist
76+
tester.set_nonpython_instance()
77+
assert tester.is_base_used() is True
78+
assert tester.has_instance() is True
79+
assert tester.has_python_instance() is False
80+
81+
# Now a python instance exists
82+
cobj = tester.get_object()
83+
assert cobj.has_python_instance()
84+
assert tester.has_instance() is True
85+
assert tester.has_python_instance() is True
86+
87+
# Now it's gone
88+
del cobj
89+
pytest.gc_collect()
90+
assert tester.has_instance() is True
91+
assert tester.has_python_instance() is False
92+
93+
# SMART_HOLDER_WIP: the behavior below is DIFFERENT from PR #2839
94+
# When we pass it as an arg to a new tester the python instance should
95+
# disappear because it wasn't created with an alias
96+
new_tester = m.SpBaseTester()
97+
98+
cobj = tester.get_object()
99+
assert cobj.has_python_instance()
100+
101+
new_tester.set_object(cobj)
102+
assert tester.has_python_instance() is True
103+
assert new_tester.has_python_instance() is True
104+
105+
del cobj
106+
pytest.gc_collect()
107+
108+
# Gone!
109+
assert tester.has_instance() is True
110+
assert tester.has_python_instance() is True # False in PR #2839
111+
assert new_tester.has_instance() is True
112+
assert new_tester.has_python_instance() is True # False in PR #2839
113+
114+
115+
def test_shared_ptr_goaway():
116+
import weakref
117+
118+
tester = m.SpGoAwayTester()
119+
120+
obj = m.SpGoAway()
121+
objref = weakref.ref(obj)
122+
123+
assert tester.obj is None
124+
125+
tester.obj = obj
126+
del obj
127+
pytest.gc_collect()
128+
129+
# python reference is no longer around
130+
assert objref() is None
131+
# C++ reference is still around
132+
assert tester.obj is not None
133+
134+
135+
def test_infinite():
136+
tester = m.SpBaseTester()
137+
while True:
138+
tester.set_object(m.SpBase())
139+
return # Comment out for manual leak checking (use `top` command).

0 commit comments

Comments
 (0)