Skip to content

Commit cdae8ab

Browse files
committed
Fix builtin exception handlers to work across modules
The builtin exception handler currently doesn't work across modules under clang/libc++ for builtin pybind exceptions like `pybind11::error_already_set` or `pybind11::stop_iteration`: under RTLD_LOCAL module loading clang considers each module's exception classes distinct types. This then means that the base exception translator fails to catch the exceptions and the fall through to the generic `std::exception` handler, which completely breaks things like `stop_iteration`: only the `stop_iteration` of the first module loaded actually works properly; later modules raise a RuntimeError with no message when trying to invoke their iterators. For example, two modules defined like this exhibit the behaviour under clang++/libc++: z1.cpp: #include <pybind11/pybind11.h> #include <pybind11/stl_bind.h> namespace py = pybind11; PYBIND11_MODULE(z1, m) { py::bind_vector<std::vector<long>>(m, "IntVector"); } z2.cpp: #include <pybind11/pybind11.h> #include <pybind11/stl_bind.h> namespace py = pybind11; PYBIND11_MODULE(z2, m) { py::bind_vector<std::vector<double>>(m, "FloatVector"); } Python: import z1, z2 for i in z2.FloatVector(): pass results in: Traceback (most recent call last): File "zs.py", line 2, in <module> for i in z2.FloatVector(): RuntimeError This commit fixes the issue by adding a new exception translator each time the internals pointer is initialized from python builtins: this generally means the internals data was initialized by some other module. (The extra translator(s) are skipped under libstdc++).
1 parent 17c98b9 commit cdae8ab

File tree

4 files changed

+47
-0
lines changed

4 files changed

+47
-0
lines changed

include/pybind11/cast.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,23 @@ PYBIND11_NOINLINE inline internals &get_internals() {
7575
const char *id = PYBIND11_INTERNALS_ID;
7676
if (builtins.contains(id) && isinstance<capsule>(builtins[id])) {
7777
internals_ptr = *static_cast<internals **>(capsule(builtins[id]));
78+
79+
// We loaded builtins through python's builtins, which means that our error_already_set and
80+
// builtin_exception may be different local classes than the ones set up in the initial
81+
// exception translator, below, so add another for our local exception classes.
82+
//
83+
// stdlibc++ doesn't require this (types there are identified only by name)
84+
#if !defined(__GLIBCXX__)
85+
internals_ptr->registered_exception_translators.push_front(
86+
[](std::exception_ptr p) -> void {
87+
try {
88+
if (p) std::rethrow_exception(p);
89+
} catch (error_already_set &e) { e.restore(); return;
90+
} catch (const builtin_exception &e) { e.set_error(); return;
91+
}
92+
}
93+
);
94+
#endif
7895
} else {
7996
internals_ptr = new internals();
8097
#if defined(WITH_THREAD)

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ string(REPLACE ".cpp" ".py" PYBIND11_PYTEST_FILES "${PYBIND11_TEST_FILES}")
7171
# built; if none of these are built (i.e. because TEST_OVERRIDE is used and
7272
# doesn't include them) the second module doesn't get built.
7373
set(PYBIND11_CROSS_MODULE_TESTS
74+
test_exceptions.py
7475
)
7576

7677
# Check if Eigen is available; if not, remove from PYBIND11_TEST_FILES (but

tests/pybind11_cross_module_tests.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,11 @@ PYBIND11_MODULE(pybind11_cross_module_tests, m) {
1717
// Definitions here are tested by importing both this module and the
1818
// relevant pybind11_tests submodule from a test_whatever.py
1919

20+
// test_exceptions.py
21+
m.def("raise_runtime_error", []() { PyErr_SetString(PyExc_RuntimeError, "My runtime error"); throw py::error_already_set(); });
22+
m.def("raise_value_error", []() { PyErr_SetString(PyExc_ValueError, "My value error"); throw py::error_already_set(); });
23+
m.def("throw_pybind_value_error", []() { throw py::value_error("pybind11 value error"); });
24+
m.def("throw_pybind_type_error", []() { throw py::type_error("pybind11 type error"); });
25+
m.def("throw_stop_iteration", []() { throw py::stop_iteration(); });
26+
2027
}

tests/test_exceptions.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from pybind11_tests import exceptions as m
4+
import pybind11_cross_module_tests as cm
45

56

67
def test_std_exception(msg):
@@ -19,6 +20,27 @@ def test_error_already_set(msg):
1920
assert msg(excinfo.value) == "foo"
2021

2122

23+
def test_cross_module_exceptions():
24+
with pytest.raises(RuntimeError) as excinfo:
25+
cm.raise_runtime_error()
26+
assert str(excinfo.value) == "My runtime error"
27+
28+
with pytest.raises(ValueError) as excinfo:
29+
cm.raise_value_error()
30+
assert str(excinfo.value) == "My value error"
31+
32+
with pytest.raises(ValueError) as excinfo:
33+
cm.throw_pybind_value_error()
34+
assert str(excinfo.value) == "pybind11 value error"
35+
36+
with pytest.raises(TypeError) as excinfo:
37+
cm.throw_pybind_type_error()
38+
assert str(excinfo.value) == "pybind11 type error"
39+
40+
with pytest.raises(StopIteration) as excinfo:
41+
cm.throw_stop_iteration()
42+
43+
2244
def test_python_call_in_catch():
2345
d = {}
2446
assert m.python_call_in_destructor(d) is True

0 commit comments

Comments
 (0)