| # 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>)" |
| ) |