| # Copyright (c) 2025 The pybind Community. | |
| # All rights reserved. Use of this source code is governed by a | |
| # BSD-style license that can be found in the LICENSE file. | |
| # This module tests the interaction of pybind11's shared_ptr and smart_holder | |
| # mechanisms with trampoline object lifetime management and inheritance slicing. | |
| # | |
| # The following combinations are covered: | |
| # | |
| # - Holder type: std::shared_ptr (class_ holder) vs. | |
| # py::smart_holder | |
| # - Conversion function: obj.cast<std::shared_ptr<T>>() vs. | |
| # py::potentially_slicing_weak_ptr<T>(obj) | |
| # - Python object type: C++ base class vs. | |
| # Python-derived trampoline class | |
| # | |
| # The tests verify | |
| # | |
| # - that casting or passing Python objects into functions returns usable | |
| # std::shared_ptr<T> instances. | |
| # - that inheritance slicing occurs as expected in controlled cases | |
| # (issue #1333). | |
| # - that surprising weak_ptr behavior (issue #5623) can be reproduced when | |
| # smart_holder is used. | |
| # - that the trampoline object remains alive in all situations | |
| # (no use-after-free) as long as the C++ shared_ptr exists. | |
| # | |
| # Where applicable, trampoline state is introspected to confirm whether the | |
| # C++ object retains knowledge of the Python override or has fallen back to | |
| # the base implementation. | |
| from __future__ import annotations | |
| import gc | |
| import weakref | |
| import pytest | |
| import env | |
| import pybind11_tests.potentially_slicing_weak_ptr as m | |
| class PyDrvdSH(m.VirtBaseSH): | |
| def get_code(self): | |
| return 200 | |
| class PyDrvdSP(m.VirtBaseSP): | |
| def get_code(self): | |
| return 200 | |
| VIRT_BASE_TYPES = { | |
| "SH": {100: m.VirtBaseSH, 200: PyDrvdSH}, | |
| "SP": {100: m.VirtBaseSP, 200: PyDrvdSP}, | |
| } | |
| RTRN_FUNCS = { | |
| "SH": { | |
| "oc": m.SH_rtrn_obj_cast_shared_ptr, | |
| "ps": m.SH_rtrn_potentially_slicing_shared_ptr, | |
| }, | |
| "SP": { | |
| "oc": m.SP_rtrn_obj_cast_shared_ptr, | |
| "ps": m.SP_rtrn_potentially_slicing_shared_ptr, | |
| }, | |
| } | |
| SP_OWNER_TYPES = { | |
| "SH": m.SH_SpOwner, | |
| "SP": m.SP_SpOwner, | |
| } | |
| WP_OWNER_TYPES = { | |
| "SH": m.SH_WpOwner, | |
| "SP": m.SP_WpOwner, | |
| } | |
| GC_IS_RELIABLE = not (env.PYPY or env.GRAALPY) | |
| @pytest.mark.parametrize("expected_code", [100, 200]) | |
| @pytest.mark.parametrize("rtrn_kind", ["oc", "ps"]) | |
| @pytest.mark.parametrize("holder_kind", ["SH", "SP"]) | |
| def test_rtrn_obj_cast_shared_ptr(holder_kind, rtrn_kind, expected_code): | |
| obj = VIRT_BASE_TYPES[holder_kind][expected_code]() | |
| ptr = RTRN_FUNCS[holder_kind][rtrn_kind](obj) | |
| assert ptr.get_code() == expected_code | |
| objref = weakref.ref(obj) | |
| del obj | |
| gc.collect() | |
| assert ptr.get_code() == expected_code # the ptr Python object keeps obj alive | |
| assert objref() is not None | |
| del ptr | |
| gc.collect() | |
| if GC_IS_RELIABLE: | |
| assert objref() is None | |
| @pytest.mark.parametrize("expected_code", [100, 200]) | |
| @pytest.mark.parametrize("holder_kind", ["SH", "SP"]) | |
| def test_with_sp_owner(holder_kind, expected_code): | |
| spo = SP_OWNER_TYPES[holder_kind]() | |
| assert spo.get_code() == -888 | |
| assert spo.get_trampoline_state() == "sp nullptr" | |
| obj = VIRT_BASE_TYPES[holder_kind][expected_code]() | |
| assert obj.get_code() == expected_code | |
| spo.set_sp(obj) | |
| assert spo.get_code() == expected_code | |
| expected_trampoline_state = ( | |
| "dynamic_cast failed" if expected_code == 100 else "trampoline alive" | |
| ) | |
| assert spo.get_trampoline_state() == expected_trampoline_state | |
| del obj | |
| gc.collect() | |
| if holder_kind == "SH": | |
| assert spo.get_code() == expected_code | |
| elif GC_IS_RELIABLE: | |
| assert ( | |
| spo.get_code() == 100 | |
| ) # see issue #1333 (inheritance slicing) and PR #5624 | |
| assert spo.get_trampoline_state() == expected_trampoline_state | |
| @pytest.mark.parametrize("expected_code", [100, 200]) | |
| @pytest.mark.parametrize("set_meth", ["set_wp", "set_wp_potentially_slicing"]) | |
| @pytest.mark.parametrize("holder_kind", ["SH", "SP"]) | |
| def test_with_wp_owner(holder_kind, set_meth, expected_code): | |
| wpo = WP_OWNER_TYPES[holder_kind]() | |
| assert wpo.get_code() == -999 | |
| assert wpo.get_trampoline_state() == "sp nullptr" | |
| obj = VIRT_BASE_TYPES[holder_kind][expected_code]() | |
| assert obj.get_code() == expected_code | |
| getattr(wpo, set_meth)(obj) | |
| if ( | |
| holder_kind == "SP" | |
| or expected_code == 100 | |
| or set_meth == "set_wp_potentially_slicing" | |
| ): | |
| assert wpo.get_code() == expected_code | |
| else: | |
| assert wpo.get_code() == -999 # see issue #5623 (weak_ptr expired) and PR #5624 | |
| if expected_code == 100: | |
| expected_trampoline_state = "dynamic_cast failed" | |
| elif holder_kind == "SH" and set_meth == "set_wp": | |
| expected_trampoline_state = "sp nullptr" | |
| else: | |
| expected_trampoline_state = "trampoline alive" | |
| assert wpo.get_trampoline_state() == expected_trampoline_state | |
| del obj | |
| gc.collect() | |
| if GC_IS_RELIABLE: | |
| assert wpo.get_code() == -999 | |
| def test_potentially_slicing_weak_ptr_not_convertible_error(): | |
| with pytest.raises(Exception) as excinfo: | |
| m.SH_rtrn_potentially_slicing_shared_ptr("") | |
| assert str(excinfo.value) == ( | |
| '"str" object is not convertible to std::weak_ptr<T>' | |
| " (with T = pybind11_tests::potentially_slicing_weak_ptr::VirtBase<0>)" | |
| ) | |
| with pytest.raises(Exception) as excinfo: | |
| m.SP_rtrn_potentially_slicing_shared_ptr([]) | |
| assert str(excinfo.value) == ( | |
| '"list" object is not convertible to std::weak_ptr<T>' | |
| " (with T = pybind11_tests::potentially_slicing_weak_ptr::VirtBase<1>)" | |
| ) |