Skip to content

Commit 34a118f

Browse files
authored
Add release_gil_before_calling_cpp_dtor annotation for class_ (#5522)
* Backport of google/pybind11clif#30088 (main PR) and google/pybind11clif#30092 (minor fixes). Note for completeness: These are identical to the current versions on the pybind11clif main branch (@ commit 4841661df5daf26ecdedaace54e64d0782e63f64): * test_class_release_gil_before_calling_cpp_dtor.cpp * test_class_release_gil_before_calling_cpp_dtor.py * Fix potential data race in test_class_release_gil_before_calling_cpp_dtor.cpp The original intent was to let the singleton leak, but making that tread-safe is slightly more involved than this solution. It's totally fine in this case if the RegistryType destructor runs on process teardown. See also: #5522 (comment) --------- Co-authored-by: Ralf W. Grosse-Kunstleve <[email protected]>
1 parent c316cf3 commit 34a118f

5 files changed

+132
-11
lines changed

include/pybind11/attr.h

+17-1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ struct dynamic_attr {};
8181
/// Annotation which enables the buffer protocol for a type
8282
struct buffer_protocol {};
8383

84+
/// Annotation which enables releasing the GIL before calling the C++ destructor of wrapped
85+
/// instances (pybind/pybind11#1446).
86+
struct release_gil_before_calling_cpp_dtor {};
87+
8488
/// Annotation which requests that a special metaclass is created for a type
8589
struct metaclass {
8690
handle value;
@@ -272,7 +276,8 @@ struct function_record {
272276
struct type_record {
273277
PYBIND11_NOINLINE type_record()
274278
: multiple_inheritance(false), dynamic_attr(false), buffer_protocol(false),
275-
default_holder(true), module_local(false), is_final(false) {}
279+
default_holder(true), module_local(false), is_final(false),
280+
release_gil_before_calling_cpp_dtor(false) {}
276281

277282
/// Handle to the parent scope
278283
handle scope;
@@ -331,6 +336,9 @@ struct type_record {
331336
/// Is the class inheritable from python classes?
332337
bool is_final : 1;
333338

339+
/// Solves pybind/pybind11#1446
340+
bool release_gil_before_calling_cpp_dtor : 1;
341+
334342
PYBIND11_NOINLINE void add_base(const std::type_info &base, void *(*caster)(void *) ) {
335343
auto *base_info = detail::get_type_info(base, false);
336344
if (!base_info) {
@@ -603,6 +611,14 @@ struct process_attribute<module_local> : process_attribute_default<module_local>
603611
static void init(const module_local &l, type_record *r) { r->module_local = l.value; }
604612
};
605613

614+
template <>
615+
struct process_attribute<release_gil_before_calling_cpp_dtor>
616+
: process_attribute_default<release_gil_before_calling_cpp_dtor> {
617+
static void init(const release_gil_before_calling_cpp_dtor &, type_record *r) {
618+
r->release_gil_before_calling_cpp_dtor = true;
619+
}
620+
};
621+
606622
/// Process a 'prepend' attribute, putting this at the beginning of the overload chain
607623
template <>
608624
struct process_attribute<prepend> : process_attribute_default<prepend> {

include/pybind11/pybind11.h

+40-10
Original file line numberDiff line numberDiff line change
@@ -1665,7 +1665,6 @@ class class_ : public detail::generic_type {
16651665
record.type_align = alignof(conditional_t<has_alias, type_alias, type> &);
16661666
record.holder_size = sizeof(holder_type);
16671667
record.init_instance = init_instance;
1668-
record.dealloc = dealloc;
16691668
record.default_holder = detail::is_instantiation<std::unique_ptr, holder_type>::value;
16701669

16711670
set_operator_new<type>(&record);
@@ -1676,6 +1675,12 @@ class class_ : public detail::generic_type {
16761675
/* Process optional arguments, if any */
16771676
process_attributes<Extra...>::init(extra..., &record);
16781677

1678+
if (record.release_gil_before_calling_cpp_dtor) {
1679+
record.dealloc = dealloc_release_gil_before_calling_cpp_dtor;
1680+
} else {
1681+
record.dealloc = dealloc_without_manipulating_gil;
1682+
}
1683+
16791684
generic_type::initialize(record);
16801685

16811686
if (has_alias) {
@@ -1996,15 +2001,14 @@ class class_ : public detail::generic_type {
19962001
init_holder(inst, v_h, (const holder_type *) holder_ptr, v_h.value_ptr<type>());
19972002
}
19982003

1999-
/// Deallocates an instance; via holder, if constructed; otherwise via operator delete.
2000-
static void dealloc(detail::value_and_holder &v_h) {
2001-
// We could be deallocating because we are cleaning up after a Python exception.
2002-
// If so, the Python error indicator will be set. We need to clear that before
2003-
// running the destructor, in case the destructor code calls more Python.
2004-
// If we don't, the Python API will exit with an exception, and pybind11 will
2005-
// throw error_already_set from the C++ destructor which is forbidden and triggers
2006-
// std::terminate().
2007-
error_scope scope;
2004+
// Deallocates an instance; via holder, if constructed; otherwise via operator delete.
2005+
// NOTE: The Python error indicator needs to cleared BEFORE this function is called.
2006+
// This is because we could be deallocating while cleaning up after a Python exception.
2007+
// If the error indicator is not cleared but the C++ destructor code makes Python C API
2008+
// calls, those calls are likely to generate a new exception, and pybind11 will then
2009+
// throw `error_already_set` from the C++ destructor. This is forbidden and will
2010+
// trigger std::terminate().
2011+
static void dealloc_impl(detail::value_and_holder &v_h) {
20082012
if (v_h.holder_constructed()) {
20092013
v_h.holder<holder_type>().~holder_type();
20102014
v_h.set_holder_constructed(false);
@@ -2015,6 +2019,32 @@ class class_ : public detail::generic_type {
20152019
v_h.value_ptr() = nullptr;
20162020
}
20172021

2022+
static void dealloc_without_manipulating_gil(detail::value_and_holder &v_h) {
2023+
error_scope scope;
2024+
dealloc_impl(v_h);
2025+
}
2026+
2027+
static void dealloc_release_gil_before_calling_cpp_dtor(detail::value_and_holder &v_h) {
2028+
error_scope scope;
2029+
// Intentionally not using `gil_scoped_release` because the non-simple
2030+
// version unconditionally calls `get_internals()`.
2031+
// `Py_BEGIN_ALLOW_THREADS`, `Py_END_ALLOW_THREADS` cannot be used
2032+
// because those macros include `{` and `}`.
2033+
PyThreadState *py_ts = PyEval_SaveThread();
2034+
try {
2035+
dealloc_impl(v_h);
2036+
} catch (...) {
2037+
// This code path is expected to be unreachable unless there is a
2038+
// bug in pybind11 itself.
2039+
// An alternative would be to mark this function, or
2040+
// `dealloc_impl()`, with `nothrow`, but that would be a subtle
2041+
// behavior change and could make debugging more difficult.
2042+
PyEval_RestoreThread(py_ts);
2043+
throw;
2044+
}
2045+
PyEval_RestoreThread(py_ts);
2046+
}
2047+
20182048
static detail::function_record *get_function_record(handle h) {
20192049
h = detail::get_function(h);
20202050
if (!h) {

tests/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ set(PYBIND11_TEST_FILES
115115
test_callbacks
116116
test_chrono
117117
test_class
118+
test_class_release_gil_before_calling_cpp_dtor
118119
test_const_name
119120
test_constants_and_functions
120121
test_copy_move
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#include <pybind11/pybind11.h>
2+
3+
#include "pybind11_tests.h"
4+
5+
#include <string>
6+
#include <unordered_map>
7+
8+
namespace pybind11_tests {
9+
namespace class_release_gil_before_calling_cpp_dtor {
10+
11+
using RegistryType = std::unordered_map<std::string, int>;
12+
13+
static RegistryType &PyGILState_Check_Results() {
14+
static RegistryType singleton; // Local static variables have thread-safe initialization.
15+
return singleton;
16+
}
17+
18+
template <int> // Using int as a trick to easily generate a series of types.
19+
struct ProbeType {
20+
private:
21+
std::string unique_key;
22+
23+
public:
24+
explicit ProbeType(const std::string &unique_key) : unique_key{unique_key} {}
25+
26+
~ProbeType() {
27+
RegistryType &reg = PyGILState_Check_Results();
28+
assert(reg.count(unique_key) == 0);
29+
reg[unique_key] = PyGILState_Check();
30+
}
31+
};
32+
33+
} // namespace class_release_gil_before_calling_cpp_dtor
34+
} // namespace pybind11_tests
35+
36+
TEST_SUBMODULE(class_release_gil_before_calling_cpp_dtor, m) {
37+
using namespace pybind11_tests::class_release_gil_before_calling_cpp_dtor;
38+
39+
py::class_<ProbeType<0>>(m, "ProbeType0").def(py::init<std::string>());
40+
41+
py::class_<ProbeType<1>>(m, "ProbeType1", py::release_gil_before_calling_cpp_dtor())
42+
.def(py::init<std::string>());
43+
44+
m.def("PopPyGILState_Check_Result", [](const std::string &unique_key) -> std::string {
45+
RegistryType &reg = PyGILState_Check_Results();
46+
if (reg.count(unique_key) == 0) {
47+
return "MISSING";
48+
}
49+
int res = reg[unique_key];
50+
reg.erase(unique_key);
51+
return std::to_string(res);
52+
});
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from __future__ import annotations
2+
3+
import gc
4+
5+
import pytest
6+
7+
from pybind11_tests import class_release_gil_before_calling_cpp_dtor as m
8+
9+
10+
@pytest.mark.parametrize(
11+
("probe_type", "unique_key", "expected_result"),
12+
[
13+
(m.ProbeType0, "without_manipulating_gil", "1"),
14+
(m.ProbeType1, "release_gil_before_calling_cpp_dtor", "0"),
15+
],
16+
)
17+
def test_gil_state_check_results(probe_type, unique_key, expected_result):
18+
probe_type(unique_key)
19+
gc.collect()
20+
result = m.PopPyGILState_Check_Result(unique_key)
21+
assert result == expected_result

0 commit comments

Comments
 (0)