mirror of https://github.com/pybind/pybind11
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
174 lines
5.5 KiB
174 lines
5.5 KiB
# 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>)" |
|
)
|
|
|