1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
|
# 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>)"
)
|