Skip to content

Commit 9353788

Browse files
pytypes: Resurrect via leak instead of un-finalize in Python 3.8
1 parent cac3edd commit 9353788

File tree

2 files changed

+51
-22
lines changed

2 files changed

+51
-22
lines changed

include/pybind11/pytypes.h

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,7 @@ class wrapper : public Base {
14221422
}
14231423
holder_type_id_ = holder_type_id;
14241424
patient_ = std::move(patient);
1425-
// @note It would be nice to put `revive_python3` here, but this is called by
1425+
// @note It would be nice to put `resurrect_python3` here, but this is called by
14261426
// `PyObject_CallFinalizer`, which will end up reversing its effect anyways.
14271427
}
14281428

@@ -1431,7 +1431,7 @@ class wrapper : public Base {
14311431
if (!lives_in_cpp()) {
14321432
throw std::runtime_error("Instance does not live in C++");
14331433
}
1434-
revive_python3();
1434+
resurrect_python3();
14351435
// Remove existing reference.
14361436
object tmp = std::move(patient_);
14371437
assert(!patient_);
@@ -1462,16 +1462,27 @@ class wrapper : public Base {
14621462
}
14631463
}
14641464

1465-
// Python3 unfortunately will not implicitly call `__del__` multiple times,
1466-
// even if the object is resurrected. This is a dirty workaround.
1467-
// @see https://bugs.python.org/issue32377
1468-
inline void revive_python3() {
1469-
#if PY_VERSION_HEX >= 0x03000000
1470-
// Reverse single-finalization constraint in Python3.
1471-
if (_PyGC_FINALIZED(patient_.ptr())) {
1465+
// Handle PEP 442, implemented in Python3, where resurrection more than once
1466+
// is a bit more dicey.
1467+
inline void resurrect_python3() {
1468+
#if PY_VERSION_HEX >= 0x03080000
1469+
// Leak it as a means to stay alive for now.
1470+
// See: https://bugs.python.org/issue40240
1471+
if (_PyGC_FINALIZED(patient_.ptr())) {
1472+
if (leaked_) {
1473+
throw std::runtime_error("__del__ called twice in Python 3.8+?");
1474+
}
1475+
leaked_ = true;
1476+
patient_.inc_ref();
1477+
}
1478+
#elif PY_VERSION_HEX >= 0x03000000
1479+
// Reverse single-finalization constraint in Python3.
1480+
// This was a really dirty workaround:
1481+
// See: https://bugs.python.org/issue32377
1482+
if (_PyGC_FINALIZED(patient_.ptr())) {
14721483
_PyGC_SET_FINALIZED(patient_.ptr(), 0);
1473-
}
1474-
#endif // PY_VERSION_HEX >= 0x03000000
1484+
}
1485+
#endif // PY_VERSION_HEX >= 0x03080000
14751486
}
14761487

14771488
private:
@@ -1483,6 +1494,9 @@ class wrapper : public Base {
14831494

14841495
object patient_;
14851496
detail::HolderTypeId holder_type_id_{detail::HolderTypeId::Unknown};
1497+
#if PY_VERSION_HEX >= 0x03080000
1498+
bool leaked_{false};
1499+
#endif // PY_VERSION_HEX >= 0x03080000
14861500
};
14871501

14881502
NAMESPACE_BEGIN(detail)

tests/test_ownership_transfer.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pybind11_tests import ConstructorStats
33

44
import pytest
5+
import sys
56
import weakref
67

78

@@ -44,7 +45,15 @@ def get_cstats():
4445
# capturing deletion properly.
4546
@pytest.unsupported_on_pypy
4647
def test_shared_ptr_derived_slicing(capture):
47-
from sys import getrefcount
48+
leaked_count = [0]
49+
is_py38 = sys.version_info[:2] >= (3, 8)
50+
51+
def py38_leak():
52+
if is_py38:
53+
leaked_count[0] += 1
54+
55+
def cstats_alive_except_leaked():
56+
return cstats.alive() - leaked_count[0]
4857

4958
# [ Bad ]
5059
cstats = ChildBad.get_cstats()
@@ -75,42 +84,48 @@ def test_shared_ptr_derived_slicing(capture):
7584
assert cstats.alive() == 1
7685
assert obj.value() == 100
7786
del obj
78-
assert cstats.alive() == 0
87+
py38_leak()
88+
assert cstats_alive_except_leaked() == 0
7989
# Use something more permanent.
8090
obj = Child() # Use factory method.
8191
obj_weak = weakref.ref(obj)
82-
assert cstats.alive() == 1
92+
assert cstats_alive_except_leaked() == 1
8393
c = m.BaseContainer(obj)
8494
del obj
85-
assert cstats.alive() == 1
95+
assert cstats_alive_except_leaked() == 1
8696
# We now still have a reference to the object. py::wrapper<> will intercept Python's
8797
# attempt to destroy `obj`, is aware the `shared_ptr<>.use_count() > 1`, and will increase
8898
# the ref count by transferring a new reference to `py::wrapper<>` (thus reviving the object,
8999
# per Python's documentation of __del__).
90100
assert obj_weak() is not None
91-
assert cstats.alive() == 1
101+
assert cstats_alive_except_leaked() == 1
92102
# This goes from C++ -> Python, and then Python -> C++ once this statement has finished.
93103
assert c.get().value() == 100
94-
assert cstats.alive() == 1
104+
assert cstats_alive_except_leaked() == 1
95105
# Destroy references (effectively in C++), and ensure that we have the desired behavior.
96106
del c
97-
assert cstats.alive() == 0
107+
py38_leak()
108+
assert cstats_alive_except_leaked() == 0
98109

99110
# Ensure that we can pass it from Python -> C++ -> Python, and ensure that C++ does not think
100111
# that it has ownership.
101112
obj = Child(10)
102113
c = m.BaseContainer(obj)
103114
del obj
104-
assert cstats.alive() == 1
115+
assert cstats_alive_except_leaked() == 1
105116
obj = c.get()
106117
# Now that we have it in Python, there should only be 1 Python reference, since
107118
# py::wrapper<> in C++ should have released its reference.
108-
assert getrefcount(obj) == 2
119+
expected_refcount = 2
120+
if is_py38:
121+
expected_refcount += 1
122+
assert sys.getrefcount(obj) == expected_refcount
109123
del c
110-
assert cstats.alive() == 1
124+
assert cstats_alive_except_leaked() == 1
111125
assert obj.value() == 100
112126
del obj
113-
assert cstats.alive() == 0
127+
py38_leak()
128+
assert cstats_alive_except_leaked() == 0
114129

115130

116131
@pytest.unsupported_on_pypy

0 commit comments

Comments
 (0)