Skip to content

Failing test for std::weak_ptr used with derived Python class and py::smart_holder #5624

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
8 changes: 8 additions & 0 deletions include/pybind11/detail/type_caster_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,14 @@ struct load_helper : value_and_holder_helper {
if (released_ptr) {
return std::shared_ptr<T>(released_ptr, type_raw_ptr);
}
auto *self_life_support
= dynamic_raw_ptr_cast_if_possible<trampoline_self_life_support>(type_raw_ptr);
if (self_life_support == nullptr) {
std::shared_ptr<void> void_shd_ptr = hld.template as_shared_ptr<void>();
std::shared_ptr<T> to_be_released(void_shd_ptr, type_raw_ptr);
vptr_gd_ptr->released_ptr = to_be_released;
return to_be_released;
}
std::shared_ptr<T> to_be_released(
type_raw_ptr, shared_ptr_trampoline_self_life_support(loaded_v_h.inst));
vptr_gd_ptr->released_ptr = to_be_released;
Expand Down
2 changes: 2 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,11 @@ set(PYBIND11_TEST_FILES
test_class_sh_trampoline_shared_from_this
test_class_sh_trampoline_shared_ptr_cpp_arg
test_class_sh_trampoline_unique_ptr
test_class_sh_trampoline_weak_ptr
test_class_sh_unique_ptr_custom_deleter
test_class_sh_unique_ptr_member
test_class_sh_virtual_py_cpp_mix
test_class_sp_trampoline_weak_ptr
test_const_name
test_constants_and_functions
test_copy_move
Expand Down
2 changes: 1 addition & 1 deletion tests/test_class_sh_trampoline_shared_ptr_cpp_arg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct SpBase {

std::shared_ptr<SpBase> pass_through_shd_ptr(const std::shared_ptr<SpBase> &obj) { return obj; }

struct PySpBase : SpBase {
struct PySpBase : SpBase, py::trampoline_self_life_support {
using SpBase::SpBase;
bool is_base_used() override { PYBIND11_OVERRIDE(bool, SpBase, is_base_used); }
};
Expand Down
63 changes: 63 additions & 0 deletions tests/test_class_sh_trampoline_weak_ptr.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) 2025 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 <memory>

namespace pybind11_tests {
namespace class_sh_trampoline_weak_ptr {

struct VirtBase {
virtual ~VirtBase() = default;
virtual int get_code() { return 100; }
};

struct PyVirtBase : VirtBase /*, py::trampoline_self_life_support */ {
using VirtBase::VirtBase;
int get_code() override { PYBIND11_OVERRIDE(int, VirtBase, get_code); }

~PyVirtBase() override {
fflush(stderr);
printf("\nLOOOK ~PyVirtBase()\n");
fflush(stdout);
}
};

struct WpOwner {
void set_wp(const std::shared_ptr<VirtBase> &sp) { wp = sp; }

int get_code() {
auto sp = wp.lock();
if (!sp) {
return -999;
}
return sp->get_code();
}

private:
std::weak_ptr<VirtBase> wp;
};

std::shared_ptr<VirtBase> pass_through_sp_VirtBase(const std::shared_ptr<VirtBase> &sp) {
return sp;
}

} // namespace class_sh_trampoline_weak_ptr
} // namespace pybind11_tests

using namespace pybind11_tests::class_sh_trampoline_weak_ptr;

TEST_SUBMODULE(class_sh_trampoline_weak_ptr, m) {
py::classh<VirtBase, PyVirtBase>(m, "VirtBase")
.def(py::init<>())
.def("get_code", &VirtBase::get_code);

py::classh<WpOwner>(m, "WpOwner")
.def(py::init<>())
.def("set_wp", &WpOwner::set_wp)
.def("get_code", &WpOwner::get_code);

m.def("pass_through_sp_VirtBase", pass_through_sp_VirtBase);
}
43 changes: 43 additions & 0 deletions tests/test_class_sh_trampoline_weak_ptr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

import gc

import pytest

import env
import pybind11_tests.class_sh_trampoline_weak_ptr as m


class PyDrvd(m.VirtBase):
def get_code(self):
return 200


@pytest.mark.parametrize(("vtype", "expected_code"), [(m.VirtBase, 100), (PyDrvd, 200)])
def test_weak_ptr_owner(vtype, expected_code):
wpo = m.WpOwner()
assert wpo.get_code() == -999

obj = vtype()
assert obj.get_code() == expected_code

wpo.set_wp(obj)
assert wpo.get_code() == expected_code

del obj
if env.PYPY or env.GRAALPY:
pytest.skip("Cannot reliably trigger GC")
assert wpo.get_code() == -999


@pytest.mark.parametrize(("vtype", "expected_code"), [(m.VirtBase, 100), (PyDrvd, 200)])
def test_pass_through_sp_VirtBase(vtype, expected_code):
obj = vtype()
ptr = m.pass_through_sp_VirtBase(obj)
print("\nLOOOK BEFORE del obj", flush=True)
del obj
print("\nLOOOK AFTER del obj", flush=True)
gc.collect()
print("\nLOOOK AFTER gc.collect()", flush=True)
assert ptr.get_code() == expected_code
print("\nLOOOK AFTER ptr.get_code()", flush=True)
82 changes: 82 additions & 0 deletions tests/test_class_sp_trampoline_weak_ptr.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2025 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 <memory>

namespace pybind11_tests {
namespace class_sp_trampoline_weak_ptr {

struct VirtBase {
virtual ~VirtBase() = default;
virtual int get_code() { return 100; }
};

struct PyVirtBase : VirtBase, py::trampoline_self_life_support {
using VirtBase::VirtBase;
int get_code() override { PYBIND11_OVERRIDE(int, VirtBase, get_code); }

~PyVirtBase() override {
fflush(stderr);
printf("\nLOOOK ~PyVirtBase()\n");
fflush(stdout);
}
};

struct WpOwner {
void set_wp(const std::shared_ptr<VirtBase> &sp) { wp = sp; }

int get_code() {
auto sp = wp.lock();
if (!sp) {
return -999;
}
return sp->get_code();
}

private:
std::weak_ptr<VirtBase> wp;
};

struct SpOwner {
void set_sp(const std::shared_ptr<VirtBase> &sp_) { sp = sp_; }

int get_code() {
if (!sp) {
return -888;
}
return sp->get_code();
}

private:
std::shared_ptr<VirtBase> sp;
};

std::shared_ptr<VirtBase> pass_through_sp_VirtBase(const std::shared_ptr<VirtBase> &sp) {
return sp;
}

} // namespace class_sp_trampoline_weak_ptr
} // namespace pybind11_tests

using namespace pybind11_tests::class_sp_trampoline_weak_ptr;

TEST_SUBMODULE(class_sp_trampoline_weak_ptr, m) {
py::class_<VirtBase, std::shared_ptr<VirtBase>, PyVirtBase>(m, "VirtBase")
.def(py::init<>())
.def("get_code", &VirtBase::get_code);

py::class_<WpOwner>(m, "WpOwner")
.def(py::init<>())
.def("set_wp", &WpOwner::set_wp)
.def("get_code", &WpOwner::get_code);

py::class_<SpOwner>(m, "SpOwner")
.def(py::init<>())
.def("set_sp", &SpOwner::set_sp)
.def("get_code", &SpOwner::get_code);

m.def("pass_through_sp_VirtBase", pass_through_sp_VirtBase);
}
86 changes: 86 additions & 0 deletions tests/test_class_sp_trampoline_weak_ptr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from __future__ import annotations

import gc

import pytest

import env
import pybind11_tests.class_sp_trampoline_weak_ptr as m


class PyDrvd(m.VirtBase):
def get_code(self):
return 200


@pytest.mark.parametrize(("vtype", "expected_code"), [(m.VirtBase, 100), (PyDrvd, 200)])
def test_with_wp_owner(vtype, expected_code):
wpo = m.WpOwner()
assert wpo.get_code() == -999

obj = vtype()
assert obj.get_code() == expected_code

wpo.set_wp(obj)
assert wpo.get_code() == expected_code

del obj
if env.PYPY or env.GRAALPY:
pytest.skip("Cannot reliably trigger GC")
assert wpo.get_code() == -999


@pytest.mark.parametrize(("vtype", "expected_code"), [(m.VirtBase, 100), (PyDrvd, 200)])
def test_with_sp_owner(vtype, expected_code):
spo = m.SpOwner()
assert spo.get_code() == -888

obj = vtype()
assert obj.get_code() == expected_code

spo.set_sp(obj)
assert spo.get_code() == expected_code

del obj
if env.PYPY or env.GRAALPY:
pytest.skip("Cannot reliably trigger GC")
print("\nLOOOK BEFORE spo.get_code() AFTER del obj", flush=True)
assert spo.get_code() == 100 # Inheritance slicing (issue #1333)
print("\nLOOOK AFTER spo.get_code() AFTER del obj", flush=True)


@pytest.mark.parametrize(("vtype", "expected_code"), [(m.VirtBase, 100), (PyDrvd, 200)])
def test_with_sp_and_wp_owners(vtype, expected_code):
spo = m.SpOwner()
wpo = m.WpOwner()

obj = vtype()
spo.set_sp(obj)
wpo.set_wp(obj)

assert spo.get_code() == expected_code
assert wpo.get_code() == expected_code

del obj
if env.PYPY or env.GRAALPY:
pytest.skip("Cannot reliably trigger GC")

# Inheritance slicing (issue #1333)
assert spo.get_code() == 100
assert wpo.get_code() == 100

del spo
assert wpo.get_code() == -999


@pytest.mark.parametrize(("vtype", "expected_code"), [(m.VirtBase, 100), (PyDrvd, 200)])
def test_pass_through_sp_VirtBase(vtype, expected_code):
obj = vtype()
ptr = m.pass_through_sp_VirtBase(obj)
print("\nLOOOK BEFORE del obj", flush=True)
del obj
print("\nLOOOK AFTER del obj", flush=True)
gc.collect()
print("\nLOOOK AFTER gc.collect()", flush=True)
assert ptr.get_code() == expected_code
print("\nLOOOK AFTER ptr.get_code()", flush=True)
Loading