Skip to content

Commit ce00785

Browse files
authored
Add release_gil_before_calling_cpp_dtor annotation for class_ (#30088)
* Add `release_gil_before_calling_cpp_dtor` annotation for `class_`. New tests still missing. * UNRELATED: comment out mingw-w64-${{matrix.env}}-python-scipy * UNRELATED: remove mingw-w64-${{matrix.env}}-python-scipy * UNRELATED: remove mingw-w64-${{matrix.env}}-python-scipy (ci_sh_def.yml) * Add test_class_release_gil_before_calling_cpp_dtor * Revert "UNRELATED: remove mingw-w64-${{matrix.env}}-python-scipy (ci_sh_def.yml)" This reverts commit d7a9411. * Revert "UNRELATED: remove mingw-w64-${{matrix.env}}-python-scipy" This reverts commit d541d1d. * Revert "UNRELATED: comment out mingw-w64-${{matrix.env}}-python-scipy" This reverts commit 7e57bd7. * Improved code organization and more comments. * Rewrite source code comment in response to review comment by @rainwoodman
1 parent 015834b commit ce00785

File tree

5 files changed

+128
-11
lines changed

5 files changed

+128
-11
lines changed

include/pybind11/attr.h

Lines changed: 17 additions & 1 deletion
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;
@@ -281,7 +285,8 @@ struct function_record {
281285
struct type_record {
282286
PYBIND11_NOINLINE type_record()
283287
: multiple_inheritance(false), dynamic_attr(false), buffer_protocol(false),
284-
default_holder(true), module_local(false), is_final(false) {}
288+
default_holder(true), module_local(false), is_final(false),
289+
release_gil_before_calling_cpp_dtor(false) {}
285290

286291
/// Handle to the parent scope
287292
handle scope;
@@ -340,6 +345,9 @@ struct type_record {
340345
/// Is the class inheritable from python classes?
341346
bool is_final : 1;
342347

348+
/// Solves pybind/pybind11#1446
349+
bool release_gil_before_calling_cpp_dtor : 1;
350+
343351
PYBIND11_NOINLINE void add_base(const std::type_info &base, void *(*caster)(void *) ) {
344352
auto *base_info = detail::get_type_info(base, false);
345353
if (!base_info) {
@@ -636,6 +644,14 @@ struct process_attribute<module_local> : process_attribute_default<module_local>
636644
static void init(const module_local &l, type_record *r) { r->module_local = l.value; }
637645
};
638646

647+
template <>
648+
struct process_attribute<release_gil_before_calling_cpp_dtor>
649+
: process_attribute_default<release_gil_before_calling_cpp_dtor> {
650+
static void init(const release_gil_before_calling_cpp_dtor &, type_record *r) {
651+
r->release_gil_before_calling_cpp_dtor = true;
652+
}
653+
};
654+
639655
/// Process a 'prepend' attribute, putting this at the beginning of the overload chain
640656
template <>
641657
struct process_attribute<prepend> : process_attribute_default<prepend> {

include/pybind11/pybind11.h

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,7 +1831,6 @@ class class_ : public detail::generic_type {
18311831
record.type_align = alignof(conditional_t<has_alias, type_alias, type> &);
18321832
record.holder_size = sizeof(holder_type);
18331833
record.init_instance = init_instance;
1834-
record.dealloc = dealloc;
18351834

18361835
// A more fitting name would be uses_unique_ptr_holder.
18371836
record.default_holder = detail::is_instantiation<std::unique_ptr, holder_type>::value;
@@ -1844,6 +1843,12 @@ class class_ : public detail::generic_type {
18441843
/* Process optional arguments, if any */
18451844
process_attributes<Extra...>::init(extra..., &record);
18461845

1846+
if (record.release_gil_before_calling_cpp_dtor) {
1847+
record.dealloc = dealloc_release_gil_before_calling_cpp_dtor;
1848+
} else {
1849+
record.dealloc = dealloc_without_manipulating_gil;
1850+
}
1851+
18471852
generic_type_initialize(record);
18481853

18491854
if (has_alias) {
@@ -2193,15 +2198,14 @@ class class_ : public detail::generic_type {
21932198
inst, holder_ptr);
21942199
}
21952200

2196-
/// Deallocates an instance; via holder, if constructed; otherwise via operator delete.
2197-
static void dealloc(detail::value_and_holder &v_h) {
2198-
// We could be deallocating because we are cleaning up after a Python exception.
2199-
// If so, the Python error indicator will be set. We need to clear that before
2200-
// running the destructor, in case the destructor code calls more Python.
2201-
// If we don't, the Python API will exit with an exception, and pybind11 will
2202-
// throw error_already_set from the C++ destructor which is forbidden and triggers
2203-
// std::terminate().
2204-
error_scope scope;
2201+
// Deallocates an instance; via holder, if constructed; otherwise via operator delete.
2202+
// NOTE: The Python error indicator needs to cleared BEFORE this function is called.
2203+
// This is because we could be deallocating while cleaning up after a Python exception.
2204+
// If the error indicator is not cleared but the C++ destructor code makes Python C API
2205+
// calls, those calls are likely to generate a new exception, and pybind11 will then
2206+
// throw `error_already_set` from the C++ destructor. This is forbidden and will
2207+
// trigger std::terminate().
2208+
static void dealloc_impl(detail::value_and_holder &v_h) {
22052209
if (v_h.holder_constructed()) {
22062210
v_h.holder<holder_type>().~holder_type();
22072211
v_h.set_holder_constructed(false);
@@ -2212,6 +2216,32 @@ class class_ : public detail::generic_type {
22122216
v_h.value_ptr() = nullptr;
22132217
}
22142218

2219+
static void dealloc_without_manipulating_gil(detail::value_and_holder &v_h) {
2220+
error_scope scope;
2221+
dealloc_impl(v_h);
2222+
}
2223+
2224+
static void dealloc_release_gil_before_calling_cpp_dtor(detail::value_and_holder &v_h) {
2225+
error_scope scope;
2226+
// Intentionally not using `gil_scoped_release` because the non-simple
2227+
// version unconditionally calls `get_internals()`.
2228+
// `Py_BEGIN_ALLOW_THREADS`, `Py_END_ALLOW_THREADS` cannot be used
2229+
// because those macros include `{` and `}`.
2230+
PyThreadState *py_ts = PyEval_SaveThread();
2231+
try {
2232+
dealloc_impl(v_h);
2233+
} catch (...) {
2234+
// This code path is expected to be unreachable unless there is a
2235+
// bug in pybind11 itself.
2236+
// An alternative would be to mark this function, or
2237+
// `dealloc_impl()`, with `nothrow`, but that would be a subtle
2238+
// behavior change and could make debugging more difficult.
2239+
PyEval_RestoreThread(py_ts);
2240+
throw;
2241+
}
2242+
PyEval_RestoreThread(py_ts);
2243+
}
2244+
22152245
static detail::function_record *get_function_record(handle h) {
22162246
h = detail::get_function(h);
22172247
if (!h) {

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ set(PYBIND11_TEST_FILES
119119
test_callbacks
120120
test_chrono
121121
test_class
122+
test_class_release_gil_before_calling_cpp_dtor
122123
test_class_sh_basic
123124
test_class_sh_disowning
124125
test_class_sh_disowning_mi
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
RegistryType &PyGILState_Check_Results() {
14+
static auto *singleton = new RegistryType();
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("GetPyGILState_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+
return std::to_string(reg[unique_key]);
50+
});
51+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import gc
2+
3+
import pytest
4+
5+
from pybind11_tests import class_release_gil_before_calling_cpp_dtor as m
6+
7+
8+
@pytest.mark.parametrize(
9+
("probe_type", "unique_key", "expected_result"),
10+
[
11+
(m.ProbeType0, "without_manipulating_gil", "1"),
12+
(m.ProbeType1, "release_gil_before_calling_cpp_dtor", "0"),
13+
],
14+
)
15+
def test_gil_state_check_results(probe_type, unique_key, expected_result):
16+
probe_type(unique_key)
17+
gc.collect()
18+
result = m.GetPyGILState_Check_Result(unique_key)
19+
assert result == expected_result

0 commit comments

Comments
 (0)