Skip to content

Commit 2b93ea7

Browse files
committed
Attach python lifetime to shared_ptr passed to C++
- Reference cycles are possible as a result, but shared_ptr is already susceptible to this in C++
1 parent 918c909 commit 2b93ea7

File tree

3 files changed

+133
-1
lines changed

3 files changed

+133
-1
lines changed

include/pybind11/cast.h

+38-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#pragma once
1212

13+
#include "gil.h"
1314
#include "pytypes.h"
1415
#include "detail/typeid.h"
1516
#include "detail/descr.h"
@@ -1524,6 +1525,42 @@ struct holder_helper {
15241525
static auto get(const T &p) -> decltype(p.get()) { return p.get(); }
15251526
};
15261527

1528+
/// Another helper class for holders that helps construct derivative holders from
1529+
/// the original holder
1530+
template <typename T>
1531+
struct holder_retriever {
1532+
static auto get_derivative_holder(const value_and_holder &v_h) -> decltype(v_h.template holder<T>()) {
1533+
return v_h.template holder<T>();
1534+
}
1535+
};
1536+
1537+
template <typename T>
1538+
struct holder_retriever<std::shared_ptr<T>> {
1539+
struct shared_ptr_deleter {
1540+
// Note: deleter destructor fails on MSVC 2015 and GCC 4.8, so we manually
1541+
// call dec_ref here instead
1542+
handle ref;
1543+
void operator()(T *) {
1544+
gil_scoped_acquire gil;
1545+
ref.dec_ref();
1546+
}
1547+
};
1548+
1549+
static auto get_derivative_holder(const value_and_holder &v_h) -> std::shared_ptr<T> {
1550+
// The shared_ptr is always given to C++ code, so construct a new shared_ptr
1551+
// that is given a custom deleter. The custom deleter increments the python
1552+
// reference count to bind the python instance lifetime with the lifetime
1553+
// of the shared_ptr.
1554+
//
1555+
// This enables things like passing the last python reference of a subclass to a
1556+
// C++ function without the python reference dying.
1557+
//
1558+
// Reference cycles will cause a leak, but this is a limitation of shared_ptr
1559+
return std::shared_ptr<T>((T*)v_h.value_ptr(),
1560+
shared_ptr_deleter{handle((PyObject*)v_h.inst).inc_ref()});
1561+
}
1562+
};
1563+
15271564
/// Type caster for holder types like std::shared_ptr, etc.
15281565
/// The SFINAE hook is provided to help work around the current lack of support
15291566
/// for smart-pointer interoperability. Please consider it an implementation
@@ -1566,7 +1603,7 @@ struct copyable_holder_caster : public type_caster_base<type> {
15661603
bool load_value(value_and_holder &&v_h) {
15671604
if (v_h.holder_constructed()) {
15681605
value = v_h.value_ptr();
1569-
holder = v_h.template holder<holder_type>();
1606+
holder = holder_retriever<holder_type>::get_derivative_holder(v_h);
15701607
return true;
15711608
} else {
15721609
throw cast_error("Unable to cast from non-held to held instance (T& to Holder<T>) "

tests/test_smart_ptr.cpp

+33
Original file line numberDiff line numberDiff line change
@@ -397,4 +397,37 @@ TEST_SUBMODULE(smart_ptr, m) {
397397
list.append(py::cast(e));
398398
return list;
399399
});
400+
401+
// For testing whether a python subclass of a C++ object dies when the
402+
// last python reference is lost
403+
struct SpBase {
404+
// returns true if the base virtual function is called
405+
virtual bool is_base_used() { return true; }
406+
407+
SpBase() = default;
408+
SpBase(const SpBase&) = delete;
409+
virtual ~SpBase() = default;
410+
};
411+
412+
struct PySpBase : SpBase {
413+
bool is_base_used() override { PYBIND11_OVERRIDE(bool, SpBase, is_base_used); }
414+
};
415+
416+
struct SpBaseTester {
417+
std::shared_ptr<SpBase> get_object() { return m_obj; }
418+
void set_object(std::shared_ptr<SpBase> obj) { m_obj = obj; }
419+
bool is_base_used() { return m_obj->is_base_used(); }
420+
std::shared_ptr<SpBase> m_obj;
421+
};
422+
423+
py::class_<SpBase, std::shared_ptr<SpBase>, PySpBase>(m, "SpBase")
424+
.def(py::init<>())
425+
.def("is_base_used", &SpBase::is_base_used);
426+
427+
py::class_<SpBaseTester>(m, "SpBaseTester")
428+
.def(py::init<>())
429+
.def("get_object", &SpBaseTester::get_object)
430+
.def("set_object", &SpBaseTester::set_object)
431+
.def("is_base_used", &SpBaseTester::is_base_used)
432+
.def_readwrite("obj", &SpBaseTester::m_obj);
400433
}

tests/test_smart_ptr.py

+62
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,65 @@ def test_shared_ptr_gc():
316316
pytest.gc_collect()
317317
for i, v in enumerate(el.get()):
318318
assert i == v.value()
319+
320+
321+
def test_shared_ptr_cpp_arg():
322+
import weakref
323+
324+
class PyChild(m.SpBase):
325+
def is_base_used(self):
326+
return False
327+
328+
tester = m.SpBaseTester()
329+
330+
obj = PyChild()
331+
objref = weakref.ref(obj)
332+
333+
# Pass the last python reference to the C++ function
334+
tester.set_object(obj)
335+
del obj
336+
pytest.gc_collect()
337+
338+
# python reference is still around since C++ has it now
339+
assert objref() is not None
340+
assert tester.is_base_used() is False
341+
assert tester.obj.is_base_used() is False
342+
assert tester.get_object() is objref()
343+
344+
345+
def test_shared_ptr_cpp_prop():
346+
class PyChild(m.SpBase):
347+
def is_base_used(self):
348+
return False
349+
350+
tester = m.SpBaseTester()
351+
352+
# Set the last python reference as a property of the C++ object
353+
tester.obj = PyChild()
354+
pytest.gc_collect()
355+
356+
# python reference is still around since C++ has it now
357+
assert tester.is_base_used() is False
358+
assert tester.obj.is_base_used() is False
359+
360+
361+
def test_shared_ptr_arg_identity():
362+
import weakref
363+
364+
tester = m.SpBaseTester()
365+
366+
obj = m.SpBase()
367+
objref = weakref.ref(obj)
368+
369+
tester.set_object(obj)
370+
del obj
371+
pytest.gc_collect()
372+
373+
# python reference is still around since C++ has it
374+
assert objref() is not None
375+
assert tester.get_object() is objref()
376+
377+
# python reference disappears once the C++ object releases it
378+
tester.set_object(None)
379+
pytest.gc_collect()
380+
assert objref() is None

0 commit comments

Comments
 (0)