diff --git a/.codespell-ignore-lines b/.codespell-ignore-lines index 2a01d63ebb..e8cbf31447 100644 --- a/.codespell-ignore-lines +++ b/.codespell-ignore-lines @@ -12,6 +12,17 @@ template template class_ &def(const detail::op_ &op, const Extra &...extra) { class_ &def_cast(const detail::op_ &op, const Extra &...extra) { + int valu; + explicit movable_int(int v) : valu{v} {} + movable_int(movable_int &&other) noexcept : valu(other.valu) { other.valu = 91; } + explicit indestructible_int(int v) : valu{v} {} + REQUIRE(hld.as_raw_ptr_unowned()->valu == 19); + REQUIRE(othr.valu == 19); + REQUIRE(orig.valu == 91); + (m.pass_valu, "Valu", "pass_valu:Valu(_MvCtor)*_CpCtor"), +atyp_valu rtrn_valu() { atyp_valu obj{"Valu"}; return obj; } + assert m.atyp_valu().get_mtxt() == "Valu" +// valu(e), ref(erence), ptr or p (pointer), r = rvalue, m = mutable, c = const, @pytest.mark.parametrize("access", ["ro", "rw", "static_ro", "static_rw"]) struct IntStruct { explicit IntStruct(int v) : value(v){}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33e281db73..80f1f0d74b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,20 @@ jobs: # Extra ubuntu latest job - runs-on: ubuntu-latest python: '3.11' + # Run tests with py::smart_holder as the default holder + # with recent (or ideally latest) released Python version. + - runs-on: ubuntu-latest + python: '3.12' + args: > + -DCMAKE_CXX_FLAGS="-DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE" + - runs-on: macos-13 + python: '3.12' + args: > + -DCMAKE_CXX_FLAGS="-DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE" + - runs-on: windows-2022 + python: '3.12' + args: > + -DCMAKE_CXX_FLAGS="/DPYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE /GR /EHsc" exclude: # The setup-python action currently doesn't have graalpy for windows - python: 'graalpy-24.1' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1abc48072a..fbecbc0aef 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -76,6 +76,7 @@ repos: - id: mixed-line-ending - id: requirements-txt-fixer - id: trailing-whitespace + exclude: \.patch?$ # Also code format the docs - repo: /~https://github.com/adamchainz/blacken-docs @@ -90,6 +91,7 @@ repos: rev: "v1.5.5" hooks: - id: remove-tabs + exclude: (^docs/.*|\.patch)?$ # Avoid directional quotes - repo: /~https://github.com/sirosen/texthooks diff --git a/CMakeLists.txt b/CMakeLists.txt index 9357f31fbd..8783dba6e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,10 +131,13 @@ set(PYBIND11_HEADERS include/pybind11/detail/common.h include/pybind11/detail/cpp_conduit.h include/pybind11/detail/descr.h + include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h include/pybind11/detail/init.h include/pybind11/detail/internals.h + include/pybind11/detail/struct_smart_holder.h include/pybind11/detail/type_caster_base.h include/pybind11/detail/typeid.h + include/pybind11/detail/using_smart_holder.h include/pybind11/detail/value_and_holder.h include/pybind11/detail/exception_translation.h include/pybind11/attr.h @@ -164,6 +167,7 @@ set(PYBIND11_HEADERS include/pybind11/stl.h include/pybind11/stl_bind.h include/pybind11/stl/filesystem.h + include/pybind11/trampoline_self_life_support.h include/pybind11/type_caster_pyobject_ptr.h include/pybind11/typing.h include/pybind11/warnings.h) diff --git a/README.rst b/README.rst index 8e0c391770..493d111634 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ .. figure:: /~https://github.com/pybind/pybind11/raw/master/docs/pybind11-logo.png :alt: pybind11 logo -**pybind11 — Seamless operability between C++11 and Python** +**pybind11 (v3) — Seamless interoperability between C++ and Python** |Latest Documentation Status| |Stable Documentation Status| |Gitter chat| |GitHub Discussions| |CI| |Build status| diff --git a/docs/advanced/cast/stl.rst b/docs/advanced/cast/stl.rst index 42b85532d8..1e17bc389c 100644 --- a/docs/advanced/cast/stl.rst +++ b/docs/advanced/cast/stl.rst @@ -169,8 +169,8 @@ macro must be specified at the top level (and outside of any namespaces), since it adds a template instantiation of ``type_caster``. If your binding code consists of multiple compilation units, it must be present in every file (typically via a common header) preceding any usage of ``std::vector``. Opaque types must -also have a corresponding ``class_`` declaration to associate them with a name -in Python, and to define a set of available operations, e.g.: +also have a corresponding ``py::class_`` declaration to associate them with a +name in Python, and to define a set of available operations, e.g.: .. code-block:: cpp diff --git a/docs/advanced/classes.rst b/docs/advanced/classes.rst index 1e221072f2..1a1648d338 100644 --- a/docs/advanced/classes.rst +++ b/docs/advanced/classes.rst @@ -80,6 +80,17 @@ helper class that is defined as follows: } }; +The ``py::trampoline_self_life_support`` base class is needed to ensure +that a ``std::unique_ptr`` can safely be passed between Python and C++. To +steer clear of notorious pitfalls (e.g. inheritance slicing), it is best +practice to always use the base class, in combination with +``py::smart_holder``. + +.. note:: + For completeness, the base class has no effect if a holder other than + ``py::smart_holder`` used, including the default ``std::unique_ptr``. + Please think twice, though, the pitfalls are very real. + The macro :c:macro:`PYBIND11_OVERRIDE_PURE` should be used for pure virtual functions, and :c:macro:`PYBIND11_OVERRIDE` should be used for functions which have a default implementation. There are also two alternate macros @@ -106,7 +117,7 @@ The binding code also needs a few minor adaptations (highlighted): } Importantly, pybind11 is made aware of the trampoline helper class by -specifying it as an extra template argument to :class:`class_`. (This can also +specifying it as an extra template argument to ``py::class_``. (This can also be combined with other template arguments such as a custom holder type; the order of template types does not matter). Following this, we are able to define a constructor as usual. @@ -118,13 +129,39 @@ Bindings should be made against the actual class, not the trampoline helper clas py::class_(m, "Animal"); .def(py::init<>()) - .def("go", &PyAnimal::go); /* <--- THIS IS WRONG, use &Animal::go */ + .def("go", &Animal::go); /* <--- DO NOT USE &PyAnimal::go HERE */ Note, however, that the above is sufficient for allowing python classes to extend ``Animal``, but not ``Dog``: see :ref:`virtual_and_inheritance` for the necessary steps required to providing proper overriding support for inherited classes. +To enable safely passing a ``std::unique_ptr`` to a trampoline object between +Python and C++, + +1. the C++ type (``Animal`` above) must be wrapped with + ``py::class_<..., py::smart_holder>`` (see :ref:`smart_holder`), and + +2. the trampoline helper class must inherit from + ``py::trampoline_self_life_support``. + +I.e. the example above needs these two changes: + +.. code-block:: cpp + + class PyAnimal : public Animal, public py::trampoline_self_life_support { + ... + }; + +.. code-block:: cpp + + py::class_(m, "Animal"); + +.. seealso:: + + A fairly minimal but complete example is in + :file:`tests/test_class_sh_trampoline_unique_ptr.cpp`. + The Python session below shows how to override ``Animal::go`` and invoke it via a virtual method call. @@ -550,9 +587,10 @@ pybind11. The underlying issue is that the ``std::unique_ptr`` holder type that is responsible for managing the lifetime of instances will reference the destructor even if no deallocations ever take place. In order to expose classes with private or protected destructors, it is possible to override the holder -type via a holder type argument to ``class_``. Pybind11 provides a helper class -``py::nodelete`` that disables any destructor invocations. In this case, it is -crucial that instances are deallocated on the C++ side to avoid memory leaks. +type via a holder type argument to ``py::class_``. Pybind11 provides a helper +class ``py::nodelete`` that disables any destructor invocations. In this case, +it is crucial that instances are deallocated on the C++ side to avoid memory +leaks. .. code-block:: cpp @@ -871,7 +909,7 @@ Multiple Inheritance pybind11 can create bindings for types that derive from multiple base types (aka. *multiple inheritance*). To do so, specify all bases in the template -arguments of the ``class_`` declaration: +arguments of the ``py::class_`` declaration: .. code-block:: cpp @@ -946,11 +984,11 @@ because of conflicting definitions on the external type: // dogs.cpp // Binding for external library class: - py::class(m, "Pet") + py::class_(m, "Pet") .def("name", &pets::Pet::name); // Binding for local extension class: - py::class(m, "Dog") + py::class_(m, "Dog") .def(py::init()); .. code-block:: cpp @@ -958,11 +996,11 @@ because of conflicting definitions on the external type: // cats.cpp, in a completely separate project from the above dogs.cpp. // Binding for external library class: - py::class(m, "Pet") + py::class_(m, "Pet") .def("get_name", &pets::Pet::name); // Binding for local extending class: - py::class(m, "Cat") + py::class_(m, "Cat") .def(py::init()); .. code-block:: pycon @@ -980,13 +1018,13 @@ the ``py::class_`` constructor: .. code-block:: cpp // Pet binding in dogs.cpp: - py::class(m, "Pet", py::module_local()) + py::class_(m, "Pet", py::module_local()) .def("name", &pets::Pet::name); .. code-block:: cpp // Pet binding in cats.cpp: - py::class(m, "Pet", py::module_local()) + py::class_(m, "Pet", py::module_local()) .def("get_name", &pets::Pet::name); This makes the Python-side ``dogs.Pet`` and ``cats.Pet`` into distinct classes, @@ -1195,7 +1233,7 @@ but once again each instantiation must be explicitly specified: T fn(V v); }; - py::class>(m, "MyClassT") + py::class_>(m, "MyClassT") .def("fn", &MyClass::fn); Custom automatic downcasters diff --git a/docs/advanced/misc.rst b/docs/advanced/misc.rst index a256da54a9..a0438f0330 100644 --- a/docs/advanced/misc.rst +++ b/docs/advanced/misc.rst @@ -188,7 +188,7 @@ from Section :ref:`inheritance`. Suppose now that ``Pet`` bindings are defined in a module named ``basic``, whereas the ``Dog`` bindings are defined somewhere else. The challenge is of course that the variable ``pet`` is not available anymore though it is needed -to indicate the inheritance relationship to the constructor of ``class_``. +to indicate the inheritance relationship to the constructor of ``py::class_``. However, it can be acquired as follows: .. code-block:: cpp @@ -200,7 +200,7 @@ However, it can be acquired as follows: .def("bark", &Dog::bark); Alternatively, you can specify the base class as a template parameter option to -``class_``, which performs an automated lookup of the corresponding Python +``py::class_``, which performs an automated lookup of the corresponding Python type. Like the above code, however, this also requires invoking the ``import`` function once to ensure that the pybind11 binding code of the module ``basic`` has been executed: diff --git a/docs/advanced/smart_ptrs.rst b/docs/advanced/smart_ptrs.rst index b9f100cf8e..599f384db0 100644 --- a/docs/advanced/smart_ptrs.rst +++ b/docs/advanced/smart_ptrs.rst @@ -1,126 +1,123 @@ -Smart pointers -############## +Smart pointers & ``py::class_`` +############################### -std::unique_ptr -=============== +The binding generator for classes, ``py::class_``, can be passed a template +type that denotes a special *holder* type that is used to manage references to +the object. If no such holder type template argument is given, the default for +a type ``T`` is ``std::unique_ptr``. -Given a class ``Example`` with Python bindings, it's possible to return -instances wrapped in C++11 unique pointers, like so +.. note:: -.. code-block:: cpp + A ``py::class_`` for a given C++ type ``T`` — and all its derived types — + can only use a single holder type. - std::unique_ptr create_example() { return std::unique_ptr(new Example()); } -.. code-block:: cpp +.. _smart_holder: - m.def("create_example", &create_example); +``py::smart_holder`` +==================== -In other words, there is nothing special that needs to be done. While returning -unique pointers in this way is allowed, it is *illegal* to use them as function -arguments. For instance, the following function signature cannot be processed -by pybind11. +Starting with pybind11v3, ``py::smart_holder`` is built into pybind11. It is +the recommended ``py::class_`` holder for most situations. However, for +backward compatibility it is **not** the default holder, and there are no +plans to make it the default holder in the future. -.. code-block:: cpp +It is extremely easy to use the safer and more versatile ``py::smart_holder``: +simply add ``py::smart_holder`` to ``py::class_``: - void do_something_with_example(std::unique_ptr ex) { ... } +* ``py::class_`` to -The above signature would imply that Python needs to give up ownership of an -object that is passed to this function, which is generally not possible (for -instance, the object might be referenced elsewhere). +* ``py::class_``. -std::shared_ptr -=============== +.. note:: -The binding generator for classes, :class:`class_`, can be passed a template -type that denotes a special *holder* type that is used to manage references to -the object. If no such holder type template argument is given, the default for -a type named ``Type`` is ``std::unique_ptr``, which means that the object -is deallocated when Python's reference count goes to zero. + A shorthand, ``py::classh``, is provided for + ``py::class_``. The ``h`` in ``py::classh`` stands + for **smart_holder** but is shortened for brevity, ensuring it has the + same number of characters as ``py::class_``. This design choice facilitates + easy experimentation with ``py::smart_holder`` without introducing + distracting whitespace noise in diffs. -It is possible to switch to other types of reference counting wrappers or smart -pointers, which is useful in codebases that rely on them. For instance, the -following snippet causes ``std::shared_ptr`` to be used instead. +The ``py::smart_holder`` functionality includes the following: -.. code-block:: cpp +* Support for **two-way** Python/C++ conversions for both + ``std::unique_ptr`` and ``std::shared_ptr`` **simultaneously**. + +* Passing a Python object back to C++ via ``std::unique_ptr``, safely + **disowning** the Python object. + +* Safely passing "trampoline" objects (objects with C++ virtual function + overrides implemented in Python, see :ref:`overriding_virtuals`) via + ``std::unique_ptr`` or ``std::shared_ptr`` back to C++: + associated Python objects are automatically kept alive for the lifetime + of the smart-pointer. + +* Full support for ``std::enable_shared_from_this`` (`cppreference + `_). - py::class_ /* <- holder type */> obj(m, "Example"); -Note that any particular class can only be associated with a single holder type. +``std::unique_ptr`` +=================== -One potential stumbling block when using holder types is that they need to be -applied consistently. Can you guess what's broken about the following binding -code? +This is the default ``py::class_`` holder and works as expected in +most situations. However, handling base-and-derived classes involves a +``reinterpret_cast``, which is, strictly speaking, undefined behavior. +Also note that the ``std::unique_ptr`` holder only supports passing a +``std::unique_ptr`` from C++ to Python, but not the other way around. +For example, the following code works as expected with ``py::class_``: .. code-block:: cpp - class Child { }; + std::unique_ptr create_example() { return std::unique_ptr(new Example()); } - class Parent { - public: - Parent() : child(std::make_shared()) { } - Child *get_child() { return child.get(); } /* Hint: ** DON'T DO THIS ** */ - private: - std::shared_ptr child; - }; +.. code-block:: cpp - PYBIND11_MODULE(example, m) { - py::class_>(m, "Child"); + m.def("create_example", &create_example); - py::class_>(m, "Parent") - .def(py::init<>()) - .def("get_child", &Parent::get_child); - } +However, this will fail with ``py::class_`` (but works with +``py::class_``): -The following Python code will cause undefined behavior (and likely a -segmentation fault). +.. code-block:: cpp -.. code-block:: python + void do_something_with_example(std::unique_ptr ex) { ... } - from example import Parent +.. note:: - print(Parent().get_child()) + The ``reinterpret_cast`` mentioned above is `here + `_. + For completeness: The same cast is also applied to ``py::smart_holder``, + but that is safe, because ``py::smart_holder`` is not templated. -The problem is that ``Parent::get_child()`` returns a pointer to an instance of -``Child``, but the fact that this instance is already managed by -``std::shared_ptr<...>`` is lost when passing raw pointers. In this case, -pybind11 will create a second independent ``std::shared_ptr<...>`` that also -claims ownership of the pointer. In the end, the object will be freed **twice** -since these shared pointers have no way of knowing about each other. -There are two ways to resolve this issue: +``std::shared_ptr`` +=================== -1. For types that are managed by a smart pointer class, never use raw pointers - in function arguments or return values. In other words: always consistently - wrap pointers into their designated holder types (such as - ``std::shared_ptr<...>``). In this case, the signature of ``get_child()`` - should be modified as follows: +It is possible to use ``std::shared_ptr`` as the holder, for example: .. code-block:: cpp - std::shared_ptr get_child() { return child; } + py::class_ /* <- holder type */>(m, "Example"); -2. Adjust the definition of ``Child`` by specifying - ``std::enable_shared_from_this`` (see cppreference_ for details) as a - base class. This adds a small bit of information to ``Child`` that allows - pybind11 to realize that there is already an existing - ``std::shared_ptr<...>`` and communicate with it. In this case, the - declaration of ``Child`` should look as follows: +Compared to using ``py::class_``, there are two noteworthy disadvantages: -.. _cppreference: http://en.cppreference.com/w/cpp/memory/enable_shared_from_this +* Because a ``py::class_`` for a given C++ type ``T`` can only use a + single holder type, ``std::unique_ptr`` cannot even be passed from C++ + to Python. This will become apparent only at runtime, often through a + segmentation fault. -.. code-block:: cpp +* Similar to the ``std::unique_ptr`` holder, the handling of base-and-derived + classes involves a ``reinterpret_cast`` that has strictly speaking undefined + behavior, although it works as expected in most situations. - class Child : public std::enable_shared_from_this { }; .. _smart_pointers: Custom smart pointers ===================== -pybind11 supports ``std::unique_ptr`` and ``std::shared_ptr`` right out of the -box. For any other custom smart pointer, transparent conversions can be enabled -using a macro invocation similar to the following. It must be declared at the -top namespace level before any binding code: +For custom smart pointers (e.g. ``c10::intrusive_ptr`` in pytorch), transparent +conversions can be enabled using a macro invocation similar to the following. +It must be declared at the top namespace level before any binding code: .. code-block:: cpp @@ -167,8 +164,70 @@ specialized: The above specialization informs pybind11 that the custom ``SmartPtr`` class provides ``.get()`` functionality via ``.getPointer()``. +.. note:: + + The two noteworthy disadvantages mentioned under the ``std::shared_ptr`` + section apply similarly to custom smart pointer holders, but there is no + established safe alternative in this case. + .. seealso:: The file :file:`tests/test_smart_ptr.cpp` contains a complete example that demonstrates how to work with custom reference-counting holder types in more detail. + + +Be careful not to accidentally undermine automatic lifetime management +====================================================================== + +``py::class_``-wrapped objects automatically manage the lifetime of the +wrapped C++ object, in collaboration with the chosen holder type. +When wrapping C++ functions involving raw pointers, care needs to be taken +to not inadvertently transfer ownership, resulting in multiple Python +objects acting as owners, causing heap-use-after-free or double-free errors. +For example: + +.. code-block:: cpp + + class Child { }; + + class Parent { + public: + Parent() : child(std::make_shared()) { } + Child *get_child() { return child.get(); } /* DANGER */ + private: + std::shared_ptr child; + }; + + PYBIND11_MODULE(example, m) { + py::class_>(m, "Child"); + + py::class_>(m, "Parent") + .def(py::init<>()) + .def("get_child", &Parent::get_child); /* PROBLEM */ + } + +The following Python code leads to undefined behavior, likely resulting in +a segmentation fault. + +.. code-block:: python + + from example import Parent + + print(Parent().get_child()) + +Part of the ``/* PROBLEM */`` here is that pybind11 falls back to using +``return_value_policy::take_ownership`` as the default (see +:ref:`return_value_policies`). The fact that the ``Child`` instance is +already managed by ``std::shared_ptr`` is lost. Therefore pybind11 +will create a second independent ``std::shared_ptr`` that also +claims ownership of the pointer, eventually leading to heap-use-after-free +or double-free errors. + +There are various ways to resolve this issue, either by changing +the ``Child`` or ``Parent`` C++ implementations (e.g. using +``std::enable_shared_from_this`` as a base class for +``Child``, or adding a member function to ``Parent`` that returns +``std::shared_ptr``), or if that is not feasible, by using +``return_value_policy::reference_internal``. What is the best approach +depends on the exact situation. diff --git a/docs/classes.rst b/docs/classes.rst index 4f2167dac1..5406668f0b 100644 --- a/docs/classes.rst +++ b/docs/classes.rst @@ -34,11 +34,18 @@ The binding code for ``Pet`` looks as follows: .def("getName", &Pet::getName); } -:class:`class_` creates bindings for a C++ *class* or *struct*-style data +``py::class_`` creates bindings for a C++ *class* or *struct*-style data structure. :func:`init` is a convenience function that takes the types of a constructor's parameters as template arguments and wraps the corresponding -constructor (see the :ref:`custom_constructors` section for details). An -interactive Python session demonstrating this example is shown below: +constructor (see the :ref:`custom_constructors` section for details). + +.. note:: + + Starting with pybind11v3, it is recommended to include `py::smart_holder` + in most situations for safety, especially if you plan to support conversions + to C++ smart pointers. See :ref:`smart_holder` for more information. + +An interactive Python session demonstrating this example is shown below: .. code-block:: pycon @@ -258,7 +265,7 @@ inheritance relationship: There are two different ways of indicating a hierarchical relationship to pybind11: the first specifies the C++ base class as an extra template -parameter of the :class:`class_`: +parameter of the ``py::class_``: .. code-block:: cpp @@ -272,7 +279,7 @@ parameter of the :class:`class_`: .def("bark", &Dog::bark); Alternatively, we can also assign a name to the previously bound ``Pet`` -:class:`class_` object and reference it when binding the ``Dog`` class: +``py::class_`` object and reference it when binding the ``Dog`` class: .. code-block:: cpp @@ -498,7 +505,7 @@ The binding code for this example looks as follows: To ensure that the nested types ``Kind`` and ``Attributes`` are created within the scope of ``Pet``, the -``pet`` :class:`class_` instance must be supplied to the :class:`enum_` and :class:`class_` +``pet`` ``py::class_`` instance must be supplied to the :class:`enum_` and ``py::class_`` constructor. The :func:`enum_::export_values` function exports the enum entries into the parent scope, which should be skipped for newer C++11-style strongly typed enums. diff --git a/docs/reference.rst b/docs/reference.rst index e64a03519d..c2757988d2 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -68,8 +68,8 @@ Convenience functions converting to Python types .. _extras: -Passing extra arguments to ``def`` or ``class_`` -================================================ +Passing extra arguments to ``def`` or ``py::class_`` +==================================================== .. doxygengroup:: annotations :members: diff --git a/include/pybind11/attr.h b/include/pybind11/attr.h index eb15ffd073..ef2ca1709c 100644 --- a/include/pybind11/attr.h +++ b/include/pybind11/attr.h @@ -276,8 +276,7 @@ struct function_record { struct type_record { PYBIND11_NOINLINE type_record() : multiple_inheritance(false), dynamic_attr(false), buffer_protocol(false), - default_holder(true), module_local(false), is_final(false), - release_gil_before_calling_cpp_dtor(false) {} + module_local(false), is_final(false), release_gil_before_calling_cpp_dtor(false) {} /// Handle to the parent scope handle scope; @@ -327,9 +326,6 @@ struct type_record { /// Does the class implement the buffer protocol? bool buffer_protocol : 1; - /// Is the default (unique_ptr) holder type used? - bool default_holder : 1; - /// Is the class definition local to the module shared object? bool module_local : 1; @@ -339,6 +335,8 @@ struct type_record { /// Solves pybind/pybind11#1446 bool release_gil_before_calling_cpp_dtor : 1; + holder_enum_t holder_enum_v = holder_enum_t::undefined; + PYBIND11_NOINLINE void add_base(const std::type_info &base, void *(*caster)(void *) ) { auto *base_info = detail::get_type_info(base, false); if (!base_info) { @@ -348,13 +346,17 @@ struct type_record { + "\" referenced unknown base type \"" + tname + "\""); } - if (default_holder != base_info->default_holder) { + // SMART_HOLDER_BAKEIN_FOLLOW_ON: Refine holder compatibility checks. + bool this_has_unique_ptr_holder = (holder_enum_v == holder_enum_t::std_unique_ptr); + bool base_has_unique_ptr_holder + = (base_info->holder_enum_v == holder_enum_t::std_unique_ptr); + if (this_has_unique_ptr_holder != base_has_unique_ptr_holder) { std::string tname(base.name()); detail::clean_type_id(tname); pybind11_fail("generic_type: type \"" + std::string(name) + "\" " - + (default_holder ? "does not have" : "has") + + (this_has_unique_ptr_holder ? "does not have" : "has") + " a non-default holder type while its base \"" + tname + "\" " - + (base_info->default_holder ? "does not" : "does")); + + (base_has_unique_ptr_holder ? "does not" : "does")); } bases.append((PyObject *) base_info->type); diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index f2c029113a..47575084c8 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -754,6 +754,7 @@ struct holder_helper { static auto get(const T &p) -> decltype(p.get()) { return p.get(); } }; +// SMART_HOLDER_BAKEIN_FOLLOW_ON: Rewrite comment, with reference to shared_ptr specialization. /// Type caster for holder types like std::shared_ptr, etc. /// The SFINAE hook is provided to help work around the current lack of support /// for smart-pointer interoperability. Please consider it an implementation @@ -789,7 +790,10 @@ struct copyable_holder_caster : public type_caster_base { protected: friend class type_caster_generic; void check_holder_compat() { - if (typeinfo->default_holder) { + // SMART_HOLDER_BAKEIN_FOLLOW_ON: Refine holder compatibility checks. + bool inst_has_unique_ptr_holder + = (typeinfo->holder_enum_v == holder_enum_t::std_unique_ptr); + if (inst_has_unique_ptr_holder) { throw cast_error("Unable to load a custom holder type from a default-holder instance"); } } @@ -835,10 +839,144 @@ struct copyable_holder_caster : public type_caster_base { holder_type holder; }; +template +struct copyable_holder_caster_shared_ptr_with_smart_holder_support_enabled : std::true_type {}; + +// SMART_HOLDER_BAKEIN_FOLLOW_ON: Refactor copyable_holder_caster to reduce code duplication. +template +struct copyable_holder_caster< + type, + std::shared_ptr, + enable_if_t::value>> + : public type_caster_base { +public: + using base = type_caster_base; + static_assert(std::is_base_of>::value, + "Holder classes are only supported for custom types"); + using base::base; + using base::cast; + using base::typeinfo; + using base::value; + + bool load(handle src, bool convert) { + if (base::template load_impl>>( + src, convert)) { + sh_load_helper.maybe_set_python_instance_is_alias(src); + return true; + } + return false; + } + + explicit operator std::shared_ptr *() { + if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + pybind11_fail("Passing `std::shared_ptr *` from Python to C++ is not supported " + "(inherently unsafe)."); + } + return std::addressof(shared_ptr_storage); + } + + explicit operator std::shared_ptr &() { + if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + shared_ptr_storage = sh_load_helper.load_as_shared_ptr(value); + } + return shared_ptr_storage; + } + + static handle + cast(const std::shared_ptr &src, return_value_policy policy, handle parent) { + const auto *ptr = src.get(); + auto st = type_caster_base::src_and_type(ptr); + if (st.second == nullptr) { + return handle(); // no type info: error will be set already + } + if (st.second->holder_enum_v == detail::holder_enum_t::smart_holder) { + return smart_holder_type_caster_support::smart_holder_from_shared_ptr( + src, policy, parent, st); + } + return type_caster_base::cast_holder(ptr, &src); + } + + // This function will succeed even if the `responsible_parent` does not own the + // wrapped C++ object directly. + // It is the responsibility of the caller to ensure that the `responsible_parent` + // has a `keep_alive` relationship with the owner of the wrapped C++ object, or + // that the wrapped C++ object lives for the duration of the process. + static std::shared_ptr shared_ptr_with_responsible_parent(handle responsible_parent) { + copyable_holder_caster loader; + loader.load(responsible_parent, /*convert=*/false); + assert(loader.typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder); + return loader.sh_load_helper.load_as_shared_ptr(loader.value, responsible_parent); + } + +protected: + friend class type_caster_generic; + void check_holder_compat() { + // SMART_HOLDER_BAKEIN_FOLLOW_ON: Refine holder compatibility checks. + bool inst_has_unique_ptr_holder + = (typeinfo->holder_enum_v == holder_enum_t::std_unique_ptr); + if (inst_has_unique_ptr_holder) { + throw cast_error("Unable to load a custom holder type from a default-holder instance"); + } + } + + void load_value(value_and_holder &&v_h) { + if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + sh_load_helper.loaded_v_h = v_h; + sh_load_helper.was_populated = true; + value = sh_load_helper.get_void_ptr_or_nullptr(); + return; + } + if (v_h.holder_constructed()) { + value = v_h.value_ptr(); + shared_ptr_storage = v_h.template holder>(); + return; + } + throw cast_error("Unable to cast from non-held to held instance (T& to Holder) " +#if !defined(PYBIND11_DETAILED_ERROR_MESSAGES) + "(#define PYBIND11_DETAILED_ERROR_MESSAGES or compile in debug mode for " + "type information)"); +#else + "of type '" + + type_id>() + "''"); +#endif + } + + template , + detail::enable_if_t::value, int> = 0> + bool try_implicit_casts(handle, bool) { + return false; + } + + template , + detail::enable_if_t::value, int> = 0> + bool try_implicit_casts(handle src, bool convert) { + for (auto &cast : typeinfo->implicit_casts) { + copyable_holder_caster sub_caster(*cast.first); + if (sub_caster.load(src, convert)) { + value = cast.second(sub_caster.value); + if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + sh_load_helper.loaded_v_h = sub_caster.sh_load_helper.loaded_v_h; + } else { + shared_ptr_storage + = std::shared_ptr(sub_caster.shared_ptr_storage, (type *) value); + } + return true; + } + } + return false; + } + + static bool try_direct_conversions(handle) { return false; } + + smart_holder_type_caster_support::load_helper> sh_load_helper; // Const2Mutbl + std::shared_ptr shared_ptr_storage; +}; + /// Specialize for the common std::shared_ptr, so users don't need to template class type_caster> : public copyable_holder_caster> {}; +// SMART_HOLDER_BAKEIN_FOLLOW_ON: Rewrite comment, with reference to unique_ptr specialization. /// Type caster for holder types like std::unique_ptr. /// Please consider the SFINAE hook an implementation detail, as explained /// in the comment for the copyable_holder_caster. @@ -854,6 +992,143 @@ struct move_only_holder_caster { static constexpr auto name = type_caster_base::name; }; +template +struct move_only_holder_caster_unique_ptr_with_smart_holder_support_enabled : std::true_type {}; + +// SMART_HOLDER_BAKEIN_FOLLOW_ON: Refactor move_only_holder_caster to reduce code duplication. +template +struct move_only_holder_caster< + type, + std::unique_ptr, + enable_if_t::value>> + : public type_caster_base { +public: + using base = type_caster_base; + static_assert(std::is_base_of>::value, + "Holder classes are only supported for custom types"); + using base::base; + using base::cast; + using base::typeinfo; + using base::value; + + static handle + cast(std::unique_ptr &&src, return_value_policy policy, handle parent) { + auto *ptr = src.get(); + auto st = type_caster_base::src_and_type(ptr); + if (st.second == nullptr) { + return handle(); // no type info: error will be set already + } + if (st.second->holder_enum_v == detail::holder_enum_t::smart_holder) { + return smart_holder_type_caster_support::smart_holder_from_unique_ptr( + std::move(src), policy, parent, st); + } + return type_caster_generic::cast(st.first, + return_value_policy::take_ownership, + {}, + st.second, + nullptr, + nullptr, + std::addressof(src)); + } + + static handle + cast(const std::unique_ptr &src, return_value_policy policy, handle parent) { + if (!src) { + return none().release(); + } + if (policy == return_value_policy::automatic) { + policy = return_value_policy::reference_internal; + } + if (policy != return_value_policy::reference_internal) { + throw cast_error("Invalid return_value_policy for const unique_ptr&"); + } + return type_caster_base::cast(src.get(), policy, parent); + } + + bool load(handle src, bool convert) { + if (base::template load_impl< + move_only_holder_caster>>(src, convert)) { + sh_load_helper.maybe_set_python_instance_is_alias(src); + return true; + } + return false; + } + + void load_value(value_and_holder &&v_h) { + if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + sh_load_helper.loaded_v_h = v_h; + sh_load_helper.loaded_v_h.type = typeinfo; + sh_load_helper.was_populated = true; + value = sh_load_helper.get_void_ptr_or_nullptr(); + return; + } + pybind11_fail("Passing `std::unique_ptr` from Python to C++ requires `py::class_` (with T = " + + clean_type_id(typeinfo->cpptype->name()) + ")"); + } + + template + using cast_op_type + = conditional_t::type, + const std::unique_ptr &>::value + || std::is_same::type, + const std::unique_ptr &>::value, + const std::unique_ptr &, + std::unique_ptr>; + + explicit operator std::unique_ptr() { + if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + return sh_load_helper.template load_as_unique_ptr(value); + } + pybind11_fail("Expected to be UNREACHABLE: " __FILE__ ":" PYBIND11_TOSTRING(__LINE__)); + } + + explicit operator const std::unique_ptr &() { + if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + // Get shared_ptr to ensure that the Python object is not disowned elsewhere. + shared_ptr_storage = sh_load_helper.load_as_shared_ptr(value); + // Build a temporary unique_ptr that is meant to never expire. + unique_ptr_storage = std::shared_ptr>( + new std::unique_ptr{ + sh_load_helper.template load_as_const_unique_ptr( + shared_ptr_storage.get())}, + [](std::unique_ptr *ptr) { + if (!ptr) { + pybind11_fail("FATAL: `const std::unique_ptr &` was disowned " + "(EXPECT UNDEFINED BEHAVIOR)."); + } + (void) ptr->release(); + delete ptr; + }); + return *unique_ptr_storage; + } + pybind11_fail("Expected to be UNREACHABLE: " __FILE__ ":" PYBIND11_TOSTRING(__LINE__)); + } + + bool try_implicit_casts(handle src, bool convert) { + for (auto &cast : typeinfo->implicit_casts) { + move_only_holder_caster sub_caster(*cast.first); + if (sub_caster.load(src, convert)) { + value = cast.second(sub_caster.value); + if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + sh_load_helper.loaded_v_h = sub_caster.sh_load_helper.loaded_v_h; + } else { + pybind11_fail("Expected to be UNREACHABLE: " __FILE__ + ":" PYBIND11_TOSTRING(__LINE__)); + } + return true; + } + } + return false; + } + + static bool try_direct_conversions(handle) { return false; } + + smart_holder_type_caster_support::load_helper> sh_load_helper; // Const2Mutbl + std::shared_ptr shared_ptr_storage; // Serves as a pseudo lock. + std::shared_ptr> unique_ptr_storage; +}; + template class type_caster> : public move_only_holder_caster> {}; @@ -887,10 +1162,14 @@ struct always_construct_holder : always_construct_holder_value {}; template struct is_holder_type : std::is_base_of, detail::type_caster> {}; -// Specialization for always-supported unique_ptr holders: + +// Specializations for always-supported holders: template struct is_holder_type> : std::true_type {}; +template +struct is_holder_type : std::true_type {}; + #ifdef PYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION // See PR #4888 // This leads to compilation errors if a specialization is missing. diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index aac6e847c6..08e23afb59 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -457,6 +457,8 @@ inline void clear_instance(PyObject *self) { if (instance->owned || v_h.holder_constructed()) { v_h.type->dealloc(v_h); } + } else if (v_h.holder_constructed()) { + v_h.type->dealloc(v_h); // Disowned instance. } } // Deallocate the value/holder layout internals: diff --git a/include/pybind11/detail/common.h b/include/pybind11/detail/common.h index 372dd069a1..5e225f8c19 100644 --- a/include/pybind11/detail/common.h +++ b/include/pybind11/detail/common.h @@ -605,6 +605,8 @@ struct instance { bool simple_instance_registered : 1; /// If true, get_internals().patients has an entry for this object bool has_patients : 1; + /// If true, this Python object needs to be kept alive for the lifetime of the C++ value. + bool is_alias : 1; /// Initializes all of the above type/values/holders data (but not the instance values /// themselves) diff --git a/include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h b/include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h new file mode 100644 index 0000000000..7c00fe98c1 --- /dev/null +++ b/include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h @@ -0,0 +1,39 @@ +// Copyright (c) 2021 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#pragma once + +#include "common.h" + +#include + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) +PYBIND11_NAMESPACE_BEGIN(detail) + +template +struct dynamic_raw_ptr_cast_is_possible : std::false_type {}; + +template +struct dynamic_raw_ptr_cast_is_possible< + To, + From, + detail::enable_if_t::value && std::is_polymorphic::value>> + : std::true_type {}; + +template ::value, int> = 0> +To *dynamic_raw_ptr_cast_if_possible(From * /*ptr*/) { + return nullptr; +} + +template ::value, int> = 0> +To *dynamic_raw_ptr_cast_if_possible(From *ptr) { + return dynamic_cast(ptr); +} + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/detail/init.h b/include/pybind11/detail/init.h index 3eeeaaf970..ed95afe58c 100644 --- a/include/pybind11/detail/init.h +++ b/include/pybind11/detail/init.h @@ -10,6 +10,7 @@ #pragma once #include "class.h" +#include "using_smart_holder.h" PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) @@ -155,7 +156,7 @@ void construct(value_and_holder &v_h, Alias *alias_ptr, bool) { // holder. This also handles types like std::shared_ptr and std::unique_ptr where T is a // derived type (through those holder's implicit conversion from derived class holder // constructors). -template +template >::value, int> = 0> void construct(value_and_holder &v_h, Holder holder, bool need_alias) { PYBIND11_WORKAROUND_INCORRECT_MSVC_C4100(need_alias); auto *ptr = holder_helper>::get(holder); @@ -197,6 +198,74 @@ void construct(value_and_holder &v_h, Alias &&result, bool) { v_h.value_ptr() = new Alias(std::move(result)); } +template +smart_holder init_smart_holder_from_unique_ptr(std::unique_ptr &&unq_ptr, + bool void_cast_raw_ptr) { + void *void_ptr = void_cast_raw_ptr ? static_cast(unq_ptr.get()) : nullptr; + return smart_holder::from_unique_ptr(std::move(unq_ptr), void_ptr); +} + +template >, + detail::enable_if_t>::value, int> = 0> +void construct(value_and_holder &v_h, std::unique_ptr, D> &&unq_ptr, bool need_alias) { + PYBIND11_WORKAROUND_INCORRECT_MSVC_C4100(need_alias); + auto *ptr = unq_ptr.get(); + no_nullptr(ptr); + if (Class::has_alias && need_alias && !is_alias(ptr)) { + throw type_error("pybind11::init(): construction failed: returned std::unique_ptr pointee " + "is not an alias instance"); + } + // Here and below: if the new object is a trampoline, the shared_from_this mechanism needs + // to be prevented from accessing the smart_holder vptr, because it does not keep the + // trampoline Python object alive. For types that don't inherit from enable_shared_from_this + // it does not matter if void_cast_raw_ptr is true or false, therefore it's not necessary + // to also inspect the type. + auto smhldr = init_smart_holder_from_unique_ptr( + std::move(unq_ptr), /*void_cast_raw_ptr*/ Class::has_alias && is_alias(ptr)); + v_h.value_ptr() = ptr; + v_h.type->init_instance(v_h.inst, &smhldr); +} + +template >, + detail::enable_if_t>::value, int> = 0> +void construct(value_and_holder &v_h, + std::unique_ptr, D> &&unq_ptr, + bool /*need_alias*/) { + auto *ptr = unq_ptr.get(); + no_nullptr(ptr); + auto smhldr + = init_smart_holder_from_unique_ptr(std::move(unq_ptr), /*void_cast_raw_ptr*/ true); + v_h.value_ptr() = ptr; + v_h.type->init_instance(v_h.inst, &smhldr); +} + +template >::value, int> = 0> +void construct(value_and_holder &v_h, std::shared_ptr> &&shd_ptr, bool need_alias) { + PYBIND11_WORKAROUND_INCORRECT_MSVC_C4100(need_alias); + auto *ptr = shd_ptr.get(); + no_nullptr(ptr); + if (Class::has_alias && need_alias && !is_alias(ptr)) { + throw type_error("pybind11::init(): construction failed: returned std::shared_ptr pointee " + "is not an alias instance"); + } + auto smhldr = smart_holder::from_shared_ptr(shd_ptr); + v_h.value_ptr() = ptr; + v_h.type->init_instance(v_h.inst, &smhldr); +} + +template >::value, int> = 0> +void construct(value_and_holder &v_h, + std::shared_ptr> &&shd_ptr, + bool /*need_alias*/) { + auto *ptr = shd_ptr.get(); + no_nullptr(ptr); + auto smhldr = smart_holder::from_shared_ptr(shd_ptr); + v_h.value_ptr() = ptr; + v_h.type->init_instance(v_h.inst, &smhldr); +} + // Implementing class for py::init<...>() template struct constructor { diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index dffb9e444f..841c8fe155 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -37,11 +37,11 @@ /// further ABI-incompatible changes may be made before the ABI is officially /// changed to the new version. #ifndef PYBIND11_INTERNALS_VERSION -# define PYBIND11_INTERNALS_VERSION 6 +# define PYBIND11_INTERNALS_VERSION 7 #endif -#if PYBIND11_INTERNALS_VERSION < 6 -# error "PYBIND11_INTERNALS_VERSION 6 is the minimum for all platforms for pybind11v3." +#if PYBIND11_INTERNALS_VERSION < 7 +# error "PYBIND11_INTERNALS_VERSION 7 is the minimum for all platforms for pybind11v3." #endif PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) @@ -211,6 +211,17 @@ struct internals { } }; +// For backwards compatibility (i.e. #ifdef guards): +#define PYBIND11_HAS_INTERNALS_WITH_SMART_HOLDER_SUPPORT + +enum class holder_enum_t : uint8_t { + undefined, + std_unique_ptr, // Default, lacking interop with std::shared_ptr. + std_shared_ptr, // Lacking interop with std::unique_ptr. + smart_holder, // Full std::unique_ptr / std::shared_ptr interop. + custom_holder, +}; + /// Additional type information which does not fit into the PyTypeObject. /// Changes to this struct also require bumping `PYBIND11_INTERNALS_VERSION`. struct type_info { @@ -226,6 +237,7 @@ struct type_info { buffer_info *(*get_buffer)(PyObject *, void *) = nullptr; void *get_buffer_data = nullptr; void *(*module_local_load)(PyObject *, const type_info *) = nullptr; + holder_enum_t holder_enum_v = holder_enum_t::undefined; /* A simple type never occurs as a (direct or indirect) parent * of a class that makes use of multiple inheritance. * A type can be simple even if it has non-simple ancestors as long as it has no descendants. @@ -233,8 +245,6 @@ struct type_info { bool simple_type : 1; /* True if there is no multiple inheritance in this type's inheritance tree */ bool simple_ancestors : 1; - /* for base vs derived holder_type checks */ - bool default_holder : 1; /* true if this is a type registered with py::module_local */ bool module_local : 1; }; diff --git a/include/pybind11/detail/struct_smart_holder.h b/include/pybind11/detail/struct_smart_holder.h new file mode 100644 index 0000000000..980fc3699b --- /dev/null +++ b/include/pybind11/detail/struct_smart_holder.h @@ -0,0 +1,349 @@ +// Copyright (c) 2020-2024 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/* Proof-of-Concept for smart pointer interoperability. + +High-level aspects: + +* Support all `unique_ptr`, `shared_ptr` interops that are feasible. + +* Cleanly and clearly report all interops that are infeasible. + +* Meant to fit into a `PyObject`, as a holder for C++ objects. + +* Support a system design that makes it impossible to trigger + C++ Undefined Behavior, especially from Python. + +* Support a system design with clean runtime inheritance casting. From this + it follows that the `smart_holder` needs to be type-erased (`void*`). + +* Handling of RTTI for the type-erased held pointer is NOT implemented here. + It is the responsibility of the caller to ensure that `static_cast` + is well-formed when calling `as_*` member functions. Inheritance casting + needs to be handled in a different layer (similar to the code organization + in boost/python/object/inheritance.hpp). + +Details: + +* The "root holder" chosen here is a `shared_ptr` (named `vptr` in this + implementation). This choice is practically inevitable because `shared_ptr` + has only very limited support for inspecting and accessing its deleter. + +* If created from a raw pointer, or a `unique_ptr` without a custom deleter, + `vptr` always uses a custom deleter, to support `unique_ptr`-like disowning. + The custom deleters could be extended to included life-time management for + external objects (e.g. `PyObject`). + +* If created from an external `shared_ptr`, or a `unique_ptr` with a custom + deleter, including life-time management for external objects is infeasible. + +* By choice, the smart_holder is movable but not copyable, to keep the design + simple, and to guard against accidental copying overhead. + +* 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. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +// pybindit = Python Bindings Innovation Track. +// Currently not in pybind11 namespace to signal that this POC does not depend +// on any existing pybind11 functionality. +namespace pybindit { +namespace memory { + +static constexpr bool type_has_shared_from_this(...) { return false; } + +template +static constexpr bool type_has_shared_from_this(const std::enable_shared_from_this *) { + return true; +} + +struct guarded_delete { + std::weak_ptr released_ptr; // Trick to keep the smart_holder memory footprint small. + std::function del_fun; // Rare case. + void (*del_ptr)(void *); // Common case. + bool use_del_fun; + bool armed_flag; + guarded_delete(std::function &&del_fun, bool armed_flag) + : del_fun{std::move(del_fun)}, del_ptr{nullptr}, use_del_fun{true}, + armed_flag{armed_flag} {} + guarded_delete(void (*del_ptr)(void *), bool armed_flag) + : del_ptr{del_ptr}, use_del_fun{false}, armed_flag{armed_flag} {} + void operator()(void *raw_ptr) const { + if (armed_flag) { + if (use_del_fun) { + del_fun(raw_ptr); + } else { + del_ptr(raw_ptr); + } + } + } +}; + +template ::value, int>::type = 0> +inline void builtin_delete_if_destructible(void *raw_ptr) { + std::default_delete{}(static_cast(raw_ptr)); +} + +template ::value, int>::type = 0> +inline void builtin_delete_if_destructible(void *) { + // This noop operator is needed to avoid a compilation error (for `delete raw_ptr;`), but + // throwing an exception from a destructor will std::terminate the process. Therefore the + // runtime check for lifetime-management correctness is implemented elsewhere (in + // ensure_pointee_is_destructible()). +} + +template +guarded_delete make_guarded_builtin_delete(bool armed_flag) { + return guarded_delete(builtin_delete_if_destructible, armed_flag); +} + +template +struct custom_deleter { + D deleter; + explicit custom_deleter(D &&deleter) : deleter{std::forward(deleter)} {} + void operator()(void *raw_ptr) { deleter(static_cast(raw_ptr)); } +}; + +template +guarded_delete make_guarded_custom_deleter(D &&uqp_del, bool armed_flag) { + return guarded_delete( + std::function(custom_deleter(std::forward(uqp_del))), armed_flag); +} + +template +inline bool is_std_default_delete(const std::type_info &rtti_deleter) { + return rtti_deleter == typeid(std::default_delete) + || rtti_deleter == typeid(std::default_delete); +} + +struct smart_holder { + const std::type_info *rtti_uqp_del = nullptr; + std::shared_ptr vptr; + bool vptr_is_using_noop_deleter : 1; + bool vptr_is_using_builtin_delete : 1; + bool vptr_is_external_shared_ptr : 1; + bool is_populated : 1; + bool is_disowned : 1; + + // Design choice: smart_holder is movable but not copyable. + smart_holder(smart_holder &&) = default; + smart_holder(const smart_holder &) = delete; + smart_holder &operator=(smart_holder &&) = delete; + smart_holder &operator=(const smart_holder &) = delete; + + smart_holder() + : vptr_is_using_noop_deleter{false}, vptr_is_using_builtin_delete{false}, + vptr_is_external_shared_ptr{false}, is_populated{false}, is_disowned{false} {} + + bool has_pointee() const { return vptr != nullptr; } + + template + static void ensure_pointee_is_destructible(const char *context) { + if (!std::is_destructible::value) { + throw std::invalid_argument(std::string("Pointee is not destructible (") + context + + ")."); + } + } + + void ensure_is_populated(const char *context) const { + if (!is_populated) { + throw std::runtime_error(std::string("Unpopulated holder (") + context + ")."); + } + } + void ensure_is_not_disowned(const char *context) const { + if (is_disowned) { + throw std::runtime_error(std::string("Holder was disowned already (") + context + + ")."); + } + } + + void ensure_vptr_is_using_builtin_delete(const char *context) const { + if (vptr_is_external_shared_ptr) { + throw std::invalid_argument(std::string("Cannot disown external shared_ptr (") + + context + ")."); + } + if (vptr_is_using_noop_deleter) { + throw std::invalid_argument(std::string("Cannot disown non-owning holder (") + context + + ")."); + } + if (!vptr_is_using_builtin_delete) { + throw std::invalid_argument(std::string("Cannot disown custom deleter (") + context + + ")."); + } + } + + template + void ensure_compatible_rtti_uqp_del(const char *context) const { + const std::type_info *rtti_requested = &typeid(D); + if (!rtti_uqp_del) { + if (!is_std_default_delete(*rtti_requested)) { + throw std::invalid_argument(std::string("Missing unique_ptr deleter (") + context + + ")."); + } + ensure_vptr_is_using_builtin_delete(context); + } else if (!(*rtti_requested == *rtti_uqp_del) + && !(vptr_is_using_builtin_delete + && is_std_default_delete(*rtti_requested))) { + throw std::invalid_argument(std::string("Incompatible unique_ptr deleter (") + context + + ")."); + } + } + + void ensure_has_pointee(const char *context) const { + if (!has_pointee()) { + throw std::invalid_argument(std::string("Disowned holder (") + context + ")."); + } + } + + void ensure_use_count_1(const char *context) const { + if (vptr == nullptr) { + throw std::invalid_argument(std::string("Cannot disown nullptr (") + context + ")."); + } + // In multithreaded environments accessing use_count can lead to + // race conditions, but in the context of Python it is a bug (elsewhere) + // if the Global Interpreter Lock (GIL) is not being held when this code + // is reached. + // PYBIND11:REMINDER: This may need to be protected by a mutex in free-threaded Python. + if (vptr.use_count() != 1) { + throw std::invalid_argument(std::string("Cannot disown use_count != 1 (") + context + + ")."); + } + } + + void reset_vptr_deleter_armed_flag(bool armed_flag) const { + auto *vptr_del_ptr = std::get_deleter(vptr); + if (vptr_del_ptr == nullptr) { + throw std::runtime_error( + "smart_holder::reset_vptr_deleter_armed_flag() called in an invalid context."); + } + vptr_del_ptr->armed_flag = armed_flag; + } + + // Caller is responsible for precondition: ensure_compatible_rtti_uqp_del() must succeed. + template + std::unique_ptr extract_deleter(const char *context) const { + const auto *gd = std::get_deleter(vptr); + if (gd && gd->use_del_fun) { + const auto &custom_deleter_ptr = gd->del_fun.template target>(); + if (custom_deleter_ptr == nullptr) { + throw std::runtime_error( + std::string("smart_holder::extract_deleter() precondition failure (") + context + + ")."); + } + static_assert(std::is_copy_constructible::value, + "Required for compatibility with smart_holder functionality."); + return std::unique_ptr(new D(custom_deleter_ptr->deleter)); + } + return nullptr; + } + + static smart_holder from_raw_ptr_unowned(void *raw_ptr) { + smart_holder hld; + hld.vptr.reset(raw_ptr, [](void *) {}); + hld.vptr_is_using_noop_deleter = true; + hld.is_populated = true; + return hld; + } + + template + T *as_raw_ptr_unowned() const { + return static_cast(vptr.get()); + } + + template + static smart_holder from_raw_ptr_take_ownership(T *raw_ptr, bool void_cast_raw_ptr = false) { + ensure_pointee_is_destructible("from_raw_ptr_take_ownership"); + smart_holder hld; + auto gd = make_guarded_builtin_delete(true); + if (void_cast_raw_ptr) { + hld.vptr.reset(static_cast(raw_ptr), std::move(gd)); + } else { + hld.vptr.reset(raw_ptr, std::move(gd)); + } + hld.vptr_is_using_builtin_delete = true; + hld.is_populated = true; + return hld; + } + + // Caller is responsible for ensuring the complex preconditions + // (see `smart_holder_type_caster_support::load_helper`). + void disown() { + reset_vptr_deleter_armed_flag(false); + is_disowned = true; + } + + // Caller is responsible for ensuring the complex preconditions + // (see `smart_holder_type_caster_support::load_helper`). + void reclaim_disowned() { + reset_vptr_deleter_armed_flag(true); + is_disowned = false; + } + + // Caller is responsible for ensuring the complex preconditions + // (see `smart_holder_type_caster_support::load_helper`). + void release_disowned() { vptr.reset(); } + + void ensure_can_release_ownership(const char *context = "ensure_can_release_ownership") const { + ensure_is_not_disowned(context); + ensure_vptr_is_using_builtin_delete(context); + ensure_use_count_1(context); + } + + // Caller is responsible for ensuring the complex preconditions + // (see `smart_holder_type_caster_support::load_helper`). + void release_ownership() { + reset_vptr_deleter_armed_flag(false); + release_disowned(); + } + + template + static smart_holder from_unique_ptr(std::unique_ptr &&unq_ptr, + void *void_ptr = nullptr) { + smart_holder hld; + hld.rtti_uqp_del = &typeid(D); + hld.vptr_is_using_builtin_delete = is_std_default_delete(*hld.rtti_uqp_del); + guarded_delete gd{nullptr, false}; + if (hld.vptr_is_using_builtin_delete) { + gd = make_guarded_builtin_delete(true); + } else { + gd = make_guarded_custom_deleter(std::move(unq_ptr.get_deleter()), true); + } + if (void_ptr != nullptr) { + hld.vptr.reset(void_ptr, std::move(gd)); + } else { + hld.vptr.reset(unq_ptr.get(), std::move(gd)); + } + (void) unq_ptr.release(); + hld.is_populated = true; + return hld; + } + + template + static smart_holder from_shared_ptr(std::shared_ptr shd_ptr) { + smart_holder hld; + hld.vptr = std::static_pointer_cast(shd_ptr); + hld.vptr_is_external_shared_ptr = true; + hld.is_populated = true; + return hld; + } + + template + std::shared_ptr as_shared_ptr() const { + return std::static_pointer_cast(vptr); + } +}; + +} // namespace memory +} // namespace pybindit diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 8751364d4c..9618b2181d 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -9,13 +9,17 @@ #pragma once +#include #include +#include #include "common.h" #include "cpp_conduit.h" #include "descr.h" +#include "dynamic_raw_ptr_cast_if_possible.h" #include "internals.h" #include "typeid.h" +#include "using_smart_holder.h" #include "value_and_holder.h" #include @@ -507,6 +511,361 @@ inline PyThreadState *get_thread_state_unchecked() { void keep_alive_impl(handle nurse, handle patient); inline PyObject *make_new_instance(PyTypeObject *type); +// PYBIND11:REMINDER: Needs refactoring of existing pybind11 code. +inline bool deregister_instance(instance *self, void *valptr, const type_info *tinfo); + +PYBIND11_NAMESPACE_BEGIN(smart_holder_type_caster_support) + +struct value_and_holder_helper { + value_and_holder loaded_v_h; + + bool have_holder() const { + return loaded_v_h.vh != nullptr && loaded_v_h.holder_constructed(); + } + + smart_holder &holder() const { return loaded_v_h.holder(); } + + void throw_if_uninitialized_or_disowned_holder(const char *typeid_name) const { + static const std::string missing_value_msg = "Missing value for wrapped C++ type `"; + if (!holder().is_populated) { + throw value_error(missing_value_msg + clean_type_id(typeid_name) + + "`: Python instance is uninitialized."); + } + if (!holder().has_pointee()) { + throw value_error(missing_value_msg + clean_type_id(typeid_name) + + "`: Python instance was disowned."); + } + } + + void throw_if_uninitialized_or_disowned_holder(const std::type_info &type_info) const { + throw_if_uninitialized_or_disowned_holder(type_info.name()); + } + + // have_holder() must be true or this function will fail. + void throw_if_instance_is_currently_owned_by_shared_ptr() const { + auto *vptr_gd_ptr = std::get_deleter(holder().vptr); + if (vptr_gd_ptr != nullptr && !vptr_gd_ptr->released_ptr.expired()) { + throw value_error("Python instance is currently owned by a std::shared_ptr."); + } + } + + void *get_void_ptr_or_nullptr() const { + if (have_holder()) { + auto &hld = holder(); + if (hld.is_populated && hld.has_pointee()) { + return hld.template as_raw_ptr_unowned(); + } + } + return nullptr; + } +}; + +template +handle smart_holder_from_unique_ptr(std::unique_ptr &&src, + return_value_policy policy, + handle parent, + const std::pair &st) { + if (policy == return_value_policy::copy) { + throw cast_error("return_value_policy::copy is invalid for unique_ptr."); + } + if (!src) { + return none().release(); + } + void *src_raw_void_ptr = const_cast(st.first); + assert(st.second != nullptr); + const detail::type_info *tinfo = st.second; + if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { + auto *self_life_support + = dynamic_raw_ptr_cast_if_possible(src.get()); + if (self_life_support != nullptr) { + value_and_holder &v_h = self_life_support->v_h; + if (v_h.inst != nullptr && v_h.vh != nullptr) { + auto &holder = v_h.holder(); + if (!holder.is_disowned) { + pybind11_fail("smart_holder_from_unique_ptr: unexpected " + "smart_holder.is_disowned failure."); + } + // Critical transfer-of-ownership section. This must stay together. + self_life_support->deactivate_life_support(); + holder.reclaim_disowned(); + (void) src.release(); + // Critical section end. + return existing_inst; + } + } + throw cast_error("Invalid unique_ptr: another instance owns this pointer already."); + } + + auto inst = reinterpret_steal(make_new_instance(tinfo->type)); + auto *inst_raw_ptr = reinterpret_cast(inst.ptr()); + inst_raw_ptr->owned = true; + void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr(); + valueptr = src_raw_void_ptr; + + if (static_cast(src.get()) == src_raw_void_ptr) { + // This is a multiple-inheritance situation that is incompatible with the current + // shared_from_this handling (see PR #3023). Is there a better solution? + src_raw_void_ptr = nullptr; + } + auto smhldr = smart_holder::from_unique_ptr(std::move(src), src_raw_void_ptr); + tinfo->init_instance(inst_raw_ptr, static_cast(&smhldr)); + + if (policy == return_value_policy::reference_internal) { + keep_alive_impl(inst, parent); + } + + return inst.release(); +} + +template +handle smart_holder_from_unique_ptr(std::unique_ptr &&src, + return_value_policy policy, + handle parent, + const std::pair &st) { + return smart_holder_from_unique_ptr( + std::unique_ptr(const_cast(src.release()), + std::move(src.get_deleter())), // Const2Mutbl + policy, + parent, + st); +} + +template +handle smart_holder_from_shared_ptr(const std::shared_ptr &src, + return_value_policy policy, + handle parent, + const std::pair &st) { + switch (policy) { + case return_value_policy::automatic: + case return_value_policy::automatic_reference: + break; + case return_value_policy::take_ownership: + throw cast_error("Invalid return_value_policy for shared_ptr (take_ownership)."); + case return_value_policy::copy: + case return_value_policy::move: + break; + case return_value_policy::reference: + throw cast_error("Invalid return_value_policy for shared_ptr (reference)."); + case return_value_policy::reference_internal: + break; + } + if (!src) { + return none().release(); + } + + auto src_raw_ptr = src.get(); + assert(st.second != nullptr); + void *src_raw_void_ptr = static_cast(src_raw_ptr); + const detail::type_info *tinfo = st.second; + if (handle existing_inst = find_registered_python_instance(src_raw_void_ptr, tinfo)) { + // PYBIND11:REMINDER: MISSING: Enforcement of consistency with existing smart_holder. + // PYBIND11:REMINDER: MISSING: keep_alive. + return existing_inst; + } + + auto inst = reinterpret_steal(make_new_instance(tinfo->type)); + auto *inst_raw_ptr = reinterpret_cast(inst.ptr()); + inst_raw_ptr->owned = true; + void *&valueptr = values_and_holders(inst_raw_ptr).begin()->value_ptr(); + valueptr = src_raw_void_ptr; + + auto smhldr + = smart_holder::from_shared_ptr(std::shared_ptr(src, const_cast(st.first))); + tinfo->init_instance(inst_raw_ptr, static_cast(&smhldr)); + + if (policy == return_value_policy::reference_internal) { + keep_alive_impl(inst, parent); + } + + return inst.release(); +} + +template +handle smart_holder_from_shared_ptr(const std::shared_ptr &src, + return_value_policy policy, + handle parent, + const std::pair &st) { + return smart_holder_from_shared_ptr(std::const_pointer_cast(src), // Const2Mutbl + policy, + parent, + st); +} + +struct shared_ptr_parent_life_support { + PyObject *parent; + explicit shared_ptr_parent_life_support(PyObject *parent) : parent{parent} { + Py_INCREF(parent); + } + // NOLINTNEXTLINE(readability-make-member-function-const) + void operator()(void *) { + gil_scoped_acquire gil; + Py_DECREF(parent); + } +}; + +struct shared_ptr_trampoline_self_life_support { + PyObject *self; + explicit shared_ptr_trampoline_self_life_support(instance *inst) + : self{reinterpret_cast(inst)} { + gil_scoped_acquire gil; + Py_INCREF(self); + } + // NOLINTNEXTLINE(readability-make-member-function-const) + void operator()(void *) { + gil_scoped_acquire gil; + Py_DECREF(self); + } +}; + +template ::value, int>::type = 0> +inline std::unique_ptr unique_with_deleter(T *raw_ptr, std::unique_ptr &&deleter) { + if (deleter == nullptr) { + return std::unique_ptr(raw_ptr); + } + return std::unique_ptr(raw_ptr, std::move(*deleter)); +} + +template ::value, int>::type = 0> +inline std::unique_ptr unique_with_deleter(T *raw_ptr, std::unique_ptr &&deleter) { + if (deleter == nullptr) { + pybind11_fail("smart_holder_type_casters: deleter is not default constructible and no" + " instance available to return."); + } + return std::unique_ptr(raw_ptr, std::move(*deleter)); +} + +template +struct load_helper : value_and_holder_helper { + bool was_populated = false; + bool python_instance_is_alias = false; + + void maybe_set_python_instance_is_alias(handle src) { + if (was_populated) { + python_instance_is_alias = reinterpret_cast(src.ptr())->is_alias; + } + } + + static std::shared_ptr make_shared_ptr_with_responsible_parent(T *raw_ptr, handle parent) { + return std::shared_ptr(raw_ptr, shared_ptr_parent_life_support(parent.ptr())); + } + + std::shared_ptr load_as_shared_ptr(void *void_raw_ptr, + handle responsible_parent = nullptr) const { + if (!have_holder()) { + return nullptr; + } + throw_if_uninitialized_or_disowned_holder(typeid(T)); + smart_holder &hld = holder(); + hld.ensure_is_not_disowned("load_as_shared_ptr"); + if (hld.vptr_is_using_noop_deleter) { + if (responsible_parent) { + return make_shared_ptr_with_responsible_parent(static_cast(void_raw_ptr), + responsible_parent); + } + throw std::runtime_error("Non-owning holder (load_as_shared_ptr)."); + } + auto *type_raw_ptr = static_cast(void_raw_ptr); + if (python_instance_is_alias) { + auto *vptr_gd_ptr = std::get_deleter(hld.vptr); + if (vptr_gd_ptr != nullptr) { + std::shared_ptr released_ptr = vptr_gd_ptr->released_ptr.lock(); + if (released_ptr) { + return std::shared_ptr(released_ptr, type_raw_ptr); + } + std::shared_ptr to_be_released( + type_raw_ptr, shared_ptr_trampoline_self_life_support(loaded_v_h.inst)); + vptr_gd_ptr->released_ptr = to_be_released; + return to_be_released; + } + auto *sptsls_ptr = std::get_deleter(hld.vptr); + if (sptsls_ptr != nullptr) { + // This code is reachable only if there are multiple registered_instances for the + // same pointee. + if (reinterpret_cast(loaded_v_h.inst) == sptsls_ptr->self) { + pybind11_fail("smart_holder_type_caster_support load_as_shared_ptr failure: " + "loaded_v_h.inst == sptsls_ptr->self"); + } + } + if (sptsls_ptr != nullptr + || !pybindit::memory::type_has_shared_from_this(type_raw_ptr)) { + return std::shared_ptr( + type_raw_ptr, shared_ptr_trampoline_self_life_support(loaded_v_h.inst)); + } + if (hld.vptr_is_external_shared_ptr) { + pybind11_fail("smart_holder_type_casters load_as_shared_ptr failure: not " + "implemented: trampoline-self-life-support for external shared_ptr " + "to type inheriting from std::enable_shared_from_this."); + } + pybind11_fail( + "smart_holder_type_casters: load_as_shared_ptr failure: internal inconsistency."); + } + std::shared_ptr void_shd_ptr = hld.template as_shared_ptr(); + return std::shared_ptr(void_shd_ptr, type_raw_ptr); + } + + template + std::unique_ptr load_as_unique_ptr(void *raw_void_ptr, + const char *context = "load_as_unique_ptr") { + if (!have_holder()) { + return unique_with_deleter(nullptr, std::unique_ptr()); + } + throw_if_uninitialized_or_disowned_holder(typeid(T)); + throw_if_instance_is_currently_owned_by_shared_ptr(); + holder().ensure_is_not_disowned(context); + holder().template ensure_compatible_rtti_uqp_del(context); + holder().ensure_use_count_1(context); + + T *raw_type_ptr = static_cast(raw_void_ptr); + + auto *self_life_support + = dynamic_raw_ptr_cast_if_possible(raw_type_ptr); + if (self_life_support == nullptr && python_instance_is_alias) { + throw value_error("Alias class (also known as trampoline) does not inherit from " + "py::trampoline_self_life_support, therefore the ownership of this " + "instance cannot safely be transferred to C++."); + } + + std::unique_ptr extracted_deleter = holder().template extract_deleter(context); + + // Critical transfer-of-ownership section. This must stay together. + if (self_life_support != nullptr) { + holder().disown(); + } else { + holder().release_ownership(); + } + auto result = unique_with_deleter(raw_type_ptr, std::move(extracted_deleter)); + if (self_life_support != nullptr) { + self_life_support->activate_life_support(loaded_v_h); + } else { + void *value_void_ptr = loaded_v_h.value_ptr(); + loaded_v_h.value_ptr() = nullptr; + deregister_instance(loaded_v_h.inst, value_void_ptr, loaded_v_h.type); + } + // Critical section end. + + return result; + } + + // This assumes load_as_shared_ptr succeeded(), and the returned shared_ptr is still alive. + // The returned unique_ptr is meant to never expire (the behavior is undefined otherwise). + template + std::unique_ptr + load_as_const_unique_ptr(T *raw_type_ptr, const char *context = "load_as_const_unique_ptr") { + if (!have_holder()) { + return unique_with_deleter(nullptr, std::unique_ptr()); + } + holder().template ensure_compatible_rtti_uqp_del(context); + return unique_with_deleter( + raw_type_ptr, std::move(holder().template extract_deleter(context))); + } +}; + +PYBIND11_NAMESPACE_END(smart_holder_type_caster_support) + class type_caster_generic { public: PYBIND11_NOINLINE explicit type_caster_generic(const std::type_info &type_info) @@ -611,6 +970,15 @@ class type_caster_generic { // Base methods for generic caster; there are overridden in copyable_holder_caster void load_value(value_and_holder &&v_h) { + if (typeinfo->holder_enum_v == detail::holder_enum_t::smart_holder) { + smart_holder_type_caster_support::value_and_holder_helper v_h_helper; + v_h_helper.loaded_v_h = v_h; + if (v_h_helper.have_holder()) { + v_h_helper.throw_if_uninitialized_or_disowned_holder(cpptype->name()); + value = v_h_helper.holder().template as_raw_ptr_unowned(); + return; + } + } auto *&vptr = v_h.value_ptr(); // Lazy allocation for unallocated values: if (vptr == nullptr) { diff --git a/include/pybind11/detail/using_smart_holder.h b/include/pybind11/detail/using_smart_holder.h new file mode 100644 index 0000000000..57f99b95f3 --- /dev/null +++ b/include/pybind11/detail/using_smart_holder.h @@ -0,0 +1,22 @@ +// Copyright (c) 2024 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#pragma once + +#include "common.h" +#include "struct_smart_holder.h" + +#include + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +using pybindit::memory::smart_holder; + +PYBIND11_NAMESPACE_BEGIN(detail) + +template +using is_smart_holder = std::is_same; + +PYBIND11_NAMESPACE_END(detail) +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/include/pybind11/detail/value_and_holder.h b/include/pybind11/detail/value_and_holder.h index ca37d70ad2..64c55cc595 100644 --- a/include/pybind11/detail/value_and_holder.h +++ b/include/pybind11/detail/value_and_holder.h @@ -7,6 +7,7 @@ #include "common.h" #include +#include #include PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 8a1739e193..4dee2c55fa 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -10,8 +10,10 @@ #pragma once #include "detail/class.h" +#include "detail/dynamic_raw_ptr_cast_if_possible.h" #include "detail/exception_translation.h" #include "detail/init.h" +#include "detail/using_smart_holder.h" #include "attr.h" #include "gil.h" #include "gil_safe_call_once.h" @@ -1444,8 +1446,8 @@ class generic_type : public object { tinfo->dealloc = rec.dealloc; tinfo->simple_type = true; tinfo->simple_ancestors = true; - tinfo->default_holder = rec.default_holder; tinfo->module_local = rec.module_local; + tinfo->holder_enum_v = rec.holder_enum_v; with_internals([&](internals &internals) { auto tindex = std::type_index(*rec.type); @@ -1618,6 +1620,239 @@ auto method_adaptor(Return (Class::*pmf)(Args...) const) -> Return (Derived::*)( return pmf; } +PYBIND11_NAMESPACE_BEGIN(detail) + +// Helper for the property_cpp_function static member functions below. +// The only purpose of these functions is to support .def_readonly & .def_readwrite. +// In this context, the PM template parameter is certain to be a Pointer to a Member. +// The main purpose of must_be_member_function_pointer is to make this obvious, and to guard +// against accidents. As a side-effect, it also explains why the syntactical overhead for +// perfect forwarding is not needed. +template +using must_be_member_function_pointer = enable_if_t::value, int>; + +// Note that property_cpp_function is intentionally in the main pybind11 namespace, +// because user-defined specializations could be useful. + +// Classic (non-smart_holder) implementations for .def_readonly and .def_readwrite +// getter and setter functions. +// WARNING: This classic implementation can lead to dangling pointers for raw pointer members. +// See test_ptr() in tests/test_class_sh_property.py +// However, this implementation works as-is (and safely) for smart_holder std::shared_ptr members. +template +struct property_cpp_function_classic { + template = 0> + static cpp_function readonly(PM pm, const handle &hdl) { + return cpp_function([pm](const T &c) -> const D & { return c.*pm; }, is_method(hdl)); + } + + template = 0> + static cpp_function read(PM pm, const handle &hdl) { + return readonly(pm, hdl); + } + + template = 0> + static cpp_function write(PM pm, const handle &hdl) { + return cpp_function([pm](T &c, const D &value) { c.*pm = value; }, is_method(hdl)); + } +}; + +PYBIND11_NAMESPACE_END(detail) + +template +struct property_cpp_function : detail::property_cpp_function_classic {}; + +PYBIND11_NAMESPACE_BEGIN(detail) + +template +struct both_t_and_d_use_type_caster_base : std::false_type {}; + +// `T` is assumed to be equivalent to `intrinsic_t`. +// `D` is may or may not be equivalent to `intrinsic_t`. +template +struct both_t_and_d_use_type_caster_base< + T, + D, + enable_if_t, type_caster>, + std::is_base_of>, make_caster>>::value>> + : std::true_type {}; + +// Specialization for raw pointer members, using smart_holder if that is the class_ holder, +// or falling back to the classic implementation if not. +// WARNING: Like the classic implementation, this implementation can lead to dangling pointers. +// See test_ptr() in tests/test_class_sh_property.py +// However, the read functions return a shared_ptr to the member, emulating the PyCLIF approach: +// /~https://github.com/google/clif/blob/c371a6d4b28d25d53a16e6d2a6d97305fb1be25a/clif/python/instance.h#L233 +// This prevents disowning of the Python object owning the raw pointer member. +template +struct property_cpp_function_sh_raw_ptr_member { + using drp = typename std::remove_pointer::type; + + template = 0> + static cpp_function readonly(PM pm, const handle &hdl) { + type_info *tinfo = get_type_info(typeid(T), /*throw_if_missing=*/true); + if (tinfo->holder_enum_v == holder_enum_t::smart_holder) { + return cpp_function( + [pm](handle c_hdl) -> std::shared_ptr { + std::shared_ptr c_sp + = type_caster>::shared_ptr_with_responsible_parent( + c_hdl); + D ptr = (*c_sp).*pm; + return std::shared_ptr(c_sp, ptr); + }, + is_method(hdl)); + } + return property_cpp_function_classic::readonly(pm, hdl); + } + + template = 0> + static cpp_function read(PM pm, const handle &hdl) { + return readonly(pm, hdl); + } + + template = 0> + static cpp_function write(PM pm, const handle &hdl) { + type_info *tinfo = get_type_info(typeid(T), /*throw_if_missing=*/true); + if (tinfo->holder_enum_v == holder_enum_t::smart_holder) { + return cpp_function([pm](T &c, D value) { c.*pm = std::forward(std::move(value)); }, + is_method(hdl)); + } + return property_cpp_function_classic::write(pm, hdl); + } +}; + +// Specialization for members held by-value, using smart_holder if that is the class_ holder, +// or falling back to the classic implementation if not. +// The read functions return a shared_ptr to the member, emulating the PyCLIF approach: +// /~https://github.com/google/clif/blob/c371a6d4b28d25d53a16e6d2a6d97305fb1be25a/clif/python/instance.h#L233 +// This prevents disowning of the Python object owning the member. +template +struct property_cpp_function_sh_member_held_by_value { + template = 0> + static cpp_function readonly(PM pm, const handle &hdl) { + type_info *tinfo = get_type_info(typeid(T), /*throw_if_missing=*/true); + if (tinfo->holder_enum_v == holder_enum_t::smart_holder) { + return cpp_function( + [pm](handle c_hdl) -> std::shared_ptr::type> { + std::shared_ptr c_sp + = type_caster>::shared_ptr_with_responsible_parent( + c_hdl); + return std::shared_ptr::type>(c_sp, + &(c_sp.get()->*pm)); + }, + is_method(hdl)); + } + return property_cpp_function_classic::readonly(pm, hdl); + } + + template = 0> + static cpp_function read(PM pm, const handle &hdl) { + type_info *tinfo = get_type_info(typeid(T), /*throw_if_missing=*/true); + if (tinfo->holder_enum_v == holder_enum_t::smart_holder) { + return cpp_function( + [pm](handle c_hdl) -> std::shared_ptr { + std::shared_ptr c_sp + = type_caster>::shared_ptr_with_responsible_parent( + c_hdl); + return std::shared_ptr(c_sp, &(c_sp.get()->*pm)); + }, + is_method(hdl)); + } + return property_cpp_function_classic::read(pm, hdl); + } + + template = 0> + static cpp_function write(PM pm, const handle &hdl) { + type_info *tinfo = get_type_info(typeid(T), /*throw_if_missing=*/true); + if (tinfo->holder_enum_v == holder_enum_t::smart_holder) { + return cpp_function([pm](T &c, const D &value) { c.*pm = value; }, is_method(hdl)); + } + return property_cpp_function_classic::write(pm, hdl); + } +}; + +// Specialization for std::unique_ptr members, using smart_holder if that is the class_ holder, +// or falling back to the classic implementation if not. +// read disowns the member unique_ptr. +// write disowns the passed Python object. +// readonly is disabled (static_assert) because there is no safe & intuitive way to make the member +// accessible as a Python object without disowning the member unique_ptr. A .def_readonly disowning +// the unique_ptr member is deemed highly prone to misunderstandings. +template +struct property_cpp_function_sh_unique_ptr_member { + template = 0> + static cpp_function readonly(PM, const handle &) { + static_assert(!is_instantiation::value, + "def_readonly cannot be used for std::unique_ptr members."); + return cpp_function{}; // Unreachable. + } + + template = 0> + static cpp_function read(PM pm, const handle &hdl) { + type_info *tinfo = get_type_info(typeid(T), /*throw_if_missing=*/true); + if (tinfo->holder_enum_v == holder_enum_t::smart_holder) { + return cpp_function( + [pm](handle c_hdl) -> D { + std::shared_ptr c_sp + = type_caster>::shared_ptr_with_responsible_parent( + c_hdl); + return D{std::move(c_sp.get()->*pm)}; + }, + is_method(hdl)); + } + return property_cpp_function_classic::read(pm, hdl); + } + + template = 0> + static cpp_function write(PM pm, const handle &hdl) { + return cpp_function([pm](T &c, D &&value) { c.*pm = std::move(value); }, is_method(hdl)); + } +}; + +PYBIND11_NAMESPACE_END(detail) + +template +struct property_cpp_function< + T, + D, + detail::enable_if_t, + detail::both_t_and_d_use_type_caster_base>::value>> + : detail::property_cpp_function_sh_raw_ptr_member {}; + +template +struct property_cpp_function, + std::is_array, + detail::is_instantiation, + detail::is_instantiation>, + detail::both_t_and_d_use_type_caster_base>::value>> + : detail::property_cpp_function_sh_member_held_by_value {}; + +template +struct property_cpp_function< + T, + D, + detail::enable_if_t, + detail::both_t_and_d_use_type_caster_base>::value>> + : detail::property_cpp_function_sh_unique_ptr_member {}; + +#ifdef PYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE +// NOTE: THIS IS MEANT FOR STRESS-TESTING OR TRIAGING ONLY! +// Running the pybind11 unit tests with smart_holder as the default holder is to ensure +// that `py::smart_holder` / `py::classh` is backward-compatible with all pre-existing +// functionality. +// Be careful not to link translation units compiled with different default holders, because +// this will cause ODR violations (https://en.wikipedia.org/wiki/One_Definition_Rule). +template +using default_holder_type = smart_holder; +#else +template +using default_holder_type = std::unique_ptr; +#endif + template class class_ : public detail::generic_type { template @@ -1634,7 +1869,7 @@ class class_ : public detail::generic_type { using type = type_; using type_alias = detail::exactly_one_t; constexpr static bool has_alias = !std::is_void::value; - using holder_type = detail::exactly_one_t, options...>; + using holder_type = detail::exactly_one_t, options...>; static_assert(detail::all_of...>::value, "Unknown/invalid class_ template parameters provided"); @@ -1665,7 +1900,16 @@ class class_ : public detail::generic_type { record.type_align = alignof(conditional_t &); record.holder_size = sizeof(holder_type); record.init_instance = init_instance; - record.default_holder = detail::is_instantiation::value; + + if (detail::is_instantiation::value) { + record.holder_enum_v = detail::holder_enum_t::std_unique_ptr; + } else if (detail::is_instantiation::value) { + record.holder_enum_v = detail::holder_enum_t::std_shared_ptr; + } else if (std::is_same::value) { + record.holder_enum_v = detail::holder_enum_t::smart_holder; + } else { + record.holder_enum_v = detail::holder_enum_t::custom_holder; + } set_operator_new(&record); @@ -1804,9 +2048,11 @@ class class_ : public detail::generic_type { class_ &def_readwrite(const char *name, D C::*pm, const Extra &...extra) { static_assert(std::is_same::value || std::is_base_of::value, "def_readwrite() requires a class member (or base class member)"); - cpp_function fget([pm](const type &c) -> const D & { return c.*pm; }, is_method(*this)), - fset([pm](type &c, const D &value) { c.*pm = value; }, is_method(*this)); - def_property(name, fget, fset, return_value_policy::reference_internal, extra...); + def_property(name, + property_cpp_function::read(pm, *this), + property_cpp_function::write(pm, *this), + return_value_policy::reference_internal, + extra...); return *this; } @@ -1814,8 +2060,10 @@ class class_ : public detail::generic_type { class_ &def_readonly(const char *name, const D C::*pm, const Extra &...extra) { static_assert(std::is_same::value || std::is_base_of::value, "def_readonly() requires a class member (or base class member)"); - cpp_function fget([pm](const type &c) -> const D & { return c.*pm; }, is_method(*this)); - def_property_readonly(name, fget, return_value_policy::reference_internal, extra...); + def_property_readonly(name, + property_cpp_function::readonly(pm, *this), + return_value_policy::reference_internal, + extra...); return *this; } @@ -1992,6 +2240,8 @@ class class_ : public detail::generic_type { /// instance. Should be called as soon as the `type` value_ptr is set for an instance. Takes /// an optional pointer to an existing holder to use; if not specified and the instance is /// `.owned`, a new holder will be constructed to manage the value pointer. + template ::value, int> = 0> static void init_instance(detail::instance *inst, const void *holder_ptr) { auto v_h = inst->get_value_and_holder(detail::get_type_info(typeid(type))); if (!v_h.instance_registered()) { @@ -2001,6 +2251,65 @@ class class_ : public detail::generic_type { init_holder(inst, v_h, (const holder_type *) holder_ptr, v_h.value_ptr()); } + template + static bool try_initialization_using_shared_from_this(holder_type *, WrappedType *, ...) { + return false; + } + + // Adopting existing approach used by type_caster_base, although it leads to somewhat fuzzy + // ownership semantics: if we detected via shared_from_this that a shared_ptr exists already, + // it is reused, irrespective of the return_value_policy in effect. + // "SomeBaseOfWrappedType" is needed because std::enable_shared_from_this is not necessarily a + // direct base of WrappedType. + template + static bool try_initialization_using_shared_from_this( + holder_type *uninitialized_location, + WrappedType *value_ptr_w_t, + const std::enable_shared_from_this *) { + auto shd_ptr = std::dynamic_pointer_cast( + detail::try_get_shared_from_this(value_ptr_w_t)); + if (!shd_ptr) { + return false; + } + // Note: inst->owned ignored. + new (uninitialized_location) holder_type(holder_type::from_shared_ptr(shd_ptr)); + return true; + } + + template ::value, int> = 0> + static void init_instance(detail::instance *inst, const void *holder_const_void_ptr) { + // Need for const_cast is a consequence of the type_info::init_instance type: + // void (*init_instance)(instance *, const void *); + auto *holder_void_ptr = const_cast(holder_const_void_ptr); + + auto v_h = inst->get_value_and_holder(detail::get_type_info(typeid(type))); + if (!v_h.instance_registered()) { + register_instance(inst, v_h.value_ptr(), v_h.type); + v_h.set_instance_registered(); + } + auto *uninitialized_location = std::addressof(v_h.holder()); + auto *value_ptr_w_t = v_h.value_ptr(); + // Try downcast from `type` to `type_alias`: + inst->is_alias + = detail::dynamic_raw_ptr_cast_if_possible(value_ptr_w_t) != nullptr; + if (holder_void_ptr) { + // Note: inst->owned ignored. + auto *holder_ptr = static_cast(holder_void_ptr); + new (uninitialized_location) holder_type(std::move(*holder_ptr)); + } else if (!try_initialization_using_shared_from_this( + uninitialized_location, value_ptr_w_t, value_ptr_w_t)) { + if (inst->owned) { + new (uninitialized_location) holder_type(holder_type::from_raw_ptr_take_ownership( + value_ptr_w_t, /*void_cast_raw_ptr*/ inst->is_alias)); + } else { + new (uninitialized_location) + holder_type(holder_type::from_raw_ptr_unowned(value_ptr_w_t)); + } + } + v_h.set_holder_constructed(); + } + // Deallocates an instance; via holder, if constructed; otherwise via operator delete. // NOTE: The Python error indicator needs to cleared BEFORE this function is called. // This is because we could be deallocating while cleaning up after a Python exception. @@ -2066,6 +2375,11 @@ class class_ : public detail::generic_type { } }; +// Supports easier switching between py::class_ and py::class_: +// users can simply replace the `_` in `class_` with `h` or vice versa. +template +using classh = class_; + /// Binds an existing constructor taking arguments Args... template detail::initimpl::constructor init() { diff --git a/include/pybind11/stl_bind.h b/include/pybind11/stl_bind.h index af3a47f39c..3eb1e53f45 100644 --- a/include/pybind11/stl_bind.h +++ b/include/pybind11/stl_bind.h @@ -487,7 +487,7 @@ PYBIND11_NAMESPACE_END(detail) // // std::vector // -template , typename... Args> +template , typename... Args> class_ bind_vector(handle scope, std::string const &name, Args &&...args) { using Class_ = class_; @@ -730,7 +730,7 @@ str format_message_key_error(const KeyType &key) { PYBIND11_NAMESPACE_END(detail) -template , typename... Args> +template , typename... Args> class_ bind_map(handle scope, const std::string &name, Args &&...args) { using KeyType = typename Map::key_type; using MappedType = typename Map::mapped_type; diff --git a/include/pybind11/trampoline_self_life_support.h b/include/pybind11/trampoline_self_life_support.h new file mode 100644 index 0000000000..484045bb17 --- /dev/null +++ b/include/pybind11/trampoline_self_life_support.h @@ -0,0 +1,60 @@ +// Copyright (c) 2021 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#pragma once + +#include "detail/common.h" +#include "detail/using_smart_holder.h" +#include "detail/value_and_holder.h" + +PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) + +PYBIND11_NAMESPACE_BEGIN(detail) +// PYBIND11:REMINDER: Needs refactoring of existing pybind11 code. +inline bool deregister_instance(instance *self, void *valptr, const type_info *tinfo); +PYBIND11_NAMESPACE_END(detail) + +// The original core idea for this struct goes back to PyCLIF: +// /~https://github.com/google/clif/blob/07f95d7e69dca2fcf7022978a55ef3acff506c19/clif/python/runtime.cc#L37 +// URL provided here mainly to give proper credit. +struct trampoline_self_life_support { + detail::value_and_holder v_h; + + trampoline_self_life_support() = default; + + void activate_life_support(const detail::value_and_holder &v_h_) { + Py_INCREF((PyObject *) v_h_.inst); + v_h = v_h_; + } + + void deactivate_life_support() { + Py_DECREF((PyObject *) v_h.inst); + v_h = detail::value_and_holder(); + } + + ~trampoline_self_life_support() { + if (v_h.inst != nullptr && v_h.vh != nullptr) { + void *value_void_ptr = v_h.value_ptr(); + if (value_void_ptr != nullptr) { + PyGILState_STATE threadstate = PyGILState_Ensure(); + v_h.value_ptr() = nullptr; + v_h.holder().release_disowned(); + detail::deregister_instance(v_h.inst, value_void_ptr, v_h.type); + Py_DECREF((PyObject *) v_h.inst); // Must be after deregister. + PyGILState_Release(threadstate); + } + } + } + + // For the next two, the default implementations generate undefined behavior (ASAN failures + // manually verified). The reason is that v_h needs to be kept default-initialized. + trampoline_self_life_support(const trampoline_self_life_support &) {} + trampoline_self_life_support(trampoline_self_life_support &&) noexcept {} + + // These should never be needed (please provide test cases if you think they are). + trampoline_self_life_support &operator=(const trampoline_self_life_support &) = delete; + trampoline_self_life_support &operator=(trampoline_self_life_support &&) = delete; +}; + +PYBIND11_NAMESPACE_END(PYBIND11_NAMESPACE) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index da396f0984..e2fab9c13d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -116,6 +116,23 @@ set(PYBIND11_TEST_FILES test_chrono test_class test_class_release_gil_before_calling_cpp_dtor + test_class_sh_basic + test_class_sh_disowning + test_class_sh_disowning_mi + test_class_sh_factory_constructors + test_class_sh_inheritance + test_class_sh_mi_thunks + test_class_sh_property + test_class_sh_property_non_owning + test_class_sh_shared_ptr_copy_move + test_class_sh_trampoline_basic + test_class_sh_trampoline_self_life_support + test_class_sh_trampoline_shared_from_this + test_class_sh_trampoline_shared_ptr_cpp_arg + test_class_sh_trampoline_unique_ptr + test_class_sh_unique_ptr_custom_deleter + test_class_sh_unique_ptr_member + test_class_sh_virtual_py_cpp_mix test_const_name test_constants_and_functions test_copy_move @@ -589,6 +606,9 @@ add_custom_command( ${CMAKE_CURRENT_BINARY_DIR}/sosize-$.txt) if(NOT PYBIND11_CUDA_TESTS) + # Test pure C++ code (not depending on Python). Provides the `test_pure_cpp` target. + add_subdirectory(pure_cpp) + # Test embedding the interpreter. Provides the `cpptest` target. add_subdirectory(test_embed) diff --git a/tests/extra_python_package/test_files.py b/tests/extra_python_package/test_files.py index 977db75c63..b01b49867f 100644 --- a/tests/extra_python_package/test_files.py +++ b/tests/extra_python_package/test_files.py @@ -45,6 +45,7 @@ "include/pybind11/pytypes.h", "include/pybind11/stl.h", "include/pybind11/stl_bind.h", + "include/pybind11/trampoline_self_life_support.h", "include/pybind11/type_caster_pyobject_ptr.h", "include/pybind11/typing.h", "include/pybind11/warnings.h", @@ -62,10 +63,13 @@ "include/pybind11/detail/common.h", "include/pybind11/detail/cpp_conduit.h", "include/pybind11/detail/descr.h", + "include/pybind11/detail/dynamic_raw_ptr_cast_if_possible.h", "include/pybind11/detail/init.h", "include/pybind11/detail/internals.h", + "include/pybind11/detail/struct_smart_holder.h", "include/pybind11/detail/type_caster_base.h", "include/pybind11/detail/typeid.h", + "include/pybind11/detail/using_smart_holder.h", "include/pybind11/detail/value_and_holder.h", "include/pybind11/detail/exception_translation.h", } diff --git a/tests/pure_cpp/CMakeLists.txt b/tests/pure_cpp/CMakeLists.txt new file mode 100644 index 0000000000..17be74b7f0 --- /dev/null +++ b/tests/pure_cpp/CMakeLists.txt @@ -0,0 +1,20 @@ +find_package(Catch 2.13.2) + +if(CATCH_FOUND) + message(STATUS "Building pure C++ tests (not depending on Python) using Catch v${CATCH_VERSION}") +else() + message(STATUS "Catch not detected. Interpreter tests will be skipped. Install Catch headers" + " manually or use `cmake -DDOWNLOAD_CATCH=ON` to fetch them automatically.") + return() +endif() + +add_executable(smart_holder_poc_test smart_holder_poc_test.cpp) +pybind11_enable_warnings(smart_holder_poc_test) +target_link_libraries(smart_holder_poc_test PRIVATE pybind11::headers Catch2::Catch2) + +add_custom_target( + test_pure_cpp + COMMAND "$" + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") + +add_dependencies(check test_pure_cpp) diff --git a/tests/pure_cpp/smart_holder_poc.h b/tests/pure_cpp/smart_holder_poc.h new file mode 100644 index 0000000000..320311b7d6 --- /dev/null +++ b/tests/pure_cpp/smart_holder_poc.h @@ -0,0 +1,51 @@ +// Copyright (c) 2020-2024 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#pragma once + +#include "pybind11/detail/struct_smart_holder.h" + +namespace pybindit { +namespace memory { +namespace smart_holder_poc { // Proof-of-Concept implementations. + +template +T &as_lvalue_ref(const smart_holder &hld) { + static const char *context = "as_lvalue_ref"; + hld.ensure_is_populated(context); + hld.ensure_has_pointee(context); + return *hld.as_raw_ptr_unowned(); +} + +template +T &&as_rvalue_ref(const smart_holder &hld) { + static const char *context = "as_rvalue_ref"; + hld.ensure_is_populated(context); + hld.ensure_has_pointee(context); + return std::move(*hld.as_raw_ptr_unowned()); +} + +template +T *as_raw_ptr_release_ownership(smart_holder &hld, + const char *context = "as_raw_ptr_release_ownership") { + hld.ensure_can_release_ownership(context); + T *raw_ptr = hld.as_raw_ptr_unowned(); + hld.release_ownership(); + return raw_ptr; +} + +template > +std::unique_ptr as_unique_ptr(smart_holder &hld) { + static const char *context = "as_unique_ptr"; + hld.ensure_compatible_rtti_uqp_del(context); + hld.ensure_use_count_1(context); + T *raw_ptr = hld.as_raw_ptr_unowned(); + hld.release_ownership(); + // KNOWN DEFECT (see PR #4850): Does not copy the deleter. + return std::unique_ptr(raw_ptr); +} + +} // namespace smart_holder_poc +} // namespace memory +} // namespace pybindit diff --git a/tests/pure_cpp/smart_holder_poc_test.cpp b/tests/pure_cpp/smart_holder_poc_test.cpp new file mode 100644 index 0000000000..24ab643eeb --- /dev/null +++ b/tests/pure_cpp/smart_holder_poc_test.cpp @@ -0,0 +1,415 @@ +#include "smart_holder_poc.h" + +#include +#include +#include +#include + +// Catch uses _ internally, which breaks gettext style defines +#ifdef _ +# undef _ +#endif + +#define CATCH_CONFIG_MAIN +#include "catch.hpp" + +using pybindit::memory::smart_holder; +namespace poc = pybindit::memory::smart_holder_poc; + +namespace helpers { + +struct movable_int { + int valu; + explicit movable_int(int v) : valu{v} {} + movable_int(movable_int &&other) noexcept : valu(other.valu) { other.valu = 91; } +}; + +template +struct functor_builtin_delete { + void operator()(T *ptr) { delete ptr; } +#if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ == 8) \ + || (defined(__clang_major__) && __clang_major__ == 3 && __clang_minor__ == 6) + // Workaround for these errors: + // gcc 4.8.5: too many initializers for 'helpers::functor_builtin_delete' + // clang 3.6: excess elements in struct initializer + functor_builtin_delete() = default; + functor_builtin_delete(const functor_builtin_delete &) {} + functor_builtin_delete(functor_builtin_delete &&) {} +#endif +}; + +template +struct functor_other_delete : functor_builtin_delete {}; + +struct indestructible_int { + int valu; + explicit indestructible_int(int v) : valu{v} {} + +private: + ~indestructible_int() = default; +}; + +struct base { + virtual int get() { return 10; } + virtual ~base() = default; +}; + +struct derived : public base { + int get() override { return 100; } +}; + +} // namespace helpers + +TEST_CASE("from_raw_ptr_unowned+as_raw_ptr_unowned", "[S]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE(*hld.as_raw_ptr_unowned() == 19); +} + +TEST_CASE("from_raw_ptr_unowned+as_lvalue_ref", "[S]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE(poc::as_lvalue_ref(hld) == 19); +} + +TEST_CASE("from_raw_ptr_unowned+as_rvalue_ref", "[S]") { + helpers::movable_int orig(19); + { + auto hld = smart_holder::from_raw_ptr_unowned(&orig); + helpers::movable_int othr(poc::as_rvalue_ref(hld)); + REQUIRE(othr.valu == 19); + REQUIRE(orig.valu == 91); + } +} + +TEST_CASE("from_raw_ptr_unowned+as_raw_ptr_release_ownership", "[E]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE_THROWS_WITH(poc::as_raw_ptr_release_ownership(hld), + "Cannot disown non-owning holder (as_raw_ptr_release_ownership)."); +} + +TEST_CASE("from_raw_ptr_unowned+as_unique_ptr", "[E]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE_THROWS_WITH(poc::as_unique_ptr(hld), + "Cannot disown non-owning holder (as_unique_ptr)."); +} + +TEST_CASE("from_raw_ptr_unowned+as_unique_ptr_with_deleter", "[E]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE_THROWS_WITH((poc::as_unique_ptr>(hld)), + "Missing unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_raw_ptr_unowned+as_shared_ptr", "[S]") { + static int value = 19; + auto hld = smart_holder::from_raw_ptr_unowned(&value); + REQUIRE(*hld.as_shared_ptr() == 19); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_lvalue_ref", "[S]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + REQUIRE(hld.has_pointee()); + REQUIRE(poc::as_lvalue_ref(hld) == 19); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_raw_ptr_release_ownership1", "[S]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + auto new_owner = std::unique_ptr(poc::as_raw_ptr_release_ownership(hld)); + REQUIRE(!hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_raw_ptr_release_ownership2", "[E]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + auto shd_ptr = hld.as_shared_ptr(); + REQUIRE_THROWS_WITH(poc::as_raw_ptr_release_ownership(hld), + "Cannot disown use_count != 1 (as_raw_ptr_release_ownership)."); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_unique_ptr1", "[S]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + std::unique_ptr new_owner = poc::as_unique_ptr(hld); + REQUIRE(!hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_unique_ptr2", "[E]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + auto shd_ptr = hld.as_shared_ptr(); + REQUIRE_THROWS_WITH(poc::as_unique_ptr(hld), + "Cannot disown use_count != 1 (as_unique_ptr)."); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_unique_ptr_with_deleter", "[E]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + REQUIRE_THROWS_WITH((poc::as_unique_ptr>(hld)), + "Missing unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_shared_ptr", "[S]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + std::shared_ptr new_owner = hld.as_shared_ptr(); + REQUIRE(hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_raw_ptr_take_ownership+disown+reclaim_disowned", "[S]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + std::unique_ptr new_owner(hld.as_raw_ptr_unowned()); + hld.disown(); + REQUIRE(poc::as_lvalue_ref(hld) == 19); + REQUIRE(*new_owner == 19); + hld.reclaim_disowned(); // Manually veriified: without this, clang++ -fsanitize=address reports + // "detected memory leaks". + // NOLINTNEXTLINE(bugprone-unused-return-value) + (void) new_owner.release(); // Manually verified: without this, clang++ -fsanitize=address + // reports "attempting double-free". + REQUIRE(poc::as_lvalue_ref(hld) == 19); + REQUIRE(new_owner.get() == nullptr); +} + +TEST_CASE("from_raw_ptr_take_ownership+disown+release_disowned", "[S]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + std::unique_ptr new_owner(hld.as_raw_ptr_unowned()); + hld.disown(); + REQUIRE(poc::as_lvalue_ref(hld) == 19); + REQUIRE(*new_owner == 19); + hld.release_disowned(); + REQUIRE(!hld.has_pointee()); +} + +TEST_CASE("from_raw_ptr_take_ownership+disown+ensure_is_not_disowned", "[E]") { + const char *context = "test_case"; + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + hld.ensure_is_not_disowned(context); // Does not throw. + std::unique_ptr new_owner(hld.as_raw_ptr_unowned()); + hld.disown(); + REQUIRE_THROWS_WITH(hld.ensure_is_not_disowned(context), + "Holder was disowned already (test_case)."); +} + +TEST_CASE("from_unique_ptr+as_lvalue_ref", "[S]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE(poc::as_lvalue_ref(hld) == 19); +} + +TEST_CASE("from_unique_ptr+as_raw_ptr_release_ownership1", "[S]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + auto new_owner = std::unique_ptr(poc::as_raw_ptr_release_ownership(hld)); + REQUIRE(!hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_unique_ptr+as_raw_ptr_release_ownership2", "[E]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + auto shd_ptr = hld.as_shared_ptr(); + REQUIRE_THROWS_WITH(poc::as_raw_ptr_release_ownership(hld), + "Cannot disown use_count != 1 (as_raw_ptr_release_ownership)."); +} + +TEST_CASE("from_unique_ptr+as_unique_ptr1", "[S]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + std::unique_ptr new_owner = poc::as_unique_ptr(hld); + REQUIRE(!hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_unique_ptr+as_unique_ptr2", "[E]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + auto shd_ptr = hld.as_shared_ptr(); + REQUIRE_THROWS_WITH(poc::as_unique_ptr(hld), + "Cannot disown use_count != 1 (as_unique_ptr)."); +} + +TEST_CASE("from_unique_ptr+as_unique_ptr_with_deleter", "[E]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE_THROWS_WITH((poc::as_unique_ptr>(hld)), + "Incompatible unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_unique_ptr+as_shared_ptr", "[S]") { + std::unique_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + std::shared_ptr new_owner = hld.as_shared_ptr(); + REQUIRE(hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_unique_ptr_derived+as_unique_ptr_base", "[S]") { + std::unique_ptr orig_owner(new helpers::derived()); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + std::unique_ptr new_owner = poc::as_unique_ptr(hld); + REQUIRE(!hld.has_pointee()); + REQUIRE(new_owner->get() == 100); +} + +TEST_CASE("from_unique_ptr_derived+as_unique_ptr_base2", "[E]") { + std::unique_ptr> orig_owner( + new helpers::derived()); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE_THROWS_WITH( + (poc::as_unique_ptr>(hld)), + "Incompatible unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_lvalue_ref", "[S]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE(poc::as_lvalue_ref(hld) == 19); +} + +TEST_CASE("from_unique_ptr_with_std_function_deleter+as_lvalue_ref", "[S]") { + std::unique_ptr> orig_owner( + new int(19), [](const int *raw_ptr) { delete raw_ptr; }); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE(poc::as_lvalue_ref(hld) == 19); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_raw_ptr_release_ownership", "[E]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE_THROWS_WITH(poc::as_raw_ptr_release_ownership(hld), + "Cannot disown custom deleter (as_raw_ptr_release_ownership)."); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_unique_ptr", "[E]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE_THROWS_WITH(poc::as_unique_ptr(hld), + "Incompatible unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_unique_ptr_with_deleter1", "[S]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + std::unique_ptr> new_owner + = poc::as_unique_ptr>(hld); + REQUIRE(!hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_unique_ptr_with_deleter2", "[E]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + REQUIRE_THROWS_WITH((poc::as_unique_ptr>(hld)), + "Incompatible unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_shared_ptr", "[S]") { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + REQUIRE(orig_owner.get() == nullptr); + std::shared_ptr new_owner = hld.as_shared_ptr(); + REQUIRE(hld.has_pointee()); + REQUIRE(*new_owner == 19); +} + +TEST_CASE("from_shared_ptr+as_lvalue_ref", "[S]") { + std::shared_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_shared_ptr(orig_owner); + REQUIRE(poc::as_lvalue_ref(hld) == 19); +} + +TEST_CASE("from_shared_ptr+as_raw_ptr_release_ownership", "[E]") { + std::shared_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_shared_ptr(orig_owner); + REQUIRE_THROWS_WITH(poc::as_raw_ptr_release_ownership(hld), + "Cannot disown external shared_ptr (as_raw_ptr_release_ownership)."); +} + +TEST_CASE("from_shared_ptr+as_unique_ptr", "[E]") { + std::shared_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_shared_ptr(orig_owner); + REQUIRE_THROWS_WITH(poc::as_unique_ptr(hld), + "Cannot disown external shared_ptr (as_unique_ptr)."); +} + +TEST_CASE("from_shared_ptr+as_unique_ptr_with_deleter", "[E]") { + std::shared_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_shared_ptr(orig_owner); + REQUIRE_THROWS_WITH((poc::as_unique_ptr>(hld)), + "Missing unique_ptr deleter (as_unique_ptr)."); +} + +TEST_CASE("from_shared_ptr+as_shared_ptr", "[S]") { + std::shared_ptr orig_owner(new int(19)); + auto hld = smart_holder::from_shared_ptr(orig_owner); + REQUIRE(*hld.as_shared_ptr() == 19); +} + +TEST_CASE("error_unpopulated_holder", "[E]") { + smart_holder hld; + REQUIRE_THROWS_WITH(poc::as_lvalue_ref(hld), "Unpopulated holder (as_lvalue_ref)."); +} + +TEST_CASE("error_disowned_holder", "[E]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + poc::as_unique_ptr(hld); + REQUIRE_THROWS_WITH(poc::as_lvalue_ref(hld), "Disowned holder (as_lvalue_ref)."); +} + +TEST_CASE("error_cannot_disown_nullptr", "[E]") { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + poc::as_unique_ptr(hld); + REQUIRE_THROWS_WITH(poc::as_unique_ptr(hld), "Cannot disown nullptr (as_unique_ptr)."); +} + +TEST_CASE("indestructible_int-from_raw_ptr_unowned+as_raw_ptr_unowned", "[S]") { + using zombie = helpers::indestructible_int; + // Using placement new instead of plain new, to not trigger leak sanitizer errors. + static std::aligned_storage::type memory_block[1]; + auto *value = new (memory_block) zombie(19); + auto hld = smart_holder::from_raw_ptr_unowned(value); + REQUIRE(hld.as_raw_ptr_unowned()->valu == 19); +} + +TEST_CASE("indestructible_int-from_raw_ptr_take_ownership", "[E]") { + helpers::indestructible_int *value = nullptr; + REQUIRE_THROWS_WITH(smart_holder::from_raw_ptr_take_ownership(value), + "Pointee is not destructible (from_raw_ptr_take_ownership)."); +} + +TEST_CASE("from_raw_ptr_take_ownership+as_shared_ptr-outliving_smart_holder", "[S]") { + // Exercises guarded_builtin_delete flag_ptr validity past destruction of smart_holder. + std::shared_ptr longer_living; + { + auto hld = smart_holder::from_raw_ptr_take_ownership(new int(19)); + longer_living = hld.as_shared_ptr(); + } + REQUIRE(*longer_living == 19); +} + +TEST_CASE("from_unique_ptr_with_deleter+as_shared_ptr-outliving_smart_holder", "[S]") { + // Exercises guarded_custom_deleter flag_ptr validity past destruction of smart_holder. + std::shared_ptr longer_living; + { + std::unique_ptr> orig_owner(new int(19)); + auto hld = smart_holder::from_unique_ptr(std::move(orig_owner)); + longer_living = hld.as_shared_ptr(); + } + REQUIRE(*longer_living == 19); +} diff --git a/tests/test_class.cpp b/tests/test_class.cpp index cb84c327a0..28dc313334 100644 --- a/tests/test_class.cpp +++ b/tests/test_class.cpp @@ -105,6 +105,12 @@ TEST_SUBMODULE(class_, m) { .def_static("__new__", [](const py::object &) { return NoConstructorNew::new_instance(); }); + // test_pass_unique_ptr + struct ToBeHeldByUniquePtr {}; + py::class_>(m, "ToBeHeldByUniquePtr") + .def(py::init<>()); + m.def("pass_unique_ptr", [](std::unique_ptr &&) {}); + // test_inheritance class Pet { public: @@ -227,11 +233,12 @@ TEST_SUBMODULE(class_, m) { m.def("mismatched_holder_1", []() { auto mod = py::module_::import("__main__"); py::class_>(mod, "MismatchBase1"); - py::class_(mod, "MismatchDerived1"); + py::class_, MismatchBase1>( + mod, "MismatchDerived1"); }); m.def("mismatched_holder_2", []() { auto mod = py::module_::import("__main__"); - py::class_(mod, "MismatchBase2"); + py::class_>(mod, "MismatchBase2"); py::class_, MismatchBase2>( mod, "MismatchDerived2"); }); @@ -625,8 +632,10 @@ CHECK_NOALIAS(8); CHECK_HOLDER(1, unique); CHECK_HOLDER(2, unique); CHECK_HOLDER(3, unique); +#ifndef PYBIND11_RUN_TESTING_WITH_SMART_HOLDER_AS_DEFAULT_BUT_NEVER_USE_IN_PRODUCTION_PLEASE CHECK_HOLDER(4, unique); CHECK_HOLDER(5, unique); +#endif CHECK_HOLDER(6, shared); CHECK_HOLDER(7, shared); CHECK_HOLDER(8, shared); diff --git a/tests/test_class.py b/tests/test_class.py index 01963d0122..2e11feb7b5 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -49,6 +49,16 @@ def test_instance_new(): assert cstats.alive() == 0 +def test_pass_unique_ptr(): + obj = m.ToBeHeldByUniquePtr() + with pytest.raises(RuntimeError) as execinfo: + m.pass_unique_ptr(obj) + assert str(execinfo.value).startswith( + "Passing `std::unique_ptr` from Python to C++ requires `py::class_` (with T = " + ) + assert "ToBeHeldByUniquePtr" in str(execinfo.value) + + def test_type(): assert m.check_type(1) == m.DerivedClass1 with pytest.raises(RuntimeError) as execinfo: diff --git a/tests/test_class_sh_basic.cpp b/tests/test_class_sh_basic.cpp new file mode 100644 index 0000000000..b89372b198 --- /dev/null +++ b/tests/test_class_sh_basic.cpp @@ -0,0 +1,247 @@ +#include "pybind11_tests.h" + +#include +#include +#include + +namespace pybind11_tests { +namespace class_sh_basic { + +struct atyp { // Short for "any type". + std::string mtxt; + atyp() : mtxt("DefaultConstructor") {} + explicit atyp(const std::string &mtxt_) : mtxt(mtxt_) {} + atyp(const atyp &other) { mtxt = other.mtxt + "_CpCtor"; } + atyp(atyp &&other) noexcept { mtxt = other.mtxt + "_MvCtor"; } +}; + +struct uconsumer { // unique_ptr consumer + std::unique_ptr held; + bool valid() const { return static_cast(held); } + + void pass_valu(std::unique_ptr obj) { held = std::move(obj); } + void pass_rref(std::unique_ptr &&obj) { held = std::move(obj); } + std::unique_ptr rtrn_valu() { return std::move(held); } + std::unique_ptr &rtrn_lref() { return held; } + const std::unique_ptr &rtrn_cref() const { return held; } +}; + +/// Custom deleter that is default constructible. +struct custom_deleter { + std::string trace_txt; + + custom_deleter() = default; + explicit custom_deleter(const std::string &trace_txt_) : trace_txt(trace_txt_) {} + + custom_deleter(const custom_deleter &other) { trace_txt = other.trace_txt + "_CpCtor"; } + + custom_deleter &operator=(const custom_deleter &rhs) { + trace_txt = rhs.trace_txt + "_CpLhs"; + return *this; + } + + custom_deleter(custom_deleter &&other) noexcept { + trace_txt = other.trace_txt + "_MvCtorTo"; + other.trace_txt += "_MvCtorFrom"; + } + + custom_deleter &operator=(custom_deleter &&rhs) noexcept { + trace_txt = rhs.trace_txt + "_MvLhs"; + rhs.trace_txt += "_MvRhs"; + return *this; + } + + void operator()(atyp *p) const { std::default_delete()(p); } + void operator()(const atyp *p) const { std::default_delete()(p); } +}; +static_assert(std::is_default_constructible::value, ""); + +/// Custom deleter that is not default constructible. +struct custom_deleter_nd : custom_deleter { + custom_deleter_nd() = delete; + explicit custom_deleter_nd(const std::string &trace_txt_) : custom_deleter(trace_txt_) {} +}; +static_assert(!std::is_default_constructible::value, ""); + +// clang-format off + +atyp rtrn_valu() { atyp obj{"rtrn_valu"}; return obj; } +atyp&& rtrn_rref() { static atyp obj; obj.mtxt = "rtrn_rref"; return std::move(obj); } +atyp const& rtrn_cref() { static atyp obj; obj.mtxt = "rtrn_cref"; return obj; } +atyp& rtrn_mref() { static atyp obj; obj.mtxt = "rtrn_mref"; return obj; } +atyp const* rtrn_cptr() { return new atyp{"rtrn_cptr"}; } +atyp* rtrn_mptr() { return new atyp{"rtrn_mptr"}; } + +std::string pass_valu(atyp obj) { return "pass_valu:" + obj.mtxt; } // NOLINT +std::string pass_cref(atyp const& obj) { return "pass_cref:" + obj.mtxt; } +std::string pass_mref(atyp& obj) { return "pass_mref:" + obj.mtxt; } +std::string pass_cptr(atyp const* obj) { return "pass_cptr:" + obj->mtxt; } +std::string pass_mptr(atyp* obj) { return "pass_mptr:" + obj->mtxt; } + +std::shared_ptr rtrn_shmp() { return std::make_shared("rtrn_shmp"); } +std::shared_ptr rtrn_shcp() { return std::shared_ptr(new atyp{"rtrn_shcp"}); } + +std::string pass_shmp(std::shared_ptr obj) { return "pass_shmp:" + obj->mtxt; } // NOLINT +std::string pass_shcp(std::shared_ptr obj) { return "pass_shcp:" + obj->mtxt; } // NOLINT + +std::unique_ptr rtrn_uqmp() { return std::unique_ptr(new atyp{"rtrn_uqmp"}); } +std::unique_ptr rtrn_uqcp() { return std::unique_ptr(new atyp{"rtrn_uqcp"}); } + +std::string pass_uqmp(std::unique_ptr obj) { return "pass_uqmp:" + obj->mtxt; } +std::string pass_uqcp(std::unique_ptr obj) { return "pass_uqcp:" + obj->mtxt; } + +struct sddm : std::default_delete {}; +struct sddc : std::default_delete {}; + +std::unique_ptr rtrn_udmp() { return std::unique_ptr(new atyp{"rtrn_udmp"}); } +std::unique_ptr rtrn_udcp() { return std::unique_ptr(new atyp{"rtrn_udcp"}); } + +std::string pass_udmp(std::unique_ptr obj) { return "pass_udmp:" + obj->mtxt; } +std::string pass_udcp(std::unique_ptr obj) { return "pass_udcp:" + obj->mtxt; } + +std::unique_ptr rtrn_udmp_del() { return std::unique_ptr(new atyp{"rtrn_udmp_del"}, custom_deleter{"udmp_deleter"}); } +std::unique_ptr rtrn_udcp_del() { return std::unique_ptr(new atyp{"rtrn_udcp_del"}, custom_deleter{"udcp_deleter"}); } + +std::string pass_udmp_del(std::unique_ptr obj) { return "pass_udmp_del:" + obj->mtxt + "," + obj.get_deleter().trace_txt; } +std::string pass_udcp_del(std::unique_ptr obj) { return "pass_udcp_del:" + obj->mtxt + "," + obj.get_deleter().trace_txt; } + +std::unique_ptr rtrn_udmp_del_nd() { return std::unique_ptr(new atyp{"rtrn_udmp_del_nd"}, custom_deleter_nd{"udmp_deleter_nd"}); } +std::unique_ptr rtrn_udcp_del_nd() { return std::unique_ptr(new atyp{"rtrn_udcp_del_nd"}, custom_deleter_nd{"udcp_deleter_nd"}); } + +std::string pass_udmp_del_nd(std::unique_ptr obj) { return "pass_udmp_del_nd:" + obj->mtxt + "," + obj.get_deleter().trace_txt; } +std::string pass_udcp_del_nd(std::unique_ptr obj) { return "pass_udcp_del_nd:" + obj->mtxt + "," + obj.get_deleter().trace_txt; } + +// clang-format on + +// Helpers for testing. +std::string get_mtxt(atyp const &obj) { return obj.mtxt; } +std::ptrdiff_t get_ptr(atyp const &obj) { return reinterpret_cast(&obj); } + +std::unique_ptr unique_ptr_roundtrip(std::unique_ptr obj) { return obj; } + +std::string pass_unique_ptr_cref(const std::unique_ptr &obj) { return obj->mtxt; } + +const std::unique_ptr &rtrn_unique_ptr_cref(const std::string &mtxt) { + static std::unique_ptr obj{new atyp{"static_ctor_arg"}}; + if (!mtxt.empty()) { + obj->mtxt = mtxt; + } + return obj; +} + +const std::unique_ptr &unique_ptr_cref_roundtrip(const std::unique_ptr &obj) { + return obj; +} + +struct SharedPtrStash { + std::vector> stash; + void Add(const std::shared_ptr &obj) { stash.push_back(obj); } +}; + +class LocalUnusualOpRef : UnusualOpRef {}; // To avoid clashing with `py::class_`. +py::object CastUnusualOpRefConstRef(const LocalUnusualOpRef &cref) { return py::cast(cref); } +py::object CastUnusualOpRefMovable(LocalUnusualOpRef &&mvbl) { return py::cast(std::move(mvbl)); } + +TEST_SUBMODULE(class_sh_basic, m) { + namespace py = pybind11; + + py::classh(m, "atyp").def(py::init<>()).def(py::init([](const std::string &mtxt) { + atyp obj; + obj.mtxt = mtxt; + return obj; + })); + + m.def("rtrn_valu", rtrn_valu); + m.def("rtrn_rref", rtrn_rref); + m.def("rtrn_cref", rtrn_cref); + m.def("rtrn_mref", rtrn_mref); + m.def("rtrn_cptr", rtrn_cptr); + m.def("rtrn_mptr", rtrn_mptr); + + m.def("pass_valu", pass_valu); + m.def("pass_cref", pass_cref); + m.def("pass_mref", pass_mref); + m.def("pass_cptr", pass_cptr); + m.def("pass_mptr", pass_mptr); + + m.def("rtrn_shmp", rtrn_shmp); + m.def("rtrn_shcp", rtrn_shcp); + + m.def("pass_shmp", pass_shmp); + m.def("pass_shcp", pass_shcp); + + m.def("rtrn_uqmp", rtrn_uqmp); + m.def("rtrn_uqcp", rtrn_uqcp); + + m.def("pass_uqmp", pass_uqmp); + m.def("pass_uqcp", pass_uqcp); + + m.def("rtrn_udmp", rtrn_udmp); + m.def("rtrn_udcp", rtrn_udcp); + + m.def("pass_udmp", pass_udmp); + m.def("pass_udcp", pass_udcp); + + m.def("rtrn_udmp_del", rtrn_udmp_del); + m.def("rtrn_udcp_del", rtrn_udcp_del); + + m.def("pass_udmp_del", pass_udmp_del); + m.def("pass_udcp_del", pass_udcp_del); + + m.def("rtrn_udmp_del_nd", rtrn_udmp_del_nd); + m.def("rtrn_udcp_del_nd", rtrn_udcp_del_nd); + + m.def("pass_udmp_del_nd", pass_udmp_del_nd); + m.def("pass_udcp_del_nd", pass_udcp_del_nd); + + py::classh(m, "uconsumer") + .def(py::init<>()) + .def("valid", &uconsumer::valid) + .def("pass_valu", &uconsumer::pass_valu) + .def("pass_rref", &uconsumer::pass_rref) + .def("rtrn_valu", &uconsumer::rtrn_valu) + .def("rtrn_lref", &uconsumer::rtrn_lref) + .def("rtrn_cref", &uconsumer::rtrn_cref); + + // Helpers for testing. + // These require selected functions above to work first, as indicated: + m.def("get_mtxt", get_mtxt); // pass_cref + m.def("get_ptr", get_ptr); // pass_cref + + m.def("unique_ptr_roundtrip", unique_ptr_roundtrip); // pass_uqmp, rtrn_uqmp + + m.def("pass_unique_ptr_cref", pass_unique_ptr_cref); + m.def("rtrn_unique_ptr_cref", rtrn_unique_ptr_cref); + m.def("unique_ptr_cref_roundtrip", unique_ptr_cref_roundtrip); + + py::classh(m, "SharedPtrStash") + .def(py::init<>()) + .def("Add", &SharedPtrStash::Add, py::arg("obj")); + + m.def("py_type_handle_of_atyp", []() { + return py::type::handle_of(); // Exercises static_cast in this function. + }); + + // Checks for type names used as arguments + m.def("args_shared_ptr", [](std::shared_ptr p) { return p; }); + m.def("args_shared_ptr_const", [](std::shared_ptr p) { return p; }); + m.def("args_unique_ptr", [](std::unique_ptr p) { return p; }); + m.def("args_unique_ptr_const", [](std::unique_ptr p) { return p; }); + + // Make sure unique_ptr type caster accept automatic_reference return value policy. + m.def( + "rtrn_uq_automatic_reference", + []() { return std::unique_ptr(new atyp("rtrn_uq_automatic_reference")); }, + pybind11::return_value_policy::automatic_reference); + + m.def("pass_shared_ptr_ptr", [](std::shared_ptr *) {}); + + py::classh(m, "LocalUnusualOpRef"); + m.def("CallCastUnusualOpRefConstRef", + []() { return CastUnusualOpRefConstRef(LocalUnusualOpRef()); }); + m.def("CallCastUnusualOpRefMovable", + []() { return CastUnusualOpRefMovable(LocalUnusualOpRef()); }); +} + +} // namespace class_sh_basic +} // namespace pybind11_tests diff --git a/tests/test_class_sh_basic.py b/tests/test_class_sh_basic.py new file mode 100644 index 0000000000..f07253a5df --- /dev/null +++ b/tests/test_class_sh_basic.py @@ -0,0 +1,246 @@ +# Importing re before pytest after observing a PyPy CI flake when importing pytest first. +from __future__ import annotations + +import re + +import pytest + +from pybind11_tests import class_sh_basic as m + + +def test_atyp_constructors(): + obj = m.atyp() + assert obj.__class__.__name__ == "atyp" + obj = m.atyp("") + assert obj.__class__.__name__ == "atyp" + obj = m.atyp("txtm") + assert obj.__class__.__name__ == "atyp" + + +@pytest.mark.parametrize( + ("rtrn_f", "expected"), + [ + (m.rtrn_valu, "rtrn_valu(_MvCtor)*_MvCtor"), + (m.rtrn_rref, "rtrn_rref(_MvCtor)*_MvCtor"), + (m.rtrn_cref, "rtrn_cref(_MvCtor)*_CpCtor"), + (m.rtrn_mref, "rtrn_mref(_MvCtor)*_CpCtor"), + (m.rtrn_cptr, "rtrn_cptr"), + (m.rtrn_mptr, "rtrn_mptr"), + (m.rtrn_shmp, "rtrn_shmp"), + (m.rtrn_shcp, "rtrn_shcp"), + (m.rtrn_uqmp, "rtrn_uqmp"), + (m.rtrn_uqcp, "rtrn_uqcp"), + (m.rtrn_udmp, "rtrn_udmp"), + (m.rtrn_udcp, "rtrn_udcp"), + ], +) +def test_cast(rtrn_f, expected): + assert re.match(expected, m.get_mtxt(rtrn_f())) + + +@pytest.mark.parametrize( + ("pass_f", "mtxt", "expected"), + [ + (m.pass_valu, "Valu", "pass_valu:Valu(_MvCtor)*_CpCtor"), + (m.pass_cref, "Cref", "pass_cref:Cref(_MvCtor)*_MvCtor"), + (m.pass_mref, "Mref", "pass_mref:Mref(_MvCtor)*_MvCtor"), + (m.pass_cptr, "Cptr", "pass_cptr:Cptr(_MvCtor)*_MvCtor"), + (m.pass_mptr, "Mptr", "pass_mptr:Mptr(_MvCtor)*_MvCtor"), + (m.pass_shmp, "Shmp", "pass_shmp:Shmp(_MvCtor)*_MvCtor"), + (m.pass_shcp, "Shcp", "pass_shcp:Shcp(_MvCtor)*_MvCtor"), + (m.pass_uqmp, "Uqmp", "pass_uqmp:Uqmp(_MvCtor)*_MvCtor"), + (m.pass_uqcp, "Uqcp", "pass_uqcp:Uqcp(_MvCtor)*_MvCtor"), + ], +) +def test_load_with_mtxt(pass_f, mtxt, expected): + assert re.match(expected, pass_f(m.atyp(mtxt))) + + +@pytest.mark.parametrize( + ("pass_f", "rtrn_f", "expected"), + [ + (m.pass_udmp, m.rtrn_udmp, "pass_udmp:rtrn_udmp"), + (m.pass_udcp, m.rtrn_udcp, "pass_udcp:rtrn_udcp"), + ], +) +def test_load_with_rtrn_f(pass_f, rtrn_f, expected): + assert pass_f(rtrn_f()) == expected + + +@pytest.mark.parametrize( + ("pass_f", "rtrn_f", "regex_expected"), + [ + ( + m.pass_udmp_del, + m.rtrn_udmp_del, + "pass_udmp_del:rtrn_udmp_del,udmp_deleter(_MvCtorTo)*_MvCtorTo", + ), + ( + m.pass_udcp_del, + m.rtrn_udcp_del, + "pass_udcp_del:rtrn_udcp_del,udcp_deleter(_MvCtorTo)*_MvCtorTo", + ), + ( + m.pass_udmp_del_nd, + m.rtrn_udmp_del_nd, + "pass_udmp_del_nd:rtrn_udmp_del_nd,udmp_deleter_nd(_MvCtorTo)*_MvCtorTo", + ), + ( + m.pass_udcp_del_nd, + m.rtrn_udcp_del_nd, + "pass_udcp_del_nd:rtrn_udcp_del_nd,udcp_deleter_nd(_MvCtorTo)*_MvCtorTo", + ), + ], +) +def test_deleter_roundtrip(pass_f, rtrn_f, regex_expected): + assert re.match(regex_expected, pass_f(rtrn_f())) + + +@pytest.mark.parametrize( + ("pass_f", "rtrn_f", "expected"), + [ + (m.pass_uqmp, m.rtrn_uqmp, "pass_uqmp:rtrn_uqmp"), + (m.pass_uqcp, m.rtrn_uqcp, "pass_uqcp:rtrn_uqcp"), + (m.pass_udmp, m.rtrn_udmp, "pass_udmp:rtrn_udmp"), + (m.pass_udcp, m.rtrn_udcp, "pass_udcp:rtrn_udcp"), + ], +) +def test_pass_unique_ptr_disowns(pass_f, rtrn_f, expected): + obj = rtrn_f() + assert pass_f(obj) == expected + with pytest.raises(ValueError) as exc_info: + pass_f(obj) + assert str(exc_info.value) == ( + "Missing value for wrapped C++ type" + + " `pybind11_tests::class_sh_basic::atyp`:" + + " Python instance was disowned." + ) + + +@pytest.mark.parametrize( + ("pass_f", "rtrn_f"), + [ + (m.pass_uqmp, m.rtrn_uqmp), + (m.pass_uqcp, m.rtrn_uqcp), + (m.pass_udmp, m.rtrn_udmp), + (m.pass_udcp, m.rtrn_udcp), + ], +) +def test_cannot_disown_use_count_ne_1(pass_f, rtrn_f): + obj = rtrn_f() + stash = m.SharedPtrStash() + stash.Add(obj) + with pytest.raises(ValueError) as exc_info: + pass_f(obj) + assert str(exc_info.value) == ("Cannot disown use_count != 1 (load_as_unique_ptr).") + + +def test_unique_ptr_roundtrip(num_round_trips=1000): + # Multiple roundtrips to stress-test instance registration/deregistration. + recycled = m.atyp("passenger") + for _ in range(num_round_trips): + id_orig = id(recycled) + recycled = m.unique_ptr_roundtrip(recycled) + assert re.match("passenger(_MvCtor)*_MvCtor", m.get_mtxt(recycled)) + id_rtrn = id(recycled) + # Ensure the returned object is a different Python instance. + assert id_rtrn != id_orig + id_orig = id_rtrn + + +def test_pass_unique_ptr_cref(): + obj = m.atyp("ctor_arg") + assert re.match("ctor_arg(_MvCtor)*_MvCtor", m.get_mtxt(obj)) + assert re.match("ctor_arg(_MvCtor)*_MvCtor", m.pass_unique_ptr_cref(obj)) + assert re.match("ctor_arg(_MvCtor)*_MvCtor", m.get_mtxt(obj)) + + +def test_rtrn_unique_ptr_cref(): + obj0 = m.rtrn_unique_ptr_cref("") + assert m.get_mtxt(obj0) == "static_ctor_arg" + obj1 = m.rtrn_unique_ptr_cref("passed_mtxt_1") + assert m.get_mtxt(obj1) == "passed_mtxt_1" + assert m.get_mtxt(obj0) == "passed_mtxt_1" + assert obj0 is obj1 + + +def test_unique_ptr_cref_roundtrip(num_round_trips=1000): + # Multiple roundtrips to stress-test implementation. + orig = m.atyp("passenger") + mtxt_orig = m.get_mtxt(orig) + recycled = orig + for _ in range(num_round_trips): + recycled = m.unique_ptr_cref_roundtrip(recycled) + assert recycled is orig + assert m.get_mtxt(recycled) == mtxt_orig + + +@pytest.mark.parametrize( + ("pass_f", "rtrn_f", "moved_out", "moved_in"), + [ + (m.uconsumer.pass_valu, m.uconsumer.rtrn_valu, True, True), + (m.uconsumer.pass_rref, m.uconsumer.rtrn_valu, True, True), + (m.uconsumer.pass_valu, m.uconsumer.rtrn_lref, True, False), + (m.uconsumer.pass_valu, m.uconsumer.rtrn_cref, True, False), + ], +) +def test_unique_ptr_consumer_roundtrip(pass_f, rtrn_f, moved_out, moved_in): + c = m.uconsumer() + assert not c.valid() + recycled = m.atyp("passenger") + mtxt_orig = m.get_mtxt(recycled) + assert re.match("passenger_(MvCtor){1,2}", mtxt_orig) + + pass_f(c, recycled) + if moved_out: + with pytest.raises(ValueError) as excinfo: + m.get_mtxt(recycled) + assert "Python instance was disowned" in str(excinfo.value) + + recycled = rtrn_f(c) + assert c.valid() != moved_in + assert m.get_mtxt(recycled) == mtxt_orig + + +def test_py_type_handle_of_atyp(): + obj = m.py_type_handle_of_atyp() + assert obj.__class__.__name__ == "pybind11_type" + + +def test_function_signatures(doc): + assert ( + doc(m.args_shared_ptr) + == "args_shared_ptr(arg0: m.class_sh_basic.atyp) -> m.class_sh_basic.atyp" + ) + assert ( + doc(m.args_shared_ptr_const) + == "args_shared_ptr_const(arg0: m.class_sh_basic.atyp) -> m.class_sh_basic.atyp" + ) + assert ( + doc(m.args_unique_ptr) + == "args_unique_ptr(arg0: m.class_sh_basic.atyp) -> m.class_sh_basic.atyp" + ) + assert ( + doc(m.args_unique_ptr_const) + == "args_unique_ptr_const(arg0: m.class_sh_basic.atyp) -> m.class_sh_basic.atyp" + ) + + +def test_unique_ptr_return_value_policy_automatic_reference(): + assert m.get_mtxt(m.rtrn_uq_automatic_reference()) == "rtrn_uq_automatic_reference" + + +def test_pass_shared_ptr_ptr(): + obj = m.atyp() + with pytest.raises(RuntimeError) as excinfo: + m.pass_shared_ptr_ptr(obj) + assert str(excinfo.value) == ( + "Passing `std::shared_ptr *` from Python to C++ is not supported" + " (inherently unsafe)." + ) + + +def test_unusual_op_ref(): + # Merely to test that this still exists and built successfully. + assert m.CallCastUnusualOpRefConstRef().__class__.__name__ == "LocalUnusualOpRef" + assert m.CallCastUnusualOpRefMovable().__class__.__name__ == "LocalUnusualOpRef" diff --git a/tests/test_class_sh_disowning.cpp b/tests/test_class_sh_disowning.cpp new file mode 100644 index 0000000000..490e6d59f2 --- /dev/null +++ b/tests/test_class_sh_disowning.cpp @@ -0,0 +1,41 @@ +#include "pybind11_tests.h" + +#include + +namespace pybind11_tests { +namespace class_sh_disowning { + +template // Using int as a trick to easily generate a series of types. +struct Atype { + int val = 0; + explicit Atype(int val_) : val{val_} {} + int get() const { return val * 10 + SerNo; } +}; + +int same_twice(std::unique_ptr> at1a, std::unique_ptr> at1b) { + return at1a->get() * 100 + at1b->get() * 10; +} + +int mixed(std::unique_ptr> at1, std::unique_ptr> at2) { + return at1->get() * 200 + at2->get() * 20; +} + +int overloaded(std::unique_ptr> at1, int i) { return at1->get() * 30 + i; } +int overloaded(std::unique_ptr> at2, int i) { return at2->get() * 40 + i; } + +} // namespace class_sh_disowning +} // namespace pybind11_tests + +TEST_SUBMODULE(class_sh_disowning, m) { + using namespace pybind11_tests::class_sh_disowning; + + py::classh>(m, "Atype1").def(py::init()).def("get", &Atype<1>::get); + py::classh>(m, "Atype2").def(py::init()).def("get", &Atype<2>::get); + + m.def("same_twice", same_twice); + + m.def("mixed", mixed); + + m.def("overloaded", (int (*)(std::unique_ptr>, int)) &overloaded); + m.def("overloaded", (int (*)(std::unique_ptr>, int)) &overloaded); +} diff --git a/tests/test_class_sh_disowning.py b/tests/test_class_sh_disowning.py new file mode 100644 index 0000000000..b9e648999f --- /dev/null +++ b/tests/test_class_sh_disowning.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import pytest + +from pybind11_tests import class_sh_disowning as m + + +def is_disowned(obj): + try: + obj.get() + except ValueError: + return True + return False + + +def test_same_twice(): + while True: + obj1a = m.Atype1(57) + obj1b = m.Atype1(62) + assert m.same_twice(obj1a, obj1b) == (57 * 10 + 1) * 100 + (62 * 10 + 1) * 10 + assert is_disowned(obj1a) + assert is_disowned(obj1b) + obj1c = m.Atype1(0) + with pytest.raises(ValueError): + # Disowning works for one argument, but not both. + m.same_twice(obj1c, obj1c) + assert is_disowned(obj1c) + return # Comment out for manual leak checking (use `top` command). + + +def test_mixed(): + first_pass = True + while True: + obj1a = m.Atype1(90) + obj2a = m.Atype2(25) + assert m.mixed(obj1a, obj2a) == (90 * 10 + 1) * 200 + (25 * 10 + 2) * 20 + assert is_disowned(obj1a) + assert is_disowned(obj2a) + + # The C++ order of evaluation of function arguments is (unfortunately) unspecified: + # https://en.cppreference.com/w/cpp/language/eval_order + # Read on. + obj1b = m.Atype1(0) + with pytest.raises(ValueError): + # If the 1st argument is evaluated first, obj1b is disowned before the conversion for + # the already disowned obj2a fails as expected. + m.mixed(obj1b, obj2a) + obj2b = m.Atype2(0) + with pytest.raises(ValueError): + # If the 2nd argument is evaluated first, obj2b is disowned before the conversion for + # the already disowned obj1a fails as expected. + m.mixed(obj1a, obj2b) + + # Either obj1b or obj2b was disowned in the expected failed m.mixed() calls above, but not + # both. + is_disowned_results = (is_disowned(obj1b), is_disowned(obj2b)) + assert is_disowned_results.count(True) == 1 + if first_pass: + first_pass = False + ix = is_disowned_results.index(True) + 1 + print(f"\nC++ function argument {ix} is evaluated first.") + + return # Comment out for manual leak checking (use `top` command). + + +def test_overloaded(): + while True: + obj1 = m.Atype1(81) + obj2 = m.Atype2(60) + with pytest.raises(TypeError): + m.overloaded(obj1, "NotInt") + assert obj1.get() == 81 * 10 + 1 # Not disowned. + assert m.overloaded(obj1, 3) == (81 * 10 + 1) * 30 + 3 + with pytest.raises(TypeError): + m.overloaded(obj2, "NotInt") + assert obj2.get() == 60 * 10 + 2 # Not disowned. + assert m.overloaded(obj2, 2) == (60 * 10 + 2) * 40 + 2 + return # Comment out for manual leak checking (use `top` command). diff --git a/tests/test_class_sh_disowning_mi.cpp b/tests/test_class_sh_disowning_mi.cpp new file mode 100644 index 0000000000..d0ffd45ec0 --- /dev/null +++ b/tests/test_class_sh_disowning_mi.cpp @@ -0,0 +1,85 @@ +#include "pybind11_tests.h" + +#include + +namespace pybind11_tests { +namespace class_sh_disowning_mi { + +// Diamond inheritance (copied from test_multiple_inheritance.cpp). +struct B { + int val_b = 10; + B() = default; + B(const B &) = default; + virtual ~B() = default; +}; + +struct C0 : public virtual B { + int val_c0 = 20; +}; + +struct C1 : public virtual B { + int val_c1 = 21; +}; + +struct D : public C0, public C1 { + int val_d = 30; +}; + +void disown_b(std::unique_ptr) {} + +// test_multiple_inheritance_python +struct Base1 { + explicit Base1(int i) : i(i) {} + int foo() const { return i; } + int i; +}; + +struct Base2 { + explicit Base2(int j) : j(j) {} + int bar() const { return j; } + int j; +}; + +int disown_base1(std::unique_ptr b1) { return b1->i * 2000 + 1; } +int disown_base2(std::unique_ptr b2) { return b2->j * 2000 + 2; } + +} // namespace class_sh_disowning_mi +} // namespace pybind11_tests + +TEST_SUBMODULE(class_sh_disowning_mi, m) { + using namespace pybind11_tests::class_sh_disowning_mi; + + py::classh(m, "B") + .def(py::init<>()) + .def_readonly("val_b", &D::val_b) + .def("b", [](B *self) { return self; }) + .def("get", [](const B &self) { return self.val_b; }); + + py::classh(m, "C0") + .def(py::init<>()) + .def_readonly("val_c0", &D::val_c0) + .def("c0", [](C0 *self) { return self; }) + .def("get", [](const C0 &self) { return self.val_b * 100 + self.val_c0; }); + + py::classh(m, "C1") + .def(py::init<>()) + .def_readonly("val_c1", &D::val_c1) + .def("c1", [](C1 *self) { return self; }) + .def("get", [](const C1 &self) { return self.val_b * 100 + self.val_c1; }); + + py::classh(m, "D") + .def(py::init<>()) + .def_readonly("val_d", &D::val_d) + .def("d", [](D *self) { return self; }) + .def("get", [](const D &self) { + return self.val_b * 1000000 + self.val_c0 * 10000 + self.val_c1 * 100 + self.val_d; + }); + + m.def("disown_b", disown_b); + + // test_multiple_inheritance_python + py::classh(m, "Base1").def(py::init()).def("foo", &Base1::foo); + py::classh(m, "Base2").def(py::init()).def("bar", &Base2::bar); + m.def("disown_base1", disown_base1); + m.def("disown_base2", disown_base2); +} diff --git a/tests/test_class_sh_disowning_mi.py b/tests/test_class_sh_disowning_mi.py new file mode 100644 index 0000000000..4a4beecce1 --- /dev/null +++ b/tests/test_class_sh_disowning_mi.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +import pytest + +import env # noqa: F401 +from pybind11_tests import class_sh_disowning_mi as m + + +def test_diamond_inheritance(): + # Very similar to test_multiple_inheritance.py:test_diamond_inheritance. + d = m.D() + assert d is d.d() + assert d is d.c0() + assert d is d.c1() + assert d is d.b() + assert d is d.c0().b() + assert d is d.c1().b() + assert d is d.c0().c1().b().c0().b() + + +def is_disowned(callable_method): + try: + callable_method() + except ValueError as e: + assert "Python instance was disowned" in str(e) # noqa: PT017 + return True + return False + + +def test_disown_b(): + b = m.B() + assert b.get() == 10 + m.disown_b(b) + assert is_disowned(b.get) + + +@pytest.mark.parametrize("var_to_disown", ["c0", "b"]) +def test_disown_c0(var_to_disown): + c0 = m.C0() + assert c0.get() == 1020 + b = c0.b() + m.disown_b(locals()[var_to_disown]) + assert is_disowned(c0.get) + assert is_disowned(b.get) + + +@pytest.mark.parametrize("var_to_disown", ["c1", "b"]) +def test_disown_c1(var_to_disown): + c1 = m.C1() + assert c1.get() == 1021 + b = c1.b() + m.disown_b(locals()[var_to_disown]) + assert is_disowned(c1.get) + assert is_disowned(b.get) + + +@pytest.mark.parametrize("var_to_disown", ["d", "c1", "c0", "b"]) +def test_disown_d(var_to_disown): + d = m.D() + assert d.get() == 10202130 + b = d.b() + c0 = d.c0() + c1 = d.c1() + m.disown_b(locals()[var_to_disown]) + assert is_disowned(d.get) + assert is_disowned(c1.get) + assert is_disowned(c0.get) + assert is_disowned(b.get) + + +# Based on test_multiple_inheritance.py:test_multiple_inheritance_python. +class MI1(m.Base1, m.Base2): + def __init__(self, i, j): + m.Base1.__init__(self, i) + m.Base2.__init__(self, j) + + +class B1: + def v(self): + return 1 + + +class MI2(B1, m.Base1, m.Base2): + def __init__(self, i, j): + B1.__init__(self) + m.Base1.__init__(self, i) + m.Base2.__init__(self, j) + + +class MI3(MI2): + def __init__(self, i, j): + MI2.__init__(self, i, j) + + +class MI4(MI3, m.Base2): + def __init__(self, i, j): + MI3.__init__(self, i, j) + # This should be ignored (Base2 is already initialized via MI2): + m.Base2.__init__(self, i + 100) + + +class MI5(m.Base2, B1, m.Base1): + def __init__(self, i, j): + B1.__init__(self) + m.Base1.__init__(self, i) + m.Base2.__init__(self, j) + + +class MI6(m.Base2, B1): + def __init__(self, i): + m.Base2.__init__(self, i) + B1.__init__(self) + + +class B2(B1): + def v(self): + return 2 + + +class B3: + def v(self): + return 3 + + +class B4(B3, B2): + def v(self): + return 4 + + +class MI7(B4, MI6): + def __init__(self, i): + B4.__init__(self) + MI6.__init__(self, i) + + +class MI8(MI6, B3): + def __init__(self, i): + MI6.__init__(self, i) + B3.__init__(self) + + +class MI8b(B3, MI6): + def __init__(self, i): + B3.__init__(self) + MI6.__init__(self, i) + + +@pytest.mark.xfail("env.PYPY") +def test_multiple_inheritance_python(): + # Based on test_multiple_inheritance.py:test_multiple_inheritance_python. + # Exercises values_and_holders with 2 value_and_holder instances. + + mi1 = MI1(1, 2) + assert mi1.foo() == 1 + assert mi1.bar() == 2 + + mi2 = MI2(3, 4) + assert mi2.v() == 1 + assert mi2.foo() == 3 + assert mi2.bar() == 4 + + mi3 = MI3(5, 6) + assert mi3.v() == 1 + assert mi3.foo() == 5 + assert mi3.bar() == 6 + + mi4 = MI4(7, 8) + assert mi4.v() == 1 + assert mi4.foo() == 7 + assert mi4.bar() == 8 + + mi5 = MI5(10, 11) + assert mi5.v() == 1 + assert mi5.foo() == 10 + assert mi5.bar() == 11 + + mi6 = MI6(12) + assert mi6.v() == 1 + assert mi6.bar() == 12 + + mi7 = MI7(13) + assert mi7.v() == 4 + assert mi7.bar() == 13 + + mi8 = MI8(14) + assert mi8.v() == 1 + assert mi8.bar() == 14 + + mi8b = MI8b(15) + assert mi8b.v() == 3 + assert mi8b.bar() == 15 + + +DISOWN_CLS_I_J_V_LIST = [ + (MI1, 1, 2, None), + (MI2, 3, 4, 1), + (MI3, 5, 6, 1), + (MI4, 7, 8, 1), + (MI5, 10, 11, 1), +] + + +@pytest.mark.xfail("env.PYPY", strict=False) +@pytest.mark.parametrize(("cls", "i", "j", "v"), DISOWN_CLS_I_J_V_LIST) +def test_disown_base1_first(cls, i, j, v): + obj = cls(i, j) + assert obj.foo() == i + assert m.disown_base1(obj) == 2000 * i + 1 + assert is_disowned(obj.foo) + assert obj.bar() == j + assert m.disown_base2(obj) == 2000 * j + 2 + assert is_disowned(obj.bar) + if v is not None: + assert obj.v() == v + + +@pytest.mark.xfail("env.PYPY", strict=False) +@pytest.mark.parametrize(("cls", "i", "j", "v"), DISOWN_CLS_I_J_V_LIST) +def test_disown_base2_first(cls, i, j, v): + obj = cls(i, j) + assert obj.bar() == j + assert m.disown_base2(obj) == 2000 * j + 2 + assert is_disowned(obj.bar) + assert obj.foo() == i + assert m.disown_base1(obj) == 2000 * i + 1 + assert is_disowned(obj.foo) + if v is not None: + assert obj.v() == v + + +@pytest.mark.xfail("env.PYPY", strict=False) +@pytest.mark.parametrize( + ("cls", "j", "v"), + [ + (MI6, 12, 1), + (MI7, 13, 4), + (MI8, 14, 1), + (MI8b, 15, 3), + ], +) +def test_disown_base2(cls, j, v): + obj = cls(j) + assert obj.bar() == j + assert m.disown_base2(obj) == 2000 * j + 2 + assert is_disowned(obj.bar) + assert obj.v() == v diff --git a/tests/test_class_sh_factory_constructors.cpp b/tests/test_class_sh_factory_constructors.cpp new file mode 100644 index 0000000000..b3a8daea51 --- /dev/null +++ b/tests/test_class_sh_factory_constructors.cpp @@ -0,0 +1,164 @@ +#include "pybind11_tests.h" + +#include +#include + +namespace pybind11_tests { +namespace class_sh_factory_constructors { + +template // Using int as a trick to easily generate a series of types. +struct atyp { // Short for "any type". + std::string mtxt; +}; + +template +std::string get_mtxt(const T &obj) { + return obj.mtxt; +} + +using atyp_valu = atyp<0x0>; +using atyp_rref = atyp<0x1>; +using atyp_cref = atyp<0x2>; +using atyp_mref = atyp<0x3>; +using atyp_cptr = atyp<0x4>; +using atyp_mptr = atyp<0x5>; +using atyp_shmp = atyp<0x6>; +using atyp_shcp = atyp<0x7>; +using atyp_uqmp = atyp<0x8>; +using atyp_uqcp = atyp<0x9>; +using atyp_udmp = atyp<0xA>; +using atyp_udcp = atyp<0xB>; + +// clang-format off + +atyp_valu rtrn_valu() { atyp_valu obj{"Valu"}; return obj; } +atyp_rref&& rtrn_rref() { static atyp_rref obj; obj.mtxt = "Rref"; return std::move(obj); } +atyp_cref const& rtrn_cref() { static atyp_cref obj; obj.mtxt = "Cref"; return obj; } +atyp_mref& rtrn_mref() { static atyp_mref obj; obj.mtxt = "Mref"; return obj; } +atyp_cptr const* rtrn_cptr() { return new atyp_cptr{"Cptr"}; } +atyp_mptr* rtrn_mptr() { return new atyp_mptr{"Mptr"}; } + +std::shared_ptr rtrn_shmp() { return std::make_shared(atyp_shmp{"Shmp"}); } +std::shared_ptr rtrn_shcp() { return std::shared_ptr(new atyp_shcp{"Shcp"}); } + +std::unique_ptr rtrn_uqmp() { return std::unique_ptr(new atyp_uqmp{"Uqmp"}); } +std::unique_ptr rtrn_uqcp() { return std::unique_ptr(new atyp_uqcp{"Uqcp"}); } + +struct sddm : std::default_delete {}; +struct sddc : std::default_delete {}; + +std::unique_ptr rtrn_udmp() { return std::unique_ptr(new atyp_udmp{"Udmp"}); } +std::unique_ptr rtrn_udcp() { return std::unique_ptr(new atyp_udcp{"Udcp"}); } + +// clang-format on + +// Minimalistic approach to achieve full coverage of construct() overloads for constructing +// smart_holder from unique_ptr and shared_ptr returns. +struct with_alias { + int val = 0; + virtual ~with_alias() = default; + // Some compilers complain about implicitly defined versions of some of the following: + with_alias() = default; + with_alias(const with_alias &) = default; + with_alias(with_alias &&) = default; + with_alias &operator=(const with_alias &) = default; + with_alias &operator=(with_alias &&) = default; +}; +struct with_alias_alias : with_alias {}; +struct sddwaa : std::default_delete {}; + +} // namespace class_sh_factory_constructors +} // namespace pybind11_tests + +TEST_SUBMODULE(class_sh_factory_constructors, m) { + using namespace pybind11_tests::class_sh_factory_constructors; + + py::classh(m, "atyp_valu") + .def(py::init(&rtrn_valu)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_rref") + .def(py::init(&rtrn_rref)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_cref") + // class_: ... must return a compatible ... + // classh: ... cannot pass object of non-trivial type ... + // .def(py::init(&rtrn_cref)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_mref") + // class_: ... must return a compatible ... + // classh: ... cannot pass object of non-trivial type ... + // .def(py::init(&rtrn_mref)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_cptr") + // class_: ... must return a compatible ... + // classh: ... must return a compatible ... + // .def(py::init(&rtrn_cptr)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_mptr") + .def(py::init(&rtrn_mptr)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_shmp") + .def(py::init(&rtrn_shmp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_shcp") + // py::class_>(m, "atyp_shcp") + // class_: ... must return a compatible ... + // classh: ... cannot pass object of non-trivial type ... + // .def(py::init(&rtrn_shcp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_uqmp") + .def(py::init(&rtrn_uqmp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_uqcp") + // class_: ... cannot pass object of non-trivial type ... + // classh: ... cannot pass object of non-trivial type ... + // .def(py::init(&rtrn_uqcp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_udmp") + .def(py::init(&rtrn_udmp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "atyp_udcp") + // py::class_>(m, "atyp_udcp") + // class_: ... must return a compatible ... + // classh: ... cannot pass object of non-trivial type ... + // .def(py::init(&rtrn_udcp)) + .def("get_mtxt", get_mtxt); + + py::classh(m, "with_alias") + .def_readonly("val", &with_alias::val) + .def(py::init([](int i) { + auto p = std::unique_ptr(new with_alias_alias); + p->val = i * 100; + return p; + })) + .def(py::init([](int i, int j) { + auto p = std::unique_ptr(new with_alias_alias); + p->val = i * 100 + j * 10; + return p; + })) + .def(py::init([](int i, int j, int k) { + auto p = std::make_shared(); + p->val = i * 100 + j * 10 + k; + return p; + })) + .def(py::init( + [](int, int, int, int) { return std::unique_ptr(new with_alias); }, + [](int, int, int, int) { + return std::unique_ptr(new with_alias); // Invalid alias factory. + })) + .def(py::init([](int, int, int, int, int) { return std::make_shared(); }, + [](int, int, int, int, int) { + return std::make_shared(); // Invalid alias factory. + })); +} diff --git a/tests/test_class_sh_factory_constructors.py b/tests/test_class_sh_factory_constructors.py new file mode 100644 index 0000000000..5d45db6fd5 --- /dev/null +++ b/tests/test_class_sh_factory_constructors.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import pytest + +from pybind11_tests import class_sh_factory_constructors as m + + +def test_atyp_factories(): + assert m.atyp_valu().get_mtxt() == "Valu" + assert m.atyp_rref().get_mtxt() == "Rref" + # sert m.atyp_cref().get_mtxt() == "Cref" + # sert m.atyp_mref().get_mtxt() == "Mref" + # sert m.atyp_cptr().get_mtxt() == "Cptr" + assert m.atyp_mptr().get_mtxt() == "Mptr" + assert m.atyp_shmp().get_mtxt() == "Shmp" + # sert m.atyp_shcp().get_mtxt() == "Shcp" + assert m.atyp_uqmp().get_mtxt() == "Uqmp" + # sert m.atyp_uqcp().get_mtxt() == "Uqcp" + assert m.atyp_udmp().get_mtxt() == "Udmp" + # sert m.atyp_udcp().get_mtxt() == "Udcp" + + +@pytest.mark.parametrize( + ("init_args", "expected"), + [ + ((3,), 300), + ((5, 7), 570), + ((9, 11, 13), 1023), + ], +) +def test_with_alias_success(init_args, expected): + assert m.with_alias(*init_args).val == expected + + +@pytest.mark.parametrize( + ("num_init_args", "smart_ptr"), + [ + (4, "std::unique_ptr"), + (5, "std::shared_ptr"), + ], +) +def test_with_alias_invalid(num_init_args, smart_ptr): + class PyDrvdWithAlias(m.with_alias): + pass + + with pytest.raises(TypeError) as excinfo: + PyDrvdWithAlias(*((0,) * num_init_args)) + assert ( + str(excinfo.value) + == "pybind11::init(): construction failed: returned " + + smart_ptr + + " pointee is not an alias instance" + ) diff --git a/tests/test_class_sh_inheritance.cpp b/tests/test_class_sh_inheritance.cpp new file mode 100644 index 0000000000..8bdd0a7f88 --- /dev/null +++ b/tests/test_class_sh_inheritance.cpp @@ -0,0 +1,90 @@ +#include "pybind11_tests.h" + +#include + +namespace pybind11_tests { +namespace class_sh_inheritance { + +template +struct base_template { + base_template() : base_id(Id) {} + virtual ~base_template() = default; + virtual int id() const { return base_id; } + int base_id; + + // Some compilers complain about implicitly defined versions of some of the following: + base_template(const base_template &) = default; + base_template(base_template &&) noexcept = default; + base_template &operator=(const base_template &) = default; + base_template &operator=(base_template &&) noexcept = default; +}; + +using base = base_template<100>; + +struct drvd : base { + int id() const override { return 2 * base_id; } +}; + +// clang-format off +inline drvd *rtrn_mptr_drvd() { return new drvd; } +inline base *rtrn_mptr_drvd_up_cast() { return new drvd; } + +inline int pass_cptr_base(base const *b) { return b->id() + 11; } +inline int pass_cptr_drvd(drvd const *d) { return d->id() + 12; } + +inline std::shared_ptr rtrn_shmp_drvd() { return std::make_shared(); } +inline std::shared_ptr rtrn_shmp_drvd_up_cast() { return std::make_shared(); } + +inline int pass_shcp_base(const std::shared_ptr& b) { return b->id() + 21; } +inline int pass_shcp_drvd(const std::shared_ptr& d) { return d->id() + 22; } +// clang-format on + +using base1 = base_template<110>; +using base2 = base_template<120>; + +// Not reusing base here because it would interfere with the single-inheritance test. +struct drvd2 : base1, base2 { + int id() const override { return 3 * base1::base_id + 4 * base2::base_id; } +}; + +// clang-format off +inline drvd2 *rtrn_mptr_drvd2() { return new drvd2; } +inline base1 *rtrn_mptr_drvd2_up_cast1() { return new drvd2; } +inline base2 *rtrn_mptr_drvd2_up_cast2() { return new drvd2; } + +inline int pass_cptr_base1(base1 const *b) { return b->id() + 21; } +inline int pass_cptr_base2(base2 const *b) { return b->id() + 22; } +inline int pass_cptr_drvd2(drvd2 const *d) { return d->id() + 23; } +// clang-format on + +TEST_SUBMODULE(class_sh_inheritance, m) { + py::classh(m, "base"); + py::classh(m, "drvd"); + + auto rvto = py::return_value_policy::take_ownership; + + m.def("rtrn_mptr_drvd", rtrn_mptr_drvd, rvto); + m.def("rtrn_mptr_drvd_up_cast", rtrn_mptr_drvd_up_cast, rvto); + m.def("pass_cptr_base", pass_cptr_base); + m.def("pass_cptr_drvd", pass_cptr_drvd); + + m.def("rtrn_shmp_drvd", rtrn_shmp_drvd); + m.def("rtrn_shmp_drvd_up_cast", rtrn_shmp_drvd_up_cast); + m.def("pass_shcp_base", pass_shcp_base); + m.def("pass_shcp_drvd", pass_shcp_drvd); + + // __init__ needed for Python inheritance. + py::classh(m, "base1").def(py::init<>()); + py::classh(m, "base2").def(py::init<>()); + py::classh(m, "drvd2"); + + m.def("rtrn_mptr_drvd2", rtrn_mptr_drvd2, rvto); + m.def("rtrn_mptr_drvd2_up_cast1", rtrn_mptr_drvd2_up_cast1, rvto); + m.def("rtrn_mptr_drvd2_up_cast2", rtrn_mptr_drvd2_up_cast2, rvto); + m.def("pass_cptr_base1", pass_cptr_base1); + m.def("pass_cptr_base2", pass_cptr_base2); + m.def("pass_cptr_drvd2", pass_cptr_drvd2); +} + +} // namespace class_sh_inheritance +} // namespace pybind11_tests diff --git a/tests/test_class_sh_inheritance.py b/tests/test_class_sh_inheritance.py new file mode 100644 index 0000000000..cd9d6f47e2 --- /dev/null +++ b/tests/test_class_sh_inheritance.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from pybind11_tests import class_sh_inheritance as m + + +def test_rtrn_mptr_drvd_pass_cptr_base(): + d = m.rtrn_mptr_drvd() + i = m.pass_cptr_base(d) # load_impl Case 2a + assert i == 2 * 100 + 11 + + +def test_rtrn_shmp_drvd_pass_shcp_base(): + d = m.rtrn_shmp_drvd() + i = m.pass_shcp_base(d) # load_impl Case 2a + assert i == 2 * 100 + 21 + + +def test_rtrn_mptr_drvd_up_cast_pass_cptr_drvd(): + b = m.rtrn_mptr_drvd_up_cast() + # the base return is down-cast immediately. + assert b.__class__.__name__ == "drvd" + i = m.pass_cptr_drvd(b) + assert i == 2 * 100 + 12 + + +def test_rtrn_shmp_drvd_up_cast_pass_shcp_drvd(): + b = m.rtrn_shmp_drvd_up_cast() + # the base return is down-cast immediately. + assert b.__class__.__name__ == "drvd" + i = m.pass_shcp_drvd(b) + assert i == 2 * 100 + 22 + + +def test_rtrn_mptr_drvd2_pass_cptr_bases(): + d = m.rtrn_mptr_drvd2() + i1 = m.pass_cptr_base1(d) # load_impl Case 2c + assert i1 == 3 * 110 + 4 * 120 + 21 + i2 = m.pass_cptr_base2(d) + assert i2 == 3 * 110 + 4 * 120 + 22 + + +def test_rtrn_mptr_drvd2_up_casts_pass_cptr_drvd2(): + b1 = m.rtrn_mptr_drvd2_up_cast1() + assert b1.__class__.__name__ == "drvd2" + i1 = m.pass_cptr_drvd2(b1) + assert i1 == 3 * 110 + 4 * 120 + 23 + b2 = m.rtrn_mptr_drvd2_up_cast2() + assert b2.__class__.__name__ == "drvd2" + i2 = m.pass_cptr_drvd2(b2) + assert i2 == 3 * 110 + 4 * 120 + 23 + + +def test_python_drvd2(): + class Drvd2(m.base1, m.base2): + def __init__(self): + m.base1.__init__(self) + m.base2.__init__(self) + + d = Drvd2() + i1 = m.pass_cptr_base1(d) # load_impl Case 2b + assert i1 == 110 + 21 + i2 = m.pass_cptr_base2(d) + assert i2 == 120 + 22 diff --git a/tests/test_class_sh_mi_thunks.cpp b/tests/test_class_sh_mi_thunks.cpp new file mode 100644 index 0000000000..d8548ec5c6 --- /dev/null +++ b/tests/test_class_sh_mi_thunks.cpp @@ -0,0 +1,93 @@ +#include "pybind11_tests.h" + +#include +#include +#include + +namespace test_class_sh_mi_thunks { + +// For general background: https://shaharmike.com/cpp/vtable-part2/ +// C++ vtables - Part 2 - Multiple Inheritance +// ... the compiler creates a 'thunk' method that corrects `this` ... + +struct Base0 { + virtual ~Base0() = default; + Base0() = default; + Base0(const Base0 &) = delete; +}; + +struct Base1 { + virtual ~Base1() = default; + // Using `vector` here because it is known to make this test very sensitive to bugs. + std::vector vec = {1, 2, 3, 4, 5}; + Base1() = default; + Base1(const Base1 &) = delete; +}; + +struct Derived : Base1, Base0 { + ~Derived() override = default; + Derived() = default; + Derived(const Derived &) = delete; +}; + +} // namespace test_class_sh_mi_thunks + +TEST_SUBMODULE(class_sh_mi_thunks, m) { + using namespace test_class_sh_mi_thunks; + + m.def("ptrdiff_drvd_base0", []() { + auto drvd = std::unique_ptr(new Derived); + auto *base0 = dynamic_cast(drvd.get()); + return std::ptrdiff_t(reinterpret_cast(drvd.get()) + - reinterpret_cast(base0)); + }); + + py::classh(m, "Base0"); + py::classh(m, "Base1"); + py::classh(m, "Derived"); + + m.def( + "get_drvd_as_base0_raw_ptr", + []() { + auto *drvd = new Derived; + auto *base0 = dynamic_cast(drvd); + return base0; + }, + py::return_value_policy::take_ownership); + + m.def("get_drvd_as_base0_shared_ptr", []() { + auto drvd = std::make_shared(); + auto base0 = std::dynamic_pointer_cast(drvd); + return base0; + }); + + m.def("get_drvd_as_base0_unique_ptr", []() { + auto drvd = std::unique_ptr(new Derived); + auto base0 = std::unique_ptr(std::move(drvd)); + return base0; + }); + + m.def("vec_size_base0_raw_ptr", [](const Base0 *obj) { + const auto *obj_der = dynamic_cast(obj); + if (obj_der == nullptr) { + return std::size_t(0); + } + return obj_der->vec.size(); + }); + + m.def("vec_size_base0_shared_ptr", [](const std::shared_ptr &obj) -> std::size_t { + const auto obj_der = std::dynamic_pointer_cast(obj); + if (!obj_der) { + return std::size_t(0); + } + return obj_der->vec.size(); + }); + + m.def("vec_size_base0_unique_ptr", [](std::unique_ptr obj) -> std::size_t { + const auto *obj_der = dynamic_cast(obj.get()); + if (obj_der == nullptr) { + return std::size_t(0); + } + return obj_der->vec.size(); + }); +} diff --git a/tests/test_class_sh_mi_thunks.py b/tests/test_class_sh_mi_thunks.py new file mode 100644 index 0000000000..32bf47554b --- /dev/null +++ b/tests/test_class_sh_mi_thunks.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import pytest + +from pybind11_tests import class_sh_mi_thunks as m + + +def test_ptrdiff_drvd_base0(): + ptrdiff = m.ptrdiff_drvd_base0() + # A failure here does not (necessarily) mean that there is a bug, but that + # test_class_sh_mi_thunks is not exercising what it is supposed to. + # If this ever fails on some platforms: use pytest.skip() + # If this ever fails on all platforms: don't know, seems extremely unlikely. + assert ptrdiff != 0 + + +@pytest.mark.parametrize( + "vec_size_fn", + [ + m.vec_size_base0_raw_ptr, + m.vec_size_base0_shared_ptr, + ], +) +@pytest.mark.parametrize( + "get_fn", + [ + m.get_drvd_as_base0_raw_ptr, + m.get_drvd_as_base0_shared_ptr, + m.get_drvd_as_base0_unique_ptr, + ], +) +def test_get_vec_size_raw_shared(get_fn, vec_size_fn): + obj = get_fn() + assert vec_size_fn(obj) == 5 + + +@pytest.mark.parametrize( + "get_fn", [m.get_drvd_as_base0_raw_ptr, m.get_drvd_as_base0_unique_ptr] +) +def test_get_vec_size_unique(get_fn): + obj = get_fn() + assert m.vec_size_base0_unique_ptr(obj) == 5 + with pytest.raises(ValueError, match="Python instance was disowned"): + m.vec_size_base0_unique_ptr(obj) + + +def test_get_shared_vec_size_unique(): + obj = m.get_drvd_as_base0_shared_ptr() + with pytest.raises(ValueError) as exc_info: + m.vec_size_base0_unique_ptr(obj) + assert ( + str(exc_info.value) == "Cannot disown external shared_ptr (load_as_unique_ptr)." + ) diff --git a/tests/test_class_sh_property.cpp b/tests/test_class_sh_property.cpp new file mode 100644 index 0000000000..8863ad7d7b --- /dev/null +++ b/tests/test_class_sh_property.cpp @@ -0,0 +1,94 @@ +// The compact 4-character naming matches that in test_class_sh_basic.cpp +// Variable names are intentionally terse, to not distract from the more important C++ type names: +// valu(e), ref(erence), ptr or p (pointer), r = rvalue, m = mutable, c = const, +// sh = shared_ptr, uq = unique_ptr. + +#include "pybind11_tests.h" + +#include + +namespace test_class_sh_property { + +struct ClassicField { + int num = -88; +}; + +struct ClassicOuter { + ClassicField *m_mptr = nullptr; + const ClassicField *m_cptr = nullptr; +}; + +struct Field { + int num = -99; +}; + +struct Outer { + Field m_valu; + Field *m_mptr = nullptr; + const Field *m_cptr = nullptr; + std::unique_ptr m_uqmp; + std::unique_ptr m_uqcp; + std::shared_ptr m_shmp; + std::shared_ptr m_shcp; +}; + +inline void DisownOuter(std::unique_ptr) {} + +struct WithCharArrayMember { + WithCharArrayMember() { std::memcpy(char6_member, "Char6", 6); } + char char6_member[6]; +}; + +struct WithConstCharPtrMember { + const char *const_char_ptr_member = "ConstChar*"; +}; + +} // namespace test_class_sh_property + +TEST_SUBMODULE(class_sh_property, m) { + using namespace test_class_sh_property; + + py::class_>(m, "ClassicField") + .def(py::init<>()) + .def_readwrite("num", &ClassicField::num); + + py::class_>(m, "ClassicOuter") + .def(py::init<>()) + .def_readonly("m_mptr_readonly", &ClassicOuter::m_mptr) + .def_readwrite("m_mptr_readwrite", &ClassicOuter::m_mptr) + .def_readwrite("m_cptr_readonly", &ClassicOuter::m_cptr) + .def_readwrite("m_cptr_readwrite", &ClassicOuter::m_cptr); + + py::classh(m, "Field").def(py::init<>()).def_readwrite("num", &Field::num); + + py::classh(m, "Outer") + .def(py::init<>()) + + .def_readonly("m_valu_readonly", &Outer::m_valu) + .def_readwrite("m_valu_readwrite", &Outer::m_valu) + + .def_readonly("m_mptr_readonly", &Outer::m_mptr) + .def_readwrite("m_mptr_readwrite", &Outer::m_mptr) + .def_readonly("m_cptr_readonly", &Outer::m_cptr) + .def_readwrite("m_cptr_readwrite", &Outer::m_cptr) + + // .def_readonly("m_uqmp_readonly", &Outer::m_uqmp) // Custom compilation Error. + .def_readwrite("m_uqmp_readwrite", &Outer::m_uqmp) + // .def_readonly("m_uqcp_readonly", &Outer::m_uqcp) // Custom compilation Error. + .def_readwrite("m_uqcp_readwrite", &Outer::m_uqcp) + + .def_readwrite("m_shmp_readonly", &Outer::m_shmp) + .def_readwrite("m_shmp_readwrite", &Outer::m_shmp) + .def_readwrite("m_shcp_readonly", &Outer::m_shcp) + .def_readwrite("m_shcp_readwrite", &Outer::m_shcp); + + m.def("DisownOuter", DisownOuter); + + py::classh(m, "WithCharArrayMember") + .def(py::init<>()) + .def_readonly("char6_member", &WithCharArrayMember::char6_member); + + py::classh(m, "WithConstCharPtrMember") + .def(py::init<>()) + .def_readonly("const_char_ptr_member", &WithConstCharPtrMember::const_char_ptr_member); +} diff --git a/tests/test_class_sh_property.py b/tests/test_class_sh_property.py new file mode 100644 index 0000000000..0250a7f78e --- /dev/null +++ b/tests/test_class_sh_property.py @@ -0,0 +1,166 @@ +# The compact 4-character naming scheme (e.g. mptr, cptr, shcp) is explained at the top of +# test_class_sh_property.cpp. +from __future__ import annotations + +import pytest + +import env # noqa: F401 +from pybind11_tests import class_sh_property as m + + +@pytest.mark.skipif( + "env.PYPY or env.GRAALPY", reason="gc after `del field` is apparently deferred" +) +@pytest.mark.parametrize("m_attr", ["m_valu_readonly", "m_valu_readwrite"]) +def test_valu_getter(m_attr): + # Reduced from PyCLIF test: + # /~https://github.com/google/clif/blob/c371a6d4b28d25d53a16e6d2a6d97305fb1be25a/clif/testing/python/nested_fields_test.py#L56 + outer = m.Outer() + field = getattr(outer, m_attr) + assert field.num == -99 + with pytest.raises(ValueError) as excinfo: + m.DisownOuter(outer) + assert str(excinfo.value) == "Cannot disown use_count != 1 (load_as_unique_ptr)." + del field + m.DisownOuter(outer) + with pytest.raises(ValueError, match="Python instance was disowned") as excinfo: + getattr(outer, m_attr) + + +def test_valu_setter(): + outer = m.Outer() + assert outer.m_valu_readonly.num == -99 + assert outer.m_valu_readwrite.num == -99 + field = m.Field() + field.num = 35 + outer.m_valu_readwrite = field + assert outer.m_valu_readonly.num == 35 + assert outer.m_valu_readwrite.num == 35 + + +@pytest.mark.parametrize("m_attr", ["m_shmp", "m_shcp"]) +def test_shp(m_attr): + m_attr_readonly = m_attr + "_readonly" + m_attr_readwrite = m_attr + "_readwrite" + outer = m.Outer() + assert getattr(outer, m_attr_readonly) is None + assert getattr(outer, m_attr_readwrite) is None + field = m.Field() + field.num = 43 + setattr(outer, m_attr_readwrite, field) + assert getattr(outer, m_attr_readonly).num == 43 + assert getattr(outer, m_attr_readwrite).num == 43 + getattr(outer, m_attr_readonly).num = 57 + getattr(outer, m_attr_readwrite).num = 57 + assert field.num == 57 + del field + assert getattr(outer, m_attr_readonly).num == 57 + assert getattr(outer, m_attr_readwrite).num == 57 + + +@pytest.mark.parametrize( + ("field_type", "num_default", "outer_type"), + [ + (m.ClassicField, -88, m.ClassicOuter), + (m.Field, -99, m.Outer), + ], +) +@pytest.mark.parametrize("m_attr", ["m_mptr", "m_cptr"]) +@pytest.mark.parametrize("r_kind", ["_readonly", "_readwrite"]) +def test_ptr(field_type, num_default, outer_type, m_attr, r_kind): + m_attr_r_kind = m_attr + r_kind + outer = outer_type() + assert getattr(outer, m_attr_r_kind) is None + field = field_type() + assert field.num == num_default + setattr(outer, m_attr + "_readwrite", field) + assert getattr(outer, m_attr_r_kind).num == num_default + field.num = 76 + assert getattr(outer, m_attr_r_kind).num == 76 + # Change to -88 or -99 to demonstrate Undefined Behavior (dangling pointer). + if num_default == 88 and m_attr == "m_mptr": + del field + assert getattr(outer, m_attr_r_kind).num == 76 + + +@pytest.mark.parametrize("m_attr_readwrite", ["m_uqmp_readwrite", "m_uqcp_readwrite"]) +def test_uqp(m_attr_readwrite): + outer = m.Outer() + assert getattr(outer, m_attr_readwrite) is None + field_orig = m.Field() + field_orig.num = 39 + setattr(outer, m_attr_readwrite, field_orig) + with pytest.raises(ValueError, match="Python instance was disowned"): + _ = field_orig.num + field_retr1 = getattr(outer, m_attr_readwrite) + assert getattr(outer, m_attr_readwrite) is None + assert field_retr1.num == 39 + field_retr1.num = 93 + setattr(outer, m_attr_readwrite, field_retr1) + with pytest.raises(ValueError): + _ = field_retr1.num + field_retr2 = getattr(outer, m_attr_readwrite) + assert field_retr2.num == 93 + + +# Proof-of-concept (POC) for safe & intuitive Python access to unique_ptr members. +# The C++ member unique_ptr is disowned to a temporary Python object for accessing +# an attribute of the member. After the attribute was accessed, the Python object +# is disowned back to the C++ member unique_ptr. +# Productizing this POC is left for a future separate PR, as needed. +class unique_ptr_field_proxy_poc: + def __init__(self, obj, field_name): + object.__setattr__(self, "__obj", obj) + object.__setattr__(self, "__field_name", field_name) + + def __getattr__(self, *args, **kwargs): + return _proxy_dereference(self, getattr, *args, **kwargs) + + def __setattr__(self, *args, **kwargs): + return _proxy_dereference(self, setattr, *args, **kwargs) + + def __delattr__(self, *args, **kwargs): + return _proxy_dereference(self, delattr, *args, **kwargs) + + +def _proxy_dereference(proxy, xxxattr, *args, **kwargs): + obj = object.__getattribute__(proxy, "__obj") + field_name = object.__getattribute__(proxy, "__field_name") + field = getattr(obj, field_name) # Disowns the C++ unique_ptr member. + assert field is not None + try: + return xxxattr(field, *args, **kwargs) + finally: + setattr(obj, field_name, field) # Disowns the temporary Python object (field). + + +@pytest.mark.parametrize("m_attr", ["m_uqmp", "m_uqcp"]) +def test_unique_ptr_field_proxy_poc(m_attr): + m_attr_readwrite = m_attr + "_readwrite" + outer = m.Outer() + field_orig = m.Field() + field_orig.num = 45 + setattr(outer, m_attr_readwrite, field_orig) + field_proxy = unique_ptr_field_proxy_poc(outer, m_attr_readwrite) + assert field_proxy.num == 45 + assert field_proxy.num == 45 + with pytest.raises(AttributeError): + _ = field_proxy.xyz + assert field_proxy.num == 45 + field_proxy.num = 82 + assert field_proxy.num == 82 + field_proxy = unique_ptr_field_proxy_poc(outer, m_attr_readwrite) + assert field_proxy.num == 82 + with pytest.raises(AttributeError): + del field_proxy.num + assert field_proxy.num == 82 + + +def test_readonly_char6_member(): + obj = m.WithCharArrayMember() + assert obj.char6_member == "Char6" + + +def test_readonly_const_char_ptr_member(): + obj = m.WithConstCharPtrMember() + assert obj.const_char_ptr_member == "ConstChar*" diff --git a/tests/test_class_sh_property_non_owning.cpp b/tests/test_class_sh_property_non_owning.cpp new file mode 100644 index 0000000000..45fe7c7beb --- /dev/null +++ b/tests/test_class_sh_property_non_owning.cpp @@ -0,0 +1,63 @@ +#include "pybind11_tests.h" + +#include +#include + +namespace test_class_sh_property_non_owning { + +struct CoreField { + explicit CoreField(int int_value = -99) : int_value{int_value} {} + int int_value; +}; + +struct DataField { + DataField(int i_value, int i_shared, int i_unique) + : core_fld_value{i_value}, core_fld_shared_ptr{new CoreField{i_shared}}, + core_fld_raw_ptr{core_fld_shared_ptr.get()}, + core_fld_unique_ptr{new CoreField{i_unique}} {} + CoreField core_fld_value; + std::shared_ptr core_fld_shared_ptr; + CoreField *core_fld_raw_ptr; + std::unique_ptr core_fld_unique_ptr; +}; + +struct DataFieldsHolder { +private: + std::vector vec; + +public: + explicit DataFieldsHolder(std::size_t vec_size) { + for (std::size_t i = 0; i < vec_size; i++) { + int i11 = static_cast(i) * 11; + vec.emplace_back(13 + i11, 14 + i11, 15 + i11); + } + } + + DataField *vec_at(std::size_t index) { + if (index >= vec.size()) { + return nullptr; + } + return &vec[index]; + } +}; + +} // namespace test_class_sh_property_non_owning + +using namespace test_class_sh_property_non_owning; + +TEST_SUBMODULE(class_sh_property_non_owning, m) { + py::classh(m, "CoreField").def_readwrite("int_value", &CoreField::int_value); + + py::classh(m, "DataField") + .def_readonly("core_fld_value_ro", &DataField::core_fld_value) + .def_readwrite("core_fld_value_rw", &DataField::core_fld_value) + .def_readonly("core_fld_shared_ptr_ro", &DataField::core_fld_shared_ptr) + .def_readwrite("core_fld_shared_ptr_rw", &DataField::core_fld_shared_ptr) + .def_readonly("core_fld_raw_ptr_ro", &DataField::core_fld_raw_ptr) + .def_readwrite("core_fld_raw_ptr_rw", &DataField::core_fld_raw_ptr) + .def_readwrite("core_fld_unique_ptr_rw", &DataField::core_fld_unique_ptr); + + py::classh(m, "DataFieldsHolder") + .def(py::init()) + .def("vec_at", &DataFieldsHolder::vec_at, py::return_value_policy::reference_internal); +} diff --git a/tests/test_class_sh_property_non_owning.py b/tests/test_class_sh_property_non_owning.py new file mode 100644 index 0000000000..33a9d4503b --- /dev/null +++ b/tests/test_class_sh_property_non_owning.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import pytest + +from pybind11_tests import class_sh_property_non_owning as m + + +@pytest.mark.parametrize("persistent_holder", [True, False]) +@pytest.mark.parametrize( + ("core_fld", "expected"), + [ + ("core_fld_value_ro", (13, 24)), + ("core_fld_value_rw", (13, 24)), + ("core_fld_shared_ptr_ro", (14, 25)), + ("core_fld_shared_ptr_rw", (14, 25)), + ("core_fld_raw_ptr_ro", (14, 25)), + ("core_fld_raw_ptr_rw", (14, 25)), + ("core_fld_unique_ptr_rw", (15, 26)), + ], +) +def test_core_fld_common(core_fld, expected, persistent_holder): + if persistent_holder: + h = m.DataFieldsHolder(2) + for i, exp in enumerate(expected): + c = getattr(h.vec_at(i), core_fld) + assert c.int_value == exp + else: + for i, exp in enumerate(expected): + c = getattr(m.DataFieldsHolder(2).vec_at(i), core_fld) + assert c.int_value == exp diff --git a/tests/test_class_sh_shared_ptr_copy_move.cpp b/tests/test_class_sh_shared_ptr_copy_move.cpp new file mode 100644 index 0000000000..889425a0b0 --- /dev/null +++ b/tests/test_class_sh_shared_ptr_copy_move.cpp @@ -0,0 +1,103 @@ +#include "pybind11_tests.h" + +#include +#include +#include + +namespace pybind11_tests { +namespace { + +const std::string fooNames[] = {"ShPtr_", "SmHld_"}; + +template +struct Foo { + std::string history; + explicit Foo(const std::string &history_) : history(history_) {} + Foo(const Foo &other) : history(other.history + "_CpCtor") {} + Foo(Foo &&other) noexcept : history(other.history + "_MvCtor") {} + Foo &operator=(const Foo &other) { + history = other.history + "_OpEqLv"; + return *this; + } + Foo &operator=(Foo &&other) noexcept { + history = other.history + "_OpEqRv"; + return *this; + } + std::string get_history() const { return "Foo" + fooNames[SerNo] + history; } +}; + +using FooShPtr = Foo<0>; +using FooSmHld = Foo<1>; + +struct Outer { + std::shared_ptr ShPtr; + std::shared_ptr SmHld; + Outer() + : ShPtr(std::make_shared("Outer")), SmHld(std::make_shared("Outer")) {} + std::shared_ptr getShPtr() const { return ShPtr; } + std::shared_ptr getSmHld() const { return SmHld; } +}; + +} // namespace + +TEST_SUBMODULE(class_sh_shared_ptr_copy_move, m) { + namespace py = pybind11; + + py::class_>(m, "FooShPtr") + .def("get_history", &FooShPtr::get_history); + py::classh(m, "FooSmHld").def("get_history", &FooSmHld::get_history); + + auto outer = py::class_(m, "Outer").def(py::init()); +#define MAKE_PROP(PropTyp) \ + MAKE_PROP_FOO(ShPtr, PropTyp) \ + MAKE_PROP_FOO(SmHld, PropTyp) + +#define MAKE_PROP_FOO(FooTyp, PropTyp) \ + .def_##PropTyp(#FooTyp "_" #PropTyp "_default", &Outer::FooTyp) \ + .def_##PropTyp( \ + #FooTyp "_" #PropTyp "_copy", &Outer::FooTyp, py::return_value_policy::copy) \ + .def_##PropTyp( \ + #FooTyp "_" #PropTyp "_move", &Outer::FooTyp, py::return_value_policy::move) + outer MAKE_PROP(readonly) MAKE_PROP(readwrite); +#undef MAKE_PROP_FOO + +#define MAKE_PROP_FOO(FooTyp, PropTyp) \ + .def_##PropTyp(#FooTyp "_property_" #PropTyp "_default", &Outer::FooTyp) \ + .def_property_##PropTyp(#FooTyp "_property_" #PropTyp "_copy", \ + &Outer::get##FooTyp, \ + py::return_value_policy::copy) \ + .def_property_##PropTyp(#FooTyp "_property_" #PropTyp "_move", \ + &Outer::get##FooTyp, \ + py::return_value_policy::move) + outer MAKE_PROP(readonly); +#undef MAKE_PROP_FOO +#undef MAKE_PROP + + m.def("test_ShPtr_copy", []() { + auto o = std::make_shared("copy"); + auto l = py::list(); + l.append(o); + return l; + }); + m.def("test_SmHld_copy", []() { + auto o = std::make_shared("copy"); + auto l = py::list(); + l.append(o); + return l; + }); + + m.def("test_ShPtr_move", []() { + auto o = std::make_shared("move"); + auto l = py::list(); + l.append(std::move(o)); + return l; + }); + m.def("test_SmHld_move", []() { + auto o = std::make_shared("move"); + auto l = py::list(); + l.append(std::move(o)); + return l; + }); +} + +} // namespace pybind11_tests diff --git a/tests/test_class_sh_shared_ptr_copy_move.py b/tests/test_class_sh_shared_ptr_copy_move.py new file mode 100644 index 0000000000..067bb47d2a --- /dev/null +++ b/tests/test_class_sh_shared_ptr_copy_move.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from pybind11_tests import class_sh_shared_ptr_copy_move as m + + +def test_shptr_copy(): + txt = m.test_ShPtr_copy()[0].get_history() + assert txt == "FooShPtr_copy" + + +def test_smhld_copy(): + txt = m.test_SmHld_copy()[0].get_history() + assert txt == "FooSmHld_copy" + + +def test_shptr_move(): + txt = m.test_ShPtr_move()[0].get_history() + assert txt == "FooShPtr_move" + + +def test_smhld_move(): + txt = m.test_SmHld_move()[0].get_history() + assert txt == "FooSmHld_move" + + +def _check_property(foo_typ, prop_typ, policy): + o = m.Outer() + name = f"{foo_typ}_{prop_typ}_{policy}" + history = f"Foo{foo_typ}_Outer" + f = getattr(o, name) + assert f.get_history() == history + # and try again to check that o did not get changed + f = getattr(o, name) + assert f.get_history() == history + + +def test_properties(): + for prop_typ in ("readonly", "readwrite", "property_readonly"): + for foo_typ in ("ShPtr", "SmHld"): + for policy in ("default", "copy", "move"): + _check_property(foo_typ, prop_typ, policy) diff --git a/tests/test_class_sh_trampoline_basic.cpp b/tests/test_class_sh_trampoline_basic.cpp new file mode 100644 index 0000000000..0f42dcc717 --- /dev/null +++ b/tests/test_class_sh_trampoline_basic.cpp @@ -0,0 +1,82 @@ +#include "pybind11_tests.h" + +#include + +namespace pybind11_tests { +namespace class_sh_trampoline_basic { + +template // Using int as a trick to easily generate a series of types. +struct Abase { + int val = 0; + virtual ~Abase() = default; + explicit Abase(int val_) : val{val_} {} + int Get() const { return val * 10 + 3; } + virtual int Add(int other_val) const = 0; + + // Some compilers complain about implicitly defined versions of some of the following: + Abase(const Abase &) = default; + Abase(Abase &&) noexcept = default; + Abase &operator=(const Abase &) = default; + Abase &operator=(Abase &&) noexcept = default; +}; + +template +struct AbaseAlias : Abase { + using Abase::Abase; + + int Add(int other_val) const override { + PYBIND11_OVERRIDE_PURE(int, /* Return type */ + Abase, /* Parent class */ + Add, /* Name of function in C++ (must match Python name) */ + other_val); + } +}; + +template <> +struct AbaseAlias<1> : Abase<1>, py::trampoline_self_life_support { + using Abase<1>::Abase; + + int Add(int other_val) const override { + PYBIND11_OVERRIDE_PURE(int, /* Return type */ + Abase<1>, /* Parent class */ + Add, /* Name of function in C++ (must match Python name) */ + other_val); + } +}; + +template +int AddInCppRawPtr(const Abase *obj, int other_val) { + return obj->Add(other_val) * 10 + 7; +} + +template +int AddInCppSharedPtr(std::shared_ptr> obj, int other_val) { + return obj->Add(other_val) * 100 + 11; +} + +template +int AddInCppUniquePtr(std::unique_ptr> obj, int other_val) { + return obj->Add(other_val) * 100 + 13; +} + +template +void wrap(py::module_ m, const char *py_class_name) { + py::classh, AbaseAlias>(m, py_class_name) + .def(py::init(), py::arg("val")) + .def("Get", &Abase::Get) + .def("Add", &Abase::Add, py::arg("other_val")); + + m.def("AddInCppRawPtr", AddInCppRawPtr, py::arg("obj"), py::arg("other_val")); + m.def("AddInCppSharedPtr", AddInCppSharedPtr, py::arg("obj"), py::arg("other_val")); + m.def("AddInCppUniquePtr", AddInCppUniquePtr, py::arg("obj"), py::arg("other_val")); +} + +} // namespace class_sh_trampoline_basic +} // namespace pybind11_tests + +using namespace pybind11_tests::class_sh_trampoline_basic; + +TEST_SUBMODULE(class_sh_trampoline_basic, m) { + wrap<0>(m, "Abase0"); + wrap<1>(m, "Abase1"); +} diff --git a/tests/test_class_sh_trampoline_basic.py b/tests/test_class_sh_trampoline_basic.py new file mode 100644 index 0000000000..eab82121fb --- /dev/null +++ b/tests/test_class_sh_trampoline_basic.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import pytest + +from pybind11_tests import class_sh_trampoline_basic as m + + +class PyDrvd0(m.Abase0): + def __init__(self, val): + super().__init__(val) + + def Add(self, other_val): + return self.Get() * 100 + other_val + + +class PyDrvd1(m.Abase1): + def __init__(self, val): + super().__init__(val) + + def Add(self, other_val): + return self.Get() * 200 + other_val + + +def test_drvd0_add(): + drvd = PyDrvd0(74) + assert drvd.Add(38) == (74 * 10 + 3) * 100 + 38 + + +def test_drvd0_add_in_cpp_raw_ptr(): + drvd = PyDrvd0(52) + assert m.AddInCppRawPtr(drvd, 27) == ((52 * 10 + 3) * 100 + 27) * 10 + 7 + + +def test_drvd0_add_in_cpp_shared_ptr(): + while True: + drvd = PyDrvd0(36) + assert m.AddInCppSharedPtr(drvd, 56) == ((36 * 10 + 3) * 100 + 56) * 100 + 11 + return # Comment out for manual leak checking (use `top` command). + + +def test_drvd0_add_in_cpp_unique_ptr(): + while True: + drvd = PyDrvd0(0) + with pytest.raises(ValueError) as exc_info: + m.AddInCppUniquePtr(drvd, 0) + assert ( + str(exc_info.value) + == "Alias class (also known as trampoline) does not inherit from" + " py::trampoline_self_life_support, therefore the ownership of this" + " instance cannot safely be transferred to C++." + ) + return # Comment out for manual leak checking (use `top` command). + + +def test_drvd1_add_in_cpp_unique_ptr(): + while True: + drvd = PyDrvd1(25) + assert m.AddInCppUniquePtr(drvd, 83) == ((25 * 10 + 3) * 200 + 83) * 100 + 13 + return # Comment out for manual leak checking (use `top` command). diff --git a/tests/test_class_sh_trampoline_self_life_support.cpp b/tests/test_class_sh_trampoline_self_life_support.cpp new file mode 100644 index 0000000000..22b728e28c --- /dev/null +++ b/tests/test_class_sh_trampoline_self_life_support.cpp @@ -0,0 +1,86 @@ +// Copyright (c) 2021 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#include "pybind11/trampoline_self_life_support.h" +#include "pybind11_tests.h" + +#include +#include +#include + +namespace pybind11_tests { +namespace class_sh_trampoline_self_life_support { + +struct Big5 { // Also known as "rule of five". + std::string history; + + explicit Big5(std::string history_start) : history{std::move(history_start)} {} + + Big5(const Big5 &other) { history = other.history + "_CpCtor"; } + + Big5(Big5 &&other) noexcept { history = other.history + "_MvCtor"; } + + Big5 &operator=(const Big5 &other) { + history = other.history + "_OpEqLv"; + return *this; + } + + Big5 &operator=(Big5 &&other) noexcept { + history = other.history + "_OpEqRv"; + return *this; + } + + virtual ~Big5() = default; + +protected: + Big5() : history{"DefaultConstructor"} {} +}; + +struct Big5Trampoline : Big5, py::trampoline_self_life_support { + using Big5::Big5; +}; + +} // namespace class_sh_trampoline_self_life_support +} // namespace pybind11_tests + +using namespace pybind11_tests::class_sh_trampoline_self_life_support; + +TEST_SUBMODULE(class_sh_trampoline_self_life_support, m) { + py::classh(m, "Big5") + .def(py::init()) + .def_readonly("history", &Big5::history); + + m.def("action", [](std::unique_ptr obj, int action_id) { + py::object o2 = py::none(); + // This is very unusual, but needed to directly exercise the trampoline_self_life_support + // CpCtor, MvCtor, operator= lvalue, operator= rvalue. + auto *obj_trampoline = dynamic_cast(obj.get()); + if (obj_trampoline != nullptr) { + switch (action_id) { + case 0: { // CpCtor + std::unique_ptr cp(new Big5Trampoline(*obj_trampoline)); + o2 = py::cast(std::move(cp)); + } break; + case 1: { // MvCtor + std::unique_ptr mv(new Big5Trampoline(std::move(*obj_trampoline))); + o2 = py::cast(std::move(mv)); + } break; + case 2: { // operator= lvalue + std::unique_ptr lv(new Big5Trampoline); + *lv = *obj_trampoline; // NOLINT clang-tidy cppcoreguidelines-slicing + o2 = py::cast(std::move(lv)); + } break; + case 3: { // operator= rvalue + std::unique_ptr rv(new Big5Trampoline); + *rv = std::move(*obj_trampoline); + o2 = py::cast(std::move(rv)); + } break; + default: + break; + } + } + py::object o1 = py::cast(std::move(obj)); + return py::make_tuple(o1, o2); + }); +} diff --git a/tests/test_class_sh_trampoline_self_life_support.py b/tests/test_class_sh_trampoline_self_life_support.py new file mode 100644 index 0000000000..d4af2ab99a --- /dev/null +++ b/tests/test_class_sh_trampoline_self_life_support.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import pytest + +import pybind11_tests.class_sh_trampoline_self_life_support as m + + +class PyBig5(m.Big5): + pass + + +def test_m_big5(): + obj = m.Big5("Seed") + assert obj.history == "Seed" + o1, o2 = m.action(obj, 0) + assert o1 is not obj + assert o1.history == "Seed" + with pytest.raises(ValueError) as excinfo: + _ = obj.history + assert "Python instance was disowned" in str(excinfo.value) + assert o2 is None + + +@pytest.mark.parametrize( + ("action_id", "expected_history"), + [ + (0, "Seed_CpCtor"), + (1, "Seed_MvCtor"), + (2, "Seed_OpEqLv"), + (3, "Seed_OpEqRv"), + ], +) +def test_py_big5(action_id, expected_history): + obj = PyBig5("Seed") + assert obj.history == "Seed" + o1, o2 = m.action(obj, action_id) + assert o1 is obj + assert o2.history == expected_history diff --git a/tests/test_class_sh_trampoline_shared_from_this.cpp b/tests/test_class_sh_trampoline_shared_from_this.cpp new file mode 100644 index 0000000000..dc6bf1c72a --- /dev/null +++ b/tests/test_class_sh_trampoline_shared_from_this.cpp @@ -0,0 +1,137 @@ +// Copyright (c) 2021 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#include "pybind11_tests.h" + +#include +#include + +namespace pybind11_tests { +namespace class_sh_trampoline_shared_from_this { + +struct Sft : std::enable_shared_from_this { + std::string history; + explicit Sft(const std::string &history_seed) : history{history_seed} {} + virtual ~Sft() = default; + +#if defined(__clang__) + // "Group of 4" begin. + // This group is not meant to be used, but will leave a trace in the + // history in case something goes wrong. + // However, compilers other than clang have a variety of issues. It is not + // worth the trouble covering all platforms. + Sft(const Sft &other) : enable_shared_from_this(other) { history = other.history + "_CpCtor"; } + + Sft(Sft &&other) noexcept { history = other.history + "_MvCtor"; } + + Sft &operator=(const Sft &other) { + history = other.history + "_OpEqLv"; + return *this; + } + + Sft &operator=(Sft &&other) noexcept { + history = other.history + "_OpEqRv"; + return *this; + } + // "Group of 4" end. +#endif +}; + +struct SftSharedPtrStash { + int ser_no; + std::vector> stash; + explicit SftSharedPtrStash(int ser_no) : ser_no{ser_no} {} + void Clear() { stash.clear(); } + void Add(const std::shared_ptr &obj) { + if (!obj->history.empty()) { + obj->history += "_Stash" + std::to_string(ser_no) + "Add"; + } + stash.push_back(obj); + } + void AddSharedFromThis(Sft *obj) { + auto sft = obj->shared_from_this(); + if (!sft->history.empty()) { + sft->history += "_Stash" + std::to_string(ser_no) + "AddSharedFromThis"; + } + stash.push_back(sft); + } + std::string history(unsigned i) { + if (i < stash.size()) { + return stash[i]->history; + } + return "OutOfRange"; + } + long use_count(unsigned i) { + if (i < stash.size()) { + return stash[i].use_count(); + } + return -1; + } +}; + +struct SftTrampoline : Sft, py::trampoline_self_life_support { + using Sft::Sft; +}; + +long use_count(const std::shared_ptr &obj) { return obj.use_count(); } + +long pass_shared_ptr(const std::shared_ptr &obj) { + auto sft = obj->shared_from_this(); + if (!sft->history.empty()) { + sft->history += "_PassSharedPtr"; + } + return sft.use_count(); +} + +std::string pass_unique_ptr_cref(const std::unique_ptr &obj) { + return obj ? obj->history : ""; +} +void pass_unique_ptr_rref(std::unique_ptr &&) { + throw std::runtime_error("Expected to not be reached."); +} + +Sft *make_pure_cpp_sft_raw_ptr(const std::string &history_seed) { return new Sft{history_seed}; } + +std::unique_ptr make_pure_cpp_sft_unq_ptr(const std::string &history_seed) { + return std::unique_ptr(new Sft{history_seed}); +} + +std::shared_ptr make_pure_cpp_sft_shd_ptr(const std::string &history_seed) { + return std::make_shared(history_seed); +} + +std::shared_ptr pass_through_shd_ptr(const std::shared_ptr &obj) { return obj; } + +} // namespace class_sh_trampoline_shared_from_this +} // namespace pybind11_tests + +using namespace pybind11_tests::class_sh_trampoline_shared_from_this; + +TEST_SUBMODULE(class_sh_trampoline_shared_from_this, m) { + py::classh(m, "Sft") + .def(py::init()) + .def(py::init([](const std::string &history, int) { + return std::make_shared(history); + })) + .def_readonly("history", &Sft::history) + // This leads to multiple entries in registered_instances: + .def(py::init([](const std::shared_ptr &existing) { return existing; })); + + py::classh(m, "SftSharedPtrStash") + .def(py::init()) + .def("Clear", &SftSharedPtrStash::Clear) + .def("Add", &SftSharedPtrStash::Add) + .def("AddSharedFromThis", &SftSharedPtrStash::AddSharedFromThis) + .def("history", &SftSharedPtrStash::history) + .def("use_count", &SftSharedPtrStash::use_count); + + m.def("use_count", use_count); + m.def("pass_shared_ptr", pass_shared_ptr); + m.def("pass_unique_ptr_cref", pass_unique_ptr_cref); + m.def("pass_unique_ptr_rref", pass_unique_ptr_rref); + m.def("make_pure_cpp_sft_raw_ptr", make_pure_cpp_sft_raw_ptr); + m.def("make_pure_cpp_sft_unq_ptr", make_pure_cpp_sft_unq_ptr); + m.def("make_pure_cpp_sft_shd_ptr", make_pure_cpp_sft_shd_ptr); + m.def("pass_through_shd_ptr", pass_through_shd_ptr); +} diff --git a/tests/test_class_sh_trampoline_shared_from_this.py b/tests/test_class_sh_trampoline_shared_from_this.py new file mode 100644 index 0000000000..fbe31387a8 --- /dev/null +++ b/tests/test_class_sh_trampoline_shared_from_this.py @@ -0,0 +1,247 @@ +from __future__ import annotations + +import sys +import weakref + +import pytest + +import env +import pybind11_tests.class_sh_trampoline_shared_from_this as m + + +class PySft(m.Sft): + pass + + +def test_release_and_shared_from_this(): + # Exercises the most direct path from building a shared_from_this-visible + # shared_ptr to calling shared_from_this. + obj = PySft("PySft") + assert obj.history == "PySft" + assert m.use_count(obj) == 1 + assert m.pass_shared_ptr(obj) == 2 + assert obj.history == "PySft_PassSharedPtr" + assert m.use_count(obj) == 1 + assert m.pass_shared_ptr(obj) == 2 + assert obj.history == "PySft_PassSharedPtr_PassSharedPtr" + assert m.use_count(obj) == 1 + + +def test_release_and_shared_from_this_leak(): + obj = PySft("") + while True: + m.pass_shared_ptr(obj) + assert not obj.history + assert m.use_count(obj) == 1 + break # Comment out for manual leak checking (use `top` command). + + +def test_release_and_stash(): + # Exercises correct functioning of guarded_delete weak_ptr. + obj = PySft("PySft") + stash1 = m.SftSharedPtrStash(1) + stash1.Add(obj) + exp_hist = "PySft_Stash1Add" + assert obj.history == exp_hist + assert m.use_count(obj) == 2 + assert stash1.history(0) == exp_hist + assert stash1.use_count(0) == 1 + assert m.pass_shared_ptr(obj) == 3 + exp_hist += "_PassSharedPtr" + assert obj.history == exp_hist + assert m.use_count(obj) == 2 + assert stash1.history(0) == exp_hist + assert stash1.use_count(0) == 1 + stash2 = m.SftSharedPtrStash(2) + stash2.Add(obj) + exp_hist += "_Stash2Add" + assert obj.history == exp_hist + assert m.use_count(obj) == 3 + assert stash2.history(0) == exp_hist + assert stash2.use_count(0) == 2 + stash2.Add(obj) + exp_hist += "_Stash2Add" + assert obj.history == exp_hist + assert m.use_count(obj) == 4 + assert stash1.history(0) == exp_hist + assert stash1.use_count(0) == 3 + assert stash2.history(0) == exp_hist + assert stash2.use_count(0) == 3 + assert stash2.history(1) == exp_hist + assert stash2.use_count(1) == 3 + del obj + assert stash2.history(0) == exp_hist + assert stash2.use_count(0) == 3 + assert stash2.history(1) == exp_hist + assert stash2.use_count(1) == 3 + stash2.Clear() + assert stash1.history(0) == exp_hist + assert stash1.use_count(0) == 1 + + +def test_release_and_stash_leak(): + obj = PySft("") + while True: + stash1 = m.SftSharedPtrStash(1) + stash1.Add(obj) + assert not obj.history + assert m.use_count(obj) == 2 + assert stash1.use_count(0) == 1 + stash1.Add(obj) + assert not obj.history + assert m.use_count(obj) == 3 + assert stash1.use_count(0) == 2 + assert stash1.use_count(1) == 2 + break # Comment out for manual leak checking (use `top` command). + + +def test_release_and_stash_via_shared_from_this(): + # Exercises that the smart_holder vptr is invisible to the shared_from_this mechanism. + obj = PySft("PySft") + 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 == "PySft_Stash1Add" + assert stash1.use_count(0) == 1 + stash1.AddSharedFromThis(obj) + assert obj.history == "PySft_Stash1Add_Stash1AddSharedFromThis" + assert stash1.use_count(0) == 2 + assert stash1.use_count(1) == 2 + + +def test_release_and_stash_via_shared_from_this_leak(): + obj = PySft("") + while True: + 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 not obj.history + assert stash1.use_count(0) == 1 + stash1.AddSharedFromThis(obj) + assert not obj.history + assert stash1.use_count(0) == 2 + assert stash1.use_count(1) == 2 + break # Comment out for manual leak checking (use `top` command). + + +def test_pass_released_shared_ptr_as_unique_ptr(): + # Exercises that returning a unique_ptr fails while a shared_from_this + # visible shared_ptr exists. + obj = PySft("PySft") + stash1 = m.SftSharedPtrStash(1) + stash1.Add(obj) # Releases shared_ptr to C++. + assert m.pass_unique_ptr_cref(obj) == "PySft_Stash1Add" + assert obj.history == "PySft_Stash1Add" + with pytest.raises(ValueError) as exc_info: + m.pass_unique_ptr_rref(obj) + assert str(exc_info.value) == ( + "Python instance is currently owned by a std::shared_ptr." + ) + assert obj.history == "PySft_Stash1Add" + + +@pytest.mark.parametrize( + "make_f", + [ + m.make_pure_cpp_sft_raw_ptr, + m.make_pure_cpp_sft_unq_ptr, + m.make_pure_cpp_sft_shd_ptr, + ], +) +def test_pure_cpp_sft_raw_ptr(make_f): + # Exercises void_cast_raw_ptr logic for different situations. + obj = make_f("PureCppSft") + assert m.pass_shared_ptr(obj) == 3 + assert obj.history == "PureCppSft_PassSharedPtr" + obj = make_f("PureCppSft") + stash1 = m.SftSharedPtrStash(1) + stash1.AddSharedFromThis(obj) + assert obj.history == "PureCppSft_Stash1AddSharedFromThis" + + +def test_multiple_registered_instances_for_same_pointee(): + obj0 = PySft("PySft") + obj0.attachment_in_dict = "Obj0" + assert m.pass_through_shd_ptr(obj0) is obj0 + while True: + obj = m.Sft(obj0) + assert obj is not obj0 + obj_pt = m.pass_through_shd_ptr(obj) + # Unpredictable! Because registered_instances is as std::unordered_multimap. + assert obj_pt is obj0 or obj_pt is obj + # Multiple registered_instances for the same pointee can lead to unpredictable results: + if obj_pt is obj0: + assert obj_pt.attachment_in_dict == "Obj0" + else: + assert not hasattr(obj_pt, "attachment_in_dict") + assert obj0.history == "PySft" + break # Comment out for manual leak checking (use `top` command). + + +def test_multiple_registered_instances_for_same_pointee_leak(): + obj0 = PySft("") + while True: + stash1 = m.SftSharedPtrStash(1) + stash1.Add(m.Sft(obj0)) + assert stash1.use_count(0) == 1 + stash1.Add(m.Sft(obj0)) + assert stash1.use_count(0) == 1 + assert stash1.use_count(1) == 1 + assert not obj0.history + break # Comment out for manual leak checking (use `top` command). + + +def test_multiple_registered_instances_for_same_pointee_recursive(): + while True: + obj0 = PySft("PySft") + if not env.PYPY: + obj0_wr = weakref.ref(obj0) + obj = obj0 + # This loop creates a chain of instances linked by shared_ptrs. + for _ in range(10): + obj_next = m.Sft(obj) + assert obj_next is not obj + obj = obj_next + del obj_next + assert obj.history == "PySft" + del obj0 + if not env.PYPY and not env.GRAALPY: + assert obj0_wr() is not None + del obj # This releases the chain recursively. + if not env.PYPY and not env.GRAALPY: + assert obj0_wr() is None + break # Comment out for manual leak checking (use `top` command). + + +# As of 2021-07-10 the pybind11 GitHub Actions valgrind build uses Python 3.9. +WORKAROUND_ENABLING_ROLLBACK_OF_PR3068 = env.LINUX and sys.version_info == (3, 9) + + +def test_std_make_shared_factory(): + class PySftMakeShared(m.Sft): + def __init__(self, history): + super().__init__(history, 0) + + obj = PySftMakeShared("PySftMakeShared") + assert obj.history == "PySftMakeShared" + if WORKAROUND_ENABLING_ROLLBACK_OF_PR3068: + try: + m.pass_through_shd_ptr(obj) + except RuntimeError as e: + str_exc_info_value = str(e) + else: + str_exc_info_value = "RuntimeError NOT RAISED" + else: + with pytest.raises(RuntimeError) as exc_info: + m.pass_through_shd_ptr(obj) + str_exc_info_value = str(exc_info.value) + assert ( + str_exc_info_value + == "smart_holder_type_casters load_as_shared_ptr failure: not implemented:" + " trampoline-self-life-support for external shared_ptr to type inheriting" + " from std::enable_shared_from_this." + ) diff --git a/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.cpp b/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.cpp new file mode 100644 index 0000000000..49e1ac885d --- /dev/null +++ b/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.cpp @@ -0,0 +1,92 @@ +// Copyright (c) 2021 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#include "pybind11_tests.h" + +#include + +namespace pybind11_tests { +namespace class_sh_trampoline_shared_ptr_cpp_arg { + +// For testing whether a python subclass of a C++ object dies when the +// last python reference is lost +struct SpBase { + // returns true if the base virtual function is called + virtual bool is_base_used() { return true; } + + // returns true if there's an associated python instance + bool has_python_instance() { + auto *tinfo = py::detail::get_type_info(typeid(SpBase)); + return (bool) py::detail::get_object_handle(this, tinfo); + } + + SpBase() = default; + SpBase(const SpBase &) = delete; + virtual ~SpBase() = default; +}; + +std::shared_ptr pass_through_shd_ptr(const std::shared_ptr &obj) { return obj; } + +struct PySpBase : SpBase { + using SpBase::SpBase; + bool is_base_used() override { PYBIND11_OVERRIDE(bool, SpBase, is_base_used); } +}; + +struct SpBaseTester { + std::shared_ptr get_object() const { return m_obj; } + void set_object(std::shared_ptr obj) { m_obj = std::move(obj); } + bool is_base_used() { return m_obj->is_base_used(); } + bool has_instance() { return (bool) m_obj; } + bool has_python_instance() { return m_obj && m_obj->has_python_instance(); } + void set_nonpython_instance() { m_obj = std::make_shared(); } + std::shared_ptr m_obj; +}; + +// For testing that a C++ class without an alias does not retain the python +// portion of the object +struct SpGoAway {}; + +struct SpGoAwayTester { + std::shared_ptr m_obj; +}; + +} // namespace class_sh_trampoline_shared_ptr_cpp_arg +} // namespace pybind11_tests + +using namespace pybind11_tests::class_sh_trampoline_shared_ptr_cpp_arg; + +TEST_SUBMODULE(class_sh_trampoline_shared_ptr_cpp_arg, m) { + // For testing whether a python subclass of a C++ object dies when the + // last python reference is lost + + py::classh(m, "SpBase") + .def(py::init<>()) + .def(py::init([](int) { return std::make_shared(); })) + .def("is_base_used", &SpBase::is_base_used) + .def("has_python_instance", &SpBase::has_python_instance); + + m.def("pass_through_shd_ptr", pass_through_shd_ptr); + m.def("pass_through_shd_ptr_release_gil", + pass_through_shd_ptr, + py::call_guard()); // PR #4196 + + py::classh(m, "SpBaseTester") + .def(py::init<>()) + .def("get_object", &SpBaseTester::get_object) + .def("set_object", &SpBaseTester::set_object) + .def("is_base_used", &SpBaseTester::is_base_used) + .def("has_instance", &SpBaseTester::has_instance) + .def("has_python_instance", &SpBaseTester::has_python_instance) + .def("set_nonpython_instance", &SpBaseTester::set_nonpython_instance) + .def_readwrite("obj", &SpBaseTester::m_obj); + + // For testing that a C++ class without an alias does not retain the python + // portion of the object + + py::classh(m, "SpGoAway").def(py::init<>()); + + py::classh(m, "SpGoAwayTester") + .def(py::init<>()) + .def_readwrite("obj", &SpGoAwayTester::m_obj); +} diff --git a/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.py b/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.py new file mode 100644 index 0000000000..a693621e3b --- /dev/null +++ b/tests/test_class_sh_trampoline_shared_ptr_cpp_arg.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +import pytest + +import env # noqa: F401 +import pybind11_tests.class_sh_trampoline_shared_ptr_cpp_arg as m + + +@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") +def test_shared_ptr_cpp_arg(): + import weakref + + class PyChild(m.SpBase): + def is_base_used(self): + return False + + tester = m.SpBaseTester() + + obj = PyChild() + objref = weakref.ref(obj) + + # Pass the last python reference to the C++ function + tester.set_object(obj) + del obj + pytest.gc_collect() + + # python reference is still around since C++ has it now + assert objref() is not None + assert tester.is_base_used() is False + assert tester.obj.is_base_used() is False + assert tester.get_object() is objref() + + +@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") +def test_shared_ptr_cpp_prop(): + class PyChild(m.SpBase): + def is_base_used(self): + return False + + tester = m.SpBaseTester() + + # Set the last python reference as a property of the C++ object + tester.obj = PyChild() + pytest.gc_collect() + + # python reference is still around since C++ has it now + assert tester.is_base_used() is False + assert tester.has_python_instance() is True + assert tester.obj.is_base_used() is False + assert tester.obj.has_python_instance() is True + + +@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") +def test_shared_ptr_arg_identity(): + import weakref + + tester = m.SpBaseTester() + + obj = m.SpBase() + objref = weakref.ref(obj) + + tester.set_object(obj) + del obj + pytest.gc_collect() + + # NOTE: the behavior below is DIFFERENT from PR #2839 + # python reference is gone because it is not an Alias instance + assert objref() is None + assert tester.has_python_instance() is False + + +@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") +def test_shared_ptr_alias_nonpython(): + tester = m.SpBaseTester() + + # C++ creates the object, a python instance shouldn't exist + tester.set_nonpython_instance() + assert tester.is_base_used() is True + assert tester.has_instance() is True + assert tester.has_python_instance() is False + + # Now a python instance exists + cobj = tester.get_object() + assert cobj.has_python_instance() + assert tester.has_instance() is True + assert tester.has_python_instance() is True + + # Now it's gone + del cobj + pytest.gc_collect() + assert tester.has_instance() is True + assert tester.has_python_instance() is False + + # When we pass it as an arg to a new tester the python instance should + # disappear because it wasn't created with an alias + new_tester = m.SpBaseTester() + + cobj = tester.get_object() + assert cobj.has_python_instance() + + new_tester.set_object(cobj) + assert tester.has_python_instance() is True + assert new_tester.has_python_instance() is True + + del cobj + pytest.gc_collect() + + # Gone! + assert tester.has_instance() is True + assert tester.has_python_instance() is False + assert new_tester.has_instance() is True + assert new_tester.has_python_instance() is False + + +@pytest.mark.skipif("env.GRAALPY", reason="Cannot reliably trigger GC") +def test_shared_ptr_goaway(): + import weakref + + tester = m.SpGoAwayTester() + + obj = m.SpGoAway() + objref = weakref.ref(obj) + + assert tester.obj is None + + tester.obj = obj + del obj + pytest.gc_collect() + + # python reference is no longer around + assert objref() is None + # C++ reference is still around + assert tester.obj is not None + + +def test_infinite(): + tester = m.SpBaseTester() + while True: + tester.set_object(m.SpBase()) + break # Comment out for manual leak checking (use `top` command). + + +@pytest.mark.parametrize( + "pass_through_func", [m.pass_through_shd_ptr, m.pass_through_shd_ptr_release_gil] +) +def test_std_make_shared_factory(pass_through_func): + class PyChild(m.SpBase): + def __init__(self): + super().__init__(0) + + obj = PyChild() + while True: + assert pass_through_func(obj) is obj + break # Comment out for manual leak checking (use `top` command). diff --git a/tests/test_class_sh_trampoline_unique_ptr.cpp b/tests/test_class_sh_trampoline_unique_ptr.cpp new file mode 100644 index 0000000000..debe3324eb --- /dev/null +++ b/tests/test_class_sh_trampoline_unique_ptr.cpp @@ -0,0 +1,63 @@ +// Copyright (c) 2021 The Pybind Development Team. +// All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +#include "pybind11/trampoline_self_life_support.h" +#include "pybind11_tests.h" + +#include + +namespace pybind11_tests { +namespace class_sh_trampoline_unique_ptr { + +class Class { +public: + virtual ~Class() = default; + + void setVal(std::uint64_t val) { val_ = val; } + std::uint64_t getVal() const { return val_; } + + virtual std::unique_ptr clone() const = 0; + virtual int foo() const = 0; + +protected: + Class() = default; + + // Some compilers complain about implicitly defined versions of some of the following: + Class(const Class &) = default; + +private: + std::uint64_t val_ = 0; +}; + +} // namespace class_sh_trampoline_unique_ptr +} // namespace pybind11_tests + +namespace pybind11_tests { +namespace class_sh_trampoline_unique_ptr { + +class PyClass : public Class, public py::trampoline_self_life_support { +public: + std::unique_ptr clone() const override { + PYBIND11_OVERRIDE_PURE(std::unique_ptr, Class, clone); + } + + int foo() const override { PYBIND11_OVERRIDE_PURE(int, Class, foo); } +}; + +} // namespace class_sh_trampoline_unique_ptr +} // namespace pybind11_tests + +TEST_SUBMODULE(class_sh_trampoline_unique_ptr, m) { + using namespace pybind11_tests::class_sh_trampoline_unique_ptr; + + py::classh(m, "Class") + .def(py::init<>()) + .def("set_val", &Class::setVal) + .def("get_val", &Class::getVal) + .def("clone", &Class::clone) + .def("foo", &Class::foo); + + m.def("clone", [](const Class &obj) { return obj.clone(); }); + m.def("clone_and_foo", [](const Class &obj) { return obj.clone()->foo(); }); +} diff --git a/tests/test_class_sh_trampoline_unique_ptr.py b/tests/test_class_sh_trampoline_unique_ptr.py new file mode 100644 index 0000000000..7799df6d61 --- /dev/null +++ b/tests/test_class_sh_trampoline_unique_ptr.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import pybind11_tests.class_sh_trampoline_unique_ptr as m + + +class MyClass(m.Class): + def foo(self): + return 10 + self.get_val() + + def clone(self): + cloned = MyClass() + cloned.set_val(self.get_val() + 3) + return cloned + + +def test_m_clone(): + obj = MyClass() + while True: + obj.set_val(5) + obj = m.clone(obj) + assert obj.get_val() == 5 + 3 + assert obj.foo() == 10 + 5 + 3 + return # Comment out for manual leak checking (use `top` command). + + +def test_m_clone_and_foo(): + obj = MyClass() + obj.set_val(7) + while True: + assert m.clone_and_foo(obj) == 10 + 7 + 3 + return # Comment out for manual leak checking (use `top` command). diff --git a/tests/test_class_sh_unique_ptr_custom_deleter.cpp b/tests/test_class_sh_unique_ptr_custom_deleter.cpp new file mode 100644 index 0000000000..adaa2e47d0 --- /dev/null +++ b/tests/test_class_sh_unique_ptr_custom_deleter.cpp @@ -0,0 +1,30 @@ +#include "pybind11_tests.h" + +#include + +namespace pybind11_tests { +namespace class_sh_unique_ptr_custom_deleter { + +// Reduced from a PyCLIF use case in the wild by @wangxf123456. +class Pet { +public: + using Ptr = std::unique_ptr>; + + std::string name; + + static Ptr New(const std::string &name) { + return Ptr(new Pet(name), std::default_delete()); + } + +private: + explicit Pet(const std::string &name) : name(name) {} +}; + +TEST_SUBMODULE(class_sh_unique_ptr_custom_deleter, m) { + py::classh(m, "Pet").def_readwrite("name", &Pet::name); + + m.def("create", &Pet::New); +} + +} // namespace class_sh_unique_ptr_custom_deleter +} // namespace pybind11_tests diff --git a/tests/test_class_sh_unique_ptr_custom_deleter.py b/tests/test_class_sh_unique_ptr_custom_deleter.py new file mode 100644 index 0000000000..34aa520682 --- /dev/null +++ b/tests/test_class_sh_unique_ptr_custom_deleter.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from pybind11_tests import class_sh_unique_ptr_custom_deleter as m + + +def test_create(): + pet = m.create("abc") + assert pet.name == "abc" diff --git a/tests/test_class_sh_unique_ptr_member.cpp b/tests/test_class_sh_unique_ptr_member.cpp new file mode 100644 index 0000000000..50c505a655 --- /dev/null +++ b/tests/test_class_sh_unique_ptr_member.cpp @@ -0,0 +1,50 @@ +#include "pybind11_tests.h" + +#include + +namespace pybind11_tests { +namespace class_sh_unique_ptr_member { + +class pointee { // NOT copyable. +public: + pointee() = default; + + int get_int() const { return 213; } + + pointee(const pointee &) = delete; + pointee(pointee &&) = delete; + pointee &operator=(const pointee &) = delete; + pointee &operator=(pointee &&) = delete; +}; + +inline std::unique_ptr make_unique_pointee() { + return std::unique_ptr(new pointee); +} + +class ptr_owner { +public: + explicit ptr_owner(std::unique_ptr ptr) : ptr_(std::move(ptr)) {} + + bool is_owner() const { return bool(ptr_); } + + std::unique_ptr give_up_ownership_via_unique_ptr() { return std::move(ptr_); } + std::shared_ptr give_up_ownership_via_shared_ptr() { return std::move(ptr_); } + +private: + std::unique_ptr ptr_; +}; + +TEST_SUBMODULE(class_sh_unique_ptr_member, m) { + py::classh(m, "pointee").def(py::init<>()).def("get_int", &pointee::get_int); + + m.def("make_unique_pointee", make_unique_pointee); + + py::class_(m, "ptr_owner") + .def(py::init>(), py::arg("ptr")) + .def("is_owner", &ptr_owner::is_owner) + .def("give_up_ownership_via_unique_ptr", &ptr_owner::give_up_ownership_via_unique_ptr) + .def("give_up_ownership_via_shared_ptr", &ptr_owner::give_up_ownership_via_shared_ptr); +} + +} // namespace class_sh_unique_ptr_member +} // namespace pybind11_tests diff --git a/tests/test_class_sh_unique_ptr_member.py b/tests/test_class_sh_unique_ptr_member.py new file mode 100644 index 0000000000..a5d2ccd234 --- /dev/null +++ b/tests/test_class_sh_unique_ptr_member.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import pytest + +from pybind11_tests import class_sh_unique_ptr_member as m + + +def test_make_unique_pointee(): + obj = m.make_unique_pointee() + assert obj.get_int() == 213 + + +@pytest.mark.parametrize( + "give_up_ownership_via", + ["give_up_ownership_via_unique_ptr", "give_up_ownership_via_shared_ptr"], +) +def test_pointee_and_ptr_owner(give_up_ownership_via): + obj = m.pointee() + assert obj.get_int() == 213 + owner = m.ptr_owner(obj) + with pytest.raises(ValueError, match="Python instance was disowned"): + obj.get_int() + assert owner.is_owner() + reclaimed = getattr(owner, give_up_ownership_via)() + assert not owner.is_owner() + assert reclaimed.get_int() == 213 diff --git a/tests/test_class_sh_virtual_py_cpp_mix.cpp b/tests/test_class_sh_virtual_py_cpp_mix.cpp new file mode 100644 index 0000000000..df8af19e48 --- /dev/null +++ b/tests/test_class_sh_virtual_py_cpp_mix.cpp @@ -0,0 +1,58 @@ +#include "pybind11_tests.h" + +#include + +namespace pybind11_tests { +namespace class_sh_virtual_py_cpp_mix { + +class Base { +public: + virtual ~Base() = default; + virtual int get() const { return 101; } + + // Some compilers complain about implicitly defined versions of some of the following: + Base() = default; + Base(const Base &) = default; +}; + +class CppDerivedPlain : public Base { +public: + int get() const override { return 202; } +}; + +class CppDerived : public Base { +public: + int get() const override { return 212; } +}; + +int get_from_cpp_plainc_ptr(const Base *b) { return b->get() + 4000; } + +int get_from_cpp_unique_ptr(std::unique_ptr b) { return b->get() + 5000; } + +struct BaseVirtualOverrider : Base, py::trampoline_self_life_support { + using Base::Base; + + int get() const override { PYBIND11_OVERRIDE(int, Base, get); } +}; + +struct CppDerivedVirtualOverrider : CppDerived, py::trampoline_self_life_support { + using CppDerived::CppDerived; + + int get() const override { PYBIND11_OVERRIDE(int, CppDerived, get); } +}; + +} // namespace class_sh_virtual_py_cpp_mix +} // namespace pybind11_tests + +using namespace pybind11_tests::class_sh_virtual_py_cpp_mix; + +TEST_SUBMODULE(class_sh_virtual_py_cpp_mix, m) { + py::classh(m, "Base").def(py::init<>()).def("get", &Base::get); + + py::classh(m, "CppDerivedPlain").def(py::init<>()); + + py::classh(m, "CppDerived").def(py::init<>()); + + m.def("get_from_cpp_plainc_ptr", get_from_cpp_plainc_ptr, py::arg("b")); + m.def("get_from_cpp_unique_ptr", get_from_cpp_unique_ptr, py::arg("b")); +} diff --git a/tests/test_class_sh_virtual_py_cpp_mix.py b/tests/test_class_sh_virtual_py_cpp_mix.py new file mode 100644 index 0000000000..33133eb889 --- /dev/null +++ b/tests/test_class_sh_virtual_py_cpp_mix.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +import pytest + +from pybind11_tests import class_sh_virtual_py_cpp_mix as m + + +class PyBase(m.Base): # Avoiding name PyDerived, for more systematic naming. + def __init__(self): + m.Base.__init__(self) + + def get(self): + return 323 + + +class PyCppDerived(m.CppDerived): + def __init__(self): + m.CppDerived.__init__(self) + + def get(self): + return 434 + + +@pytest.mark.parametrize( + ("ctor", "expected"), + [ + (m.Base, 101), + (PyBase, 323), + (m.CppDerivedPlain, 202), + (m.CppDerived, 212), + (PyCppDerived, 434), + ], +) +def test_base_get(ctor, expected): + obj = ctor() + assert obj.get() == expected + + +@pytest.mark.parametrize( + ("ctor", "expected"), + [ + (m.Base, 4101), + (PyBase, 4323), + (m.CppDerivedPlain, 4202), + (m.CppDerived, 4212), + (PyCppDerived, 4434), + ], +) +def test_get_from_cpp_plainc_ptr(ctor, expected): + obj = ctor() + assert m.get_from_cpp_plainc_ptr(obj) == expected + + +@pytest.mark.parametrize( + ("ctor", "expected"), + [ + (m.Base, 5101), + (PyBase, 5323), + (m.CppDerivedPlain, 5202), + (m.CppDerived, 5212), + (PyCppDerived, 5434), + ], +) +def test_get_from_cpp_unique_ptr(ctor, expected): + obj = ctor() + assert m.get_from_cpp_unique_ptr(obj) == expected