diff --git a/docs/changelog.rst b/docs/changelog.rst index 9f7dec01..4f23f8ca 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -34,7 +34,7 @@ Version 1.3.0 (TBD) * Reduced the per-instance overhead of nanobind by 1 pointer and simplified the internal hash table types to crunch ``libnanobind``. (commit `de018d `__). -* Reduced the size of nanobind type objects by 6 pointers. (PR `#194 +* Reduced the size of nanobind type objects by 5 pointers. (PR `#194 `__, `#195 `__, and commit `d82ca9 `__). @@ -108,6 +108,11 @@ Version 1.3.0 (TBD) ``some_enum < None`` will still fail, but now with a more informative error. +* nanobind now has limited support for binding types that inherit from + ``std::enable_shared_from_this``. See the :ref:`advanced section + on object ownership ` for more details. + (PR `#212 `__). + * ABI version 8. Version 1.2.0 (April 24, 2023) diff --git a/docs/ownership.rst b/docs/ownership.rst index 9a568fd6..600853e5 100644 --- a/docs/ownership.rst +++ b/docs/ownership.rst @@ -330,12 +330,30 @@ nanobind's support for shared pointers requires an extra include directive: You don't need to specify a return value policy annotation when a function returns a shared pointer. -Shared pointer support has one major limitation in nanobind: the -``std::enable_shared_from_this`` base class that normally enables safe -conversion of raw pointers to the associated shared pointer *may not be used*. -Further detail can be found in the *advanced* :ref:`section ` -on object ownership. If you need this feature, switch to intrusive reference -counting explained below. +nanobind's implementation of ``std::shared_ptr`` support typically +allocates a new ``shared_ptr`` control block each time a Python object +must be converted to ``std::shared_ptr``. The new ``shared_ptr`` +"owns" a reference to the Python object, and its deleter drops that +reference. This has the advantage that the Python portion of the +object will be kept alive by its C++-side references (which is +important when implementing C++ virtual methods in Python), but it can +be inefficient when passing the same object back and forth between +Python and C++ many times, and it means that the ``use_count()`` +method of ``std::shared_ptr`` will return a value that does not +capture all uses. Some of these problems can be mitigated by modifying +``T`` so that it inherits from ``std::enable_shared_from_this``. +See the :ref:`advanced section ` on object ownership +for more details on the implementation. + +nanobind has limited support for objects that inherit from +``std::enable_shared_from_this`` to allow safe conversion of raw +pointers to shared pointers. The safest way to deal with these objects +is to always use ``std::make_shared(...)`` when constructing them in C++, +and always pass them across the Python/C++ boundary wrapped in an explicit +``std::shared_ptr``. If you do this, then there shouldn't be any +surprises. If you will be passing raw ``T*`` pointers around, then +read the :ref:`advanced section on object ownership ` +for additional caveats. .. _intrusive_intro: diff --git a/docs/ownership_adv.rst b/docs/ownership_adv.rst index 130c33db..23706962 100644 --- a/docs/ownership_adv.rst +++ b/docs/ownership_adv.rst @@ -118,7 +118,7 @@ in the introductory section on object ownership and provides detail on how shared pointer conversion is *implemented* by nanobind. When the user calls a C++ function taking an argument of type -``std::shared`` from Python, ownership of that object must be +``std::shared_ptr`` from Python, ownership of that object must be shared between C++ to Python. nanobind does this by increasing the reference count of the ``PyObject`` and then creating a ``std::shared_ptr`` with a new control block containing a custom deleter that will in turn reduce the Python @@ -139,19 +139,110 @@ true global reference count. .. _enable_shared_from_this: -Limitations -^^^^^^^^^^^ - -nanobind refuses conversion of classes that derive from -``std::enable_shared_from_this``. This is a fundamental limitation: -nanobind instances do not create a base shared pointer that declares -ownership of an object. Other parts of a C++ codebase might then incorrectly -assume ownership and eventually try to ``delete`` a nanobind instance -allocated using ``pymalloc`` (which is undefined behavior). A compile-time -assertion catches this and warns about the problem. - -Replacing shared pointers with :ref:`intrusive reference counting -` fixes this limitations. +enable_shared_from_this +^^^^^^^^^^^^^^^^^^^^^^^ + +The C++ standard library class ``std::enable_shared_from_this`` +allows an object that inherits from it to locate an existing +``std::shared_ptr`` that manages that object. nanobind supports +types that inherit from ``enable_shared_from_this``, with some caveats +described in this section. + +Background (not nanobind-specific): Suppose a type ``ST`` inherits +from ``std::enable_shared_from_this``. When a raw pointer ``ST +*obj`` or ``std::unique_ptr obj`` is wrapped in a shared pointer +using a constructor of the form ``std::shared_ptr(obj, ...)``, a +reference to the new ``shared_ptr``\'s control block is saved (as +``std::weak_ptr``) inside the object. This allows new +``shared_ptr``\s that share ownership with the existing one to be +obtained for the same object using ``obj->shared_from_this()`` or +``obj->weak_from_this()``. + +nanobind's support for ``std::enable_shared_from_this`` consists of three +behaviors: + +* If a raw pointer ``ST *obj`` is returned from C++ to Python, and + there already exists an associated ``std::shared_ptr`` which + ``obj->shared_from_this()`` can locate, then nanobind will produce a + Python instance that shares ownership with it. The behavior is + identical to what would happen if the C++ code did ``return + obj->shared_from_this();`` (returning an explicit + ``std::shared_ptr`` to Python) rather than ``return obj;``. + The return value policy has limited effect in this case; you will get + shared ownership on the Python side regardless of whether you used + `rv_policy::take_ownership` or `rv_policy::reference`. + (`rv_policy::copy` and `rv_policy::move` will still create a new + object that has no ongoing relationship to the returned pointer.) + + * Note that this behavior occurs only if such a ``std::shared_ptr`` + already exists! If not, then nanobind behaves as it would without + ``enable_shared_from_this``: a raw pointer will transfer exclusive + ownership to Python by default, or will create a non-owning reference + if you use `rv_policy::reference`. + +* If a Python object is passed to C++ as ``std::shared_ptr obj``, + and there already exists an associated ``std::shared_ptr`` which + ``obj->shared_from_this()`` can locate, then nanobind will produce a + ``std::shared_ptr`` that shares ownership with it: an additional + reference to the same control block, rather than a new control block + (as would occur without ``enable_shared_from_this``). This improves + performance and makes the result of ``shared_ptr::use_count()`` more + accurate. + +* If a Python object is passed to C++ as ``std::shared_ptr obj``, and + there is no associated ``std::shared_ptr`` that + ``obj->shared_from_this()`` can locate, then nanobind will produce + a ``std::shared_ptr`` as usual (with a new control block whose deleter + drops a Python object reference), *and* will do so in a way that enables + future calls to ``obj->shared_from_this()`` to find it as long + as any ``shared_ptr`` that shares this control block is still alive on + the C++ side. + + (Once all of the ``std::shared_ptr``\s that share this control block + have been destroyed, the underlying PyObject reference being + managed by the ``shared_ptr`` deleter will be dropped, + and ``shared_from_this()`` will stop working. It can be reenabled by + passing the Python object back to C++ as ``std::shared_ptr`` once more, + which will create another control block.) + +Bindings for a class that supports ``enable_shared_from_this`` will be +slightly larger than bindings for a class that doesn't, as nanobind +must produce type-specific code to implement the above behaviors. + +.. warning:: The ``shared_from_this()`` method will only work when there + is actually a ``std::shared_ptr`` managing the object. A nanobind + instance constructed from Python will not have an associated + ``std::shared_ptr`` yet, so ``shared_from_this()`` will throw an + exception if you pass such an instance to C++ using a reference or + raw pointer. ``shared_from_this()`` will only work when there exists + a corresponding live ``std::shared_ptr`` on the C++ side. + + The only situation where nanobind will create the first + ``std::shared_ptr`` for an object (thus enabling + ``shared_from_this()``), even with ``enable_shared_from_this``, is + when a Python instance is passed to C++ as the explicit type + ``std::shared_ptr``. If you don't do this, or if no such + ``std::shared_ptr`` is still alive, then ``shared_from_this()`` will + throw an exception. It also works to create the ``std::shared_ptr`` + on the C++ side, such as by using a factory function which always + uses ``std::make_shared(...)`` to construct the object, and + returns the resulting ``std::shared_ptr`` to Python. + +There is no way to enable ``shared_from_this`` immediately upon +regular Python-side object construction (i.e., ``SomeType(*args)`` +rather than ``SomeType.some_fn(*args)``). If this limitation creates +a problem for your application, you might get better results by using +:ref:`intrusive reference counting ` instead. + +.. warning:: C++ code that receives a raw pointer ``T *obj`` *must not* + assume that it has exclusive ownership of ``obj``, or even that + ``obj`` is allocated on the C++ heap (via ``operator new``); + ``obj`` might instead be a subobject of a nanobind instance + allocated from Python. This applies even if ``T`` supports + ``shared_from_this()`` and there is no associated + ``std::shared_ptr``. Lack of a ``shared_ptr`` does *not* imply + exclusive ownership; it just means there's no way to share ownership + with whoever the current owner is. .. _unique_ptr_adv: diff --git a/docs/porting.rst b/docs/porting.rst index 8d5dd72f..cefbf97c 100644 --- a/docs/porting.rst +++ b/docs/porting.rst @@ -132,16 +132,22 @@ both of the following include directives to your code: .. code-block:: cpp #include - #include + #include Binding functions that take ``std::unique_ptr`` arguments involves some limitations that can be avoided by changing their signatures to ``std::unique_ptr>`` (:ref:`details `). -Usage of ``std::enable_shared_from_this`` is **prohibited** and will raise a -compile-time assertion (:ref:`details `) . This is -consistent with the philosophy of this library: *the codebase has to adapt to -the binding tool and not the other way around*. +Use of ``std::enable_shared_from_this`` is permitted, but since +nanobind does not use holder types, an object +constructed in Python will typically not have any associated +``std::shared_ptr`` until it is passed to a C++ function that +accepts ``std::shared_ptr``. That means a C++ function that accepts +a raw ``T*`` and calls ``shared_from_this()`` on it might stop working +when ported from pybind11 to nanobind. You can solve this problem +by always passing such objects across the Python/C++ boundary as +``std::shared_ptr`` rather than as ``T*``. See the :ref:`advanced section +on object ownership ` for more details. Custom constructors ------------------- diff --git a/include/nanobind/nb_class.h b/include/nanobind/nb_class.h index 4120f43f..2540990f 100644 --- a/include/nanobind/nb_class.h +++ b/include/nanobind/nb_class.h @@ -48,7 +48,11 @@ enum class type_flags : uint32_t { /// Is this a trampoline class meant to be overloaded in Python? is_trampoline = (1 << 12), - // Six more flag bits available (13 through 18) without needing + /// Is this a class that inherits from enable_shared_from_this? + /// If so, type_data::keep_shared_from_this_alive is also set. + has_shared_from_this = (1 << 13), + + // Five more flag bits available (14 through 18) without needing // a larger reorganization }; @@ -90,6 +94,7 @@ struct type_data { const std::type_info **implicit; bool (**implicit_py)(PyTypeObject *, PyObject *, cleanup_list *) noexcept; void (*set_self_py)(void *, PyObject *) noexcept; + bool (*keep_shared_from_this_alive)(PyObject *) noexcept; #if defined(Py_LIMITED_API) size_t dictoffset; #endif @@ -386,6 +391,24 @@ class class_ : public object { } } + if constexpr (detail::has_shared_from_this_v) { + d.flags |= (uint32_t) detail::type_flags::has_shared_from_this; + d.keep_shared_from_this_alive = [](PyObject *self) noexcept { + // weak_from_this().lock() is equivalent to shared_from_this(), + // except that it returns an empty shared_ptr instead of + // throwing an exception if there is no active shared_ptr + // for this object. (Added in C++17.) + if (auto sp = inst_ptr(self)->weak_from_this().lock()) { + detail::keep_alive(self, new auto(std::move(sp)), + [](void *p) noexcept { + delete (decltype(sp) *) p; + }); + return true; + } + return false; + }; + } + (detail::type_extra_apply(d, extra), ...); m_ptr = detail::nb_type_new(&d); diff --git a/include/nanobind/nb_traits.h b/include/nanobind/nb_traits.h index 66ea2105..e950f1c9 100644 --- a/include/nanobind/nb_traits.h +++ b/include/nanobind/nb_traits.h @@ -132,6 +132,16 @@ struct detector>, Op, Arg> avoid redundancy when combined with nb::arg(...).none(). */ template struct remove_opt_mono { using type = T; }; +// Detect std::enable_shared_from_this without including +template +auto has_shared_from_this_impl(T *ptr) -> + decltype(ptr->weak_from_this().lock().get(), std::true_type{}); +std::false_type has_shared_from_this_impl(...); + +template +constexpr bool has_shared_from_this_v = + decltype(has_shared_from_this_impl((T *) nullptr))::value; + NAMESPACE_END(detail) template diff --git a/include/nanobind/stl/shared_ptr.h b/include/nanobind/stl/shared_ptr.h index 4e86e09c..8831c3b3 100644 --- a/include/nanobind/stl/shared_ptr.h +++ b/include/nanobind/stl/shared_ptr.h @@ -15,33 +15,40 @@ NAMESPACE_BEGIN(NB_NAMESPACE) NAMESPACE_BEGIN(detail) +// shared_ptr deleter that reduces the reference count of a Python object +struct py_deleter { + void operator()(void *) noexcept { + // Don't run the deleter if the interpreter has been shut down + if (!Py_IsInitialized()) + return; + gil_scoped_acquire guard; + Py_DECREF(o); + } + + PyObject *o; +}; + /** - * Create a generic std::shared_ptr to evade population of a potential - * std::enable_shared_from_this weak pointer. The specified deleter reduces the - * reference count of the Python object. + * Create a std::shared_ptr for `ptr` that owns a reference to the Python + * object `h`; if `ptr` is non-null, then the refcount of `h` is incremented + * before creating the shared_ptr and decremented by its deleter. + * + * Usually this is instantiated with T = void, to reduce template bloat. + * But if the pointee type uses enable_shared_from_this, we instantiate + * with T = that type, in order to allow its internal weak_ptr to share + * ownership with the shared_ptr we're creating. * * The next two functions are simultaneously marked as 'inline' (to avoid * linker errors) and 'NB_NOINLINE' (to avoid them being inlined into every * single shared_ptr type_caster, which would enlarge the binding size) */ -inline NB_NOINLINE std::shared_ptr -shared_from_python(void *ptr, handle h) noexcept { - struct py_deleter { - void operator()(void *) noexcept { - // Don't run the deleter if the interpreter has been shut down - if (!Py_IsInitialized()) - return; - gil_scoped_acquire guard; - Py_DECREF(o); - } - - PyObject *o; - }; - +template +inline NB_NOINLINE std::shared_ptr +shared_from_python(T *ptr, handle h) noexcept { if (ptr) - return std::shared_ptr(ptr, py_deleter{ h.inc_ref().ptr() }); + return std::shared_ptr(ptr, py_deleter{ h.inc_ref().ptr() }); else - return std::shared_ptr((PyObject *) nullptr); + return std::shared_ptr(nullptr); } inline NB_NOINLINE void shared_from_cpp(std::shared_ptr &&ptr, @@ -50,14 +57,6 @@ inline NB_NOINLINE void shared_from_cpp(std::shared_ptr &&ptr, [](void *p) noexcept { delete (std::shared_ptr *) p; }); } -template -struct uses_shared_from_this : std::false_type { }; - -template -struct uses_shared_from_this< - T, std::void_t().shared_from_this())>> - : std::true_type { }; - template struct type_caster> { using Value = std::shared_ptr; using Caster = make_caster; @@ -65,11 +64,6 @@ template struct type_caster> { "Binding 'shared_ptr' requires that 'T' can also be bound " "by nanobind. It appears that you specified a type which " "would undergo conversion/copying, which is not allowed."); - static_assert(!uses_shared_from_this::value, - "nanobind does not permit use of std::shared_from_this, " - "which can cause undefined behavior. (Refer to " - "https://nanobind.readthedocs.io/en/latest/ownership.html " - "for details.)"); static constexpr auto Name = Caster::Name; static constexpr bool IsClass = true; @@ -84,9 +78,23 @@ template struct type_caster> { if (!caster.from_python(src, flags, cleanup)) return false; - value = std::static_pointer_cast( - shared_from_python(caster.operator T *(), src)); - + T *ptr = caster.operator T *(); + if constexpr (has_shared_from_this_v) { + if (ptr) { + if (auto sp = ptr->weak_from_this().lock()) { + // There is already a C++ shared_ptr for this object. Use it. + value = std::static_pointer_cast(std::move(sp)); + return true; + } + } + // Otherwise create a new one. Use shared_from_python(...) + // so that future calls to ptr->shared_from_this() can share + // ownership with it. + value = shared_from_python(ptr, src); + } else { + value = std::static_pointer_cast( + shared_from_python(static_cast(ptr), src)); + } return true; } diff --git a/src/nb_type.cpp b/src/nb_type.cpp index 055c189b..bc440638 100644 --- a/src/nb_type.cpp +++ b/src/nb_type.cpp @@ -660,7 +660,8 @@ PyObject *nb_type_new(const type_init_data *t) noexcept { has_type_slots = t->flags & (uint32_t) type_init_flags::has_type_slots, has_supplement = t->flags & (uint32_t) type_init_flags::has_supplement, has_dynamic_attr = t->flags & (uint32_t) type_flags::has_dynamic_attr, - intrusive_ptr = t->flags & (uint32_t) type_flags::intrusive_ptr; + intrusive_ptr = t->flags & (uint32_t) type_flags::intrusive_ptr, + has_shared_from_this = t->flags & (uint32_t) type_flags::has_shared_from_this; nb_internals &internals = internals_get(); str name(t->name), qualname = name; @@ -824,6 +825,12 @@ PyObject *nb_type_new(const type_init_data *t) noexcept { to->set_self_py = tb->set_self_py; } + if (!has_shared_from_this && tb && + (tb->flags & (uint32_t) type_flags::has_shared_from_this)) { + to->flags |= (uint32_t) type_flags::has_shared_from_this; + to->keep_shared_from_this_alive = tb->keep_shared_from_this_alive; + } + to->name = name_copy; to->type_py = (PyTypeObject *) result; @@ -1112,9 +1119,6 @@ static PyObject *nb_type_put_common(void *value, type_data *t, rv_policy rvp, if (!inst) return nullptr; - if (is_new) - *is_new = true; - void *new_value = inst_ptr(inst); if (rvp == rv_policy::move) { if (t->flags & (uint32_t) type_flags::is_move_constructible) { @@ -1156,6 +1160,18 @@ static PyObject *nb_type_put_common(void *value, type_data *t, rv_policy rvp, } } + // If we can find an existing C++ shared_ptr for this object, and + // the instance we're creating just holds a pointer, then take out + // another C++ shared_ptr that shares ownership with the existing + // one, and tie its lifetime to the Python object. This is the + // same thing done by the caster when + // returning shared_ptr to Python explicitly. + if ((t->flags & (uint32_t) type_flags::has_shared_from_this) && + !store_in_obj && t->keep_shared_from_this_alive((PyObject *) inst)) + rvp = rv_policy::reference; + else if (is_new) + *is_new = true; + inst->destruct = rvp != rv_policy::reference && rvp != rv_policy::reference_internal; inst->cpp_delete = rvp == rv_policy::take_ownership; inst->ready = true; diff --git a/tests/test_holders.cpp b/tests/test_holders.cpp index 149ef12f..fa39ef4a 100644 --- a/tests/test_holders.cpp +++ b/tests/test_holders.cpp @@ -27,6 +27,24 @@ struct SharedWrapper { std::shared_ptr value; }; struct UniqueWrapper { std::unique_ptr value; }; struct UniqueWrapper2 { std::unique_ptr> value; }; +struct ExampleST : std::enable_shared_from_this { + int value; + ExampleST(int value) : value(value) { created++; } + ~ExampleST() { deleted++; } + + static ExampleST *make(int value) { return new ExampleST(value); } + static std::shared_ptr make_shared(int value) { + return std::make_shared(value); + } +}; +struct SharedWrapperST { + std::shared_ptr value; + ExampleST* get() const { return value.get(); } +}; + +static_assert(nb::detail::has_shared_from_this_v); +static_assert(!nb::detail::has_shared_from_this_v); + enum class PetKind { Cat, Dog }; struct Pet { const PetKind kind; }; struct Dog : Pet { Dog() : Pet{PetKind::Dog} { } }; @@ -68,6 +86,66 @@ NB_MODULE(test_holders_ext, m) { m.def("passthrough", [](std::shared_ptr shared) { return shared; }); + // ------- enable_shared_from_this ------- + + nb::class_(m, "ExampleST") + .def(nb::init()) + .def("has_shared_from_this", [](ExampleST& self) { + return !self.weak_from_this().expired(); + }) + .def("shared_from_this", [](ExampleST& self) { + return self.shared_from_this(); + }) + .def("use_count", [](ExampleST& self) { + return self.weak_from_this().use_count(); + }) + .def_rw("value", &ExampleST::value) + .def_static("make", &ExampleST::make) + .def_static("make_shared", &ExampleST::make_shared); + + struct DerivedST : ExampleST { + using ExampleST::ExampleST; + }; + static_assert(nb::detail::has_shared_from_this_v); + nb::class_(m, "DerivedST") + .def(nb::init()) + .def_static("make", [](int v) { + return static_cast(ExampleST::make(v)); + }) + .def_static("make_shared", [](int v) { + return std::static_pointer_cast(ExampleST::make_shared(v)); + }); + + nb::class_(m, "SharedWrapperST") + .def(nb::init>()) + .def_static("from_existing", [](ExampleST *obj) { + return SharedWrapperST{obj->shared_from_this()}; + }) + .def_static("from_wrapper", [](SharedWrapperST& w) { + return SharedWrapperST{w.value}; + }) + .def("use_count", [](SharedWrapperST& self) { + return self.value.use_count(); + }) + .def("same_owner", [](SharedWrapperST& self, ExampleST& other) { + auto self_s = self.value; + auto other_s = other.shared_from_this(); + return !self_s.owner_before(other_s) && + !other_s.owner_before(self_s); + }) + .def("get_own", &SharedWrapperST::get) + .def("get_ref", &SharedWrapperST::get, nb::rv_policy::reference) + .def_rw("ptr", &SharedWrapperST::value) + .def_prop_rw("value", + [](SharedWrapperST &t) { return t.value->value; }, + [](SharedWrapperST &t, int value) { t.value->value = value; }); + + m.def("owns_cpp", [](nb::handle h) { return nb::inst_state(h).second; }); + m.def("same_owner", [](const SharedWrapperST& a, + const SharedWrapperST& b) { + return !a.value.owner_before(b.value) && !b.value.owner_before(a.value); + }); + // ------- unique_ptr ------- m.def("unique_from_cpp", diff --git a/tests/test_holders.py b/tests/test_holders.py index b7feff72..00c76f60 100644 --- a/tests/test_holders.py +++ b/tests/test_holders.py @@ -1,3 +1,4 @@ +import sys import test_holders_ext as t import pytest from common import collect @@ -176,3 +177,171 @@ def test09_tag_based_unique(): def test09_tag_based_shared(): assert isinstance(t.make_pet_s(t.PetKind.Dog), t.Dog) assert isinstance(t.make_pet_s(t.PetKind.Cat), t.Cat) + + +def check_shared_from_this_py_owned(ty, factory, value): + e = ty(value) + + # Creating from Python does not enable shared_from_this + assert e.value == value + assert not e.has_shared_from_this() + assert t.owns_cpp(e) + + # Passing to C++ as a shared_ptr does + w = t.SharedWrapperST(e) + assert e.has_shared_from_this() + assert w.ptr is e + + # Execute shared_from_this on the C++ side + w2 = t.SharedWrapperST.from_existing(e) + assert e.use_count() == 2 + assert w.value == w2.value == e.value == value + assert t.same_owner(w, w2) + + # Returning a raw pointer from C++ locates the existing instance + assert w2.get_own() is w2.get_ref() is e + assert t.owns_cpp(e) + + if hasattr(sys, "getrefcount"): + # One reference is held by the C++ shared_ptr, one by our + # locals dict, and one by the arg to getrefcount + rc = sys.getrefcount(e) + assert rc == 3 + + # Dropping the Python object does not actually destroy it, because + # the shared_ptr holds a reference. There is still a PyObject* at + # the same address. + prev_id = id(e) + del e + collect() + assert t.stats() == (1, 0) + assert id(w.get_ref()) == prev_id + assert t.owns_cpp(w.get_ref()) + assert type(w.get_ref()) is ty + + # Dropping the wrappers' shared_ptrs drops the PyObject reference and + # the object is finally GC'ed + del w, w2 + collect() + assert t.stats() == (1, 1) + + +def test10_shared_from_this_create_in_python(clean): + check_shared_from_this_py_owned(t.ExampleST, t.ExampleST, 42) + + # Subclass in C++ + t.reset() + check_shared_from_this_py_owned(t.DerivedST, t.DerivedST, 30) + + # Subclass in Python + class SubST(t.ExampleST): + pass + + t.reset() + check_shared_from_this_py_owned(SubST, SubST, 20) + + +def test11_shared_from_this_create_raw_in_cpp(clean): + # Creating a raw pointer from C++ does not enable shared_from_this; + # although the object is held by pointer rather than value, the logical + # ownership transfers to Python and the behavior is equivalent to test10. + # Once we get a shared_ptr it owns a reference to the Python object. + check_shared_from_this_py_owned(t.ExampleST, t.ExampleST.make, 10) + + # Subclass in C++ + t.reset() + check_shared_from_this_py_owned(t.DerivedST, t.DerivedST.make, 5) + + +def test12_shared_from_this_create_shared_in_cpp(clean): + # Creating a shared_ptr from C++ enables shared_from_this. Now the + # shared_ptr does not keep the Python object alive; it's directly + # owning the ExampleST object on the C++ side. + e = t.ExampleST.make_shared(10) + assert e.value == 10 + assert e.has_shared_from_this() + assert e.shared_from_this() is e # same instance + assert e.use_count() == 1 + assert not t.owns_cpp(e) + if hasattr(sys, "getrefcount"): + # One reference is held by our locals dict and one by the + # arg to getrefcount + rc = sys.getrefcount(e) + assert rc == 2 + + w = t.SharedWrapperST.from_existing(e) + assert w.ptr is e + # One shared_ptr whose lifetime is tied to e. And one inside the wrapper + assert e.use_count() == 2 + + # Drop the Python object; C++ object still remains owned by the wrapper + del e + collect() + assert t.stats() == (1, 0) + assert w.use_count() == 1 + + # Get a new Python object reference; it will share ownership of the + # same underlying C++ object + e2 = w.get_own() + assert not t.owns_cpp(e2) + assert w.ptr is e2 + assert w.use_count() == 2 + + del e2 + collect() + assert t.stats() == (1, 0) + assert w.use_count() == 1 + + # Get a new C++-side reference + w2 = t.SharedWrapperST.from_wrapper(w) + assert w2.use_count() == 2 + assert t.same_owner(w, w2) + + # Get another one by roundtripping through Python. + # The nanobind conversion to shared_ptr should use the + # existing shared_from_this shared_ptr + w3 = t.SharedWrapperST(w.ptr) + collect() # on pypy the w.ptr temporary can stay alive + assert w3.use_count() == 3 + assert t.same_owner(w2, w3) + + # Destroy everything + assert t.stats() == (1, 0) + del w, w2, w3 + collect() + assert t.stats() == (1, 1) + + +def test13_shared_from_this_create_derived_in_cpp(clean): + # This tests that keep_shared_from_this_alive is inherited by + # derived classes properly + + # Pass shared_ptr to Python + e = t.DerivedST.make_shared(20) + assert type(e) is t.DerivedST + assert e.value == 20 + assert e.has_shared_from_this() + assert not t.owns_cpp(e) + assert e.use_count() == 1 + + # Pass it back to C++ + w = t.SharedWrapperST(e) + assert e.use_count() == w.use_count() == 2 + + del e + collect() + assert t.stats() == (1, 0) + assert w.use_count() == 1 + + # Pass it back to Python as a raw pointer + e = w.get_own() + # ExampleST is not polymorphic, so the derived-class identity is + # lost once the Python instance is destroyed + assert type(e) is t.ExampleST + assert not t.owns_cpp(e) + assert w.use_count() == 2 + assert w.get_own() is e + + del e, w + collect() + assert t.stats() == (1, 1)