Skip to content

Commit 5437330

Browse files
committed
Add and document py::error_already_set::discard_as_unraisable()
To deal with exceptions that hit destructors or other noexcept functions. Includes fixes to support Python 2.7 and extends documentation on error handling. @virtuald and @YannickJadoul both contributed to this PR.
1 parent 1732046 commit 5437330

File tree

5 files changed

+157
-3
lines changed

5 files changed

+157
-3
lines changed

docs/advanced/classes.rst

+38
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,44 @@ crucial that instances are deallocated on the C++ side to avoid memory leaks.
559559
py::class_<MyClass, std::unique_ptr<MyClass, py::nodelete>>(m, "MyClass")
560560
.def(py::init<>())
561561
562+
.. _destructors_that_call_python:
563+
564+
Destructors that call Python
565+
============================
566+
567+
If a Python function is invoked from a C++ destructor, an exception may be thrown
568+
of type :class:`error_already_set`. If this error is thrown out of a class destructor,
569+
``std::terminate()`` will be called, terminating the process. Class destructors
570+
must catch all exceptions of type :class:`error_already_set` to discard the Python
571+
exception using :func:`error_already_set::discard_as_unraisable`.
572+
573+
Every Python function should be treated as *possibly throwing*. When a Python generator
574+
stops yielding items, Python will throw a ``StopIteration`` exception, which can pass
575+
though C++ destructors if the generator's stack frame holds the last reference to C++
576+
objects.
577+
578+
For more information, see :ref:`the documentation on exceptions <unraisable_exceptions>`.
579+
580+
.. code-block:: cpp
581+
582+
class MyClass {
583+
public:
584+
~MyClass() {
585+
try {
586+
py::print("Even printing is dangerous in a destructor");
587+
py::exec("raise ValueError('This is an unraisable exception')");
588+
} catch (py::error_already_set &e) {
589+
// error_context should be information about where/why the occurred,
590+
// e.g. use __func__ to get the name of the current function
591+
e.discard_as_unraisable(__func__);
592+
}
593+
}
594+
};
595+
596+
.. note::
597+
598+
pybind11 does not support C++ destructors marked ``noexcept(false)``.
599+
562600
.. _implicit_conversions:
563601

564602
Implicit conversions

docs/advanced/exceptions.rst

+49-3
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,15 @@ exceptions:
5353
| | a Python exception back to Python. |
5454
+--------------------------------------+--------------------------------------+
5555

56-
When a Python function invoked from C++ throws an exception, it is converted
57-
into a C++ exception of type :class:`error_already_set` whose string payload
58-
contains a textual summary.
56+
When a Python function invoked from C++ throws an exception, pybind11 will convert
57+
it into a C++ exception of type :class:`error_already_set` whose string payload
58+
contains a textual summary. If you call the Python C-API directly, and it
59+
returns an error, you should ``throw py::error_already_set();``, which allows
60+
pybind11 to deal with the exception and pass it back to the Python interpreter.
61+
(Another option is to call ``PyErr_Clear`` in the
62+
`Python C-API <https://docs.python.org/3/c-api/exceptions.html#c.PyErr_Clear>`_
63+
to clear the error. The Python error must be thrown or cleared, or Python/pybind11
64+
will be left in an invalid state.)
5965

6066
There is also a special exception :class:`cast_error` that is thrown by
6167
:func:`handle::call` when the input arguments cannot be converted to Python
@@ -142,3 +148,43 @@ section.
142148
Exceptions that you do not plan to handle should simply not be caught, or
143149
may be explicitly (re-)thrown to delegate it to the other,
144150
previously-declared existing exception translators.
151+
152+
.. _unraisable_exceptions:
153+
154+
Handling unraisable exceptions
155+
==============================
156+
157+
If a Python function invoked from a C++ destructor or any function marked
158+
``noexcept(true)`` (collectively, "noexcept functions") throws an exception, there
159+
is no way to propagate the exception, as such functions may not throw at
160+
run-time.
161+
162+
Neither Python nor C++ allow exceptions raised in a noexcept function to propagate. In
163+
Python, an exception raised in a class's ``__del__`` method is logged as an
164+
unraisable error. In Python 3.8+, a system hook is triggered and an auditing
165+
event is logged. In C++, ``std::terminate()`` is called to abort immediately.
166+
167+
Any noexcept function should have a try-catch block that traps
168+
class:`error_already_set` (or any other exception that can occur). Note that pybind11
169+
wrappers around Python exceptions such as :class:`pybind11::value_error` are *not*
170+
Python exceptions; they are C++ exceptions that pybind11 catches and converts to
171+
Python exceptions. Noexcept functions cannot propagate these exceptions either.
172+
You can convert them to Python exceptions and then discard as unraisable.
173+
174+
.. code-block:: cpp
175+
176+
void nonthrowing_func() noexcept(true) {
177+
try {
178+
// ...
179+
} catch (py::error_already_set &eas) {
180+
// Discard the Python error using Python APIs, using the C++ magic
181+
// variable __func__. Python already knows the type and value and of the
182+
// exception object.
183+
eas.discard_as_unraisable(__func__);
184+
} catch (const std::exception &e) {
185+
// Log and discard C++ exceptions.
186+
// (We cannot use discard_as_unraisable, since we have a generic C++
187+
// exception, not an exception that originated from Python.)
188+
third_party::log(e);
189+
}
190+
}

include/pybind11/pytypes.h

+16
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,22 @@ class error_already_set : public std::runtime_error {
337337
/// error variables (but the `.what()` string is still available).
338338
void restore() { PyErr_Restore(m_type.release().ptr(), m_value.release().ptr(), m_trace.release().ptr()); }
339339

340+
/// If it is impossible to raise the currently-held error, such as in destructor, we can write
341+
/// it out using Python's unraisable hook (sys.unraisablehook). The error context should be
342+
/// some object whose repr() helps identify the location of the error. Python already knows the
343+
/// type and value of the error, so there is no need to repeat that. For example, __func__ could
344+
/// be helpful. After this call, the current object no longer stores the error variables,
345+
/// and neither does Python.
346+
void discard_as_unraisable(object err_context) {
347+
restore();
348+
PyErr_WriteUnraisable(err_context.ptr());
349+
}
350+
void discard_as_unraisable(const char *err_context) {
351+
auto obj = reinterpret_steal<object>(PYBIND11_FROM_STRING(err_context));
352+
restore();
353+
PyErr_WriteUnraisable(obj.ptr());
354+
}
355+
340356
// Does nothing; provided for backwards compatibility.
341357
PYBIND11_DEPRECATED("Use of error_already_set.clear() is deprecated")
342358
void clear() {}

tests/test_exceptions.cpp

+24
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,25 @@ struct PythonCallInDestructor {
6565
py::dict d;
6666
};
6767

68+
69+
70+
struct PythonAlreadySetInDestructor {
71+
PythonAlreadySetInDestructor(const py::str &s) : s(s) {}
72+
~PythonAlreadySetInDestructor() {
73+
py::dict foo;
74+
try {
75+
// Assign to a py::object to force read access of nonexistent dict entry
76+
py::object o = foo["bar"];
77+
}
78+
catch (py::error_already_set& ex) {
79+
ex.discard_as_unraisable(s);
80+
}
81+
}
82+
83+
py::str s;
84+
};
85+
86+
6887
TEST_SUBMODULE(exceptions, m) {
6988
m.def("throw_std_exception", []() {
7089
throw std::runtime_error("This exception was intentionally thrown.");
@@ -183,6 +202,11 @@ TEST_SUBMODULE(exceptions, m) {
183202
return false;
184203
});
185204

205+
m.def("python_alreadyset_in_destructor", [](py::str s) {
206+
PythonAlreadySetInDestructor alreadyset_in_destructor(s);
207+
return true;
208+
});
209+
186210
// test_nested_throws
187211
m.def("try_catch", [m](py::object exc_type, py::function f, py::args args) {
188212
try { f(*args); }

tests/test_exceptions.py

+30
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# -*- coding: utf-8 -*-
2+
import sys
3+
24
import pytest
35

46
from pybind11_tests import exceptions as m
@@ -48,6 +50,34 @@ def test_python_call_in_catch():
4850
assert d["good"] is True
4951

5052

53+
def test_python_alreadyset_in_destructor(monkeypatch, capsys):
54+
hooked = False
55+
triggered = [False] # mutable, so Python 2.7 closure can modify it
56+
57+
if hasattr(sys, 'unraisablehook'): # Python 3.8+
58+
hooked = True
59+
default_hook = sys.unraisablehook
60+
61+
def hook(unraisable_hook_args):
62+
exc_type, exc_value, exc_tb, err_msg, obj = unraisable_hook_args
63+
if obj == 'already_set demo':
64+
triggered[0] = True
65+
default_hook(unraisable_hook_args)
66+
return
67+
68+
# Use monkeypatch so pytest can apply and remove the patch as appropriate
69+
monkeypatch.setattr(sys, 'unraisablehook', hook)
70+
71+
assert m.python_alreadyset_in_destructor('already_set demo') is True
72+
if hooked:
73+
assert triggered[0] is True
74+
75+
captured = capsys.readouterr()
76+
captured_err = captured[1] # Python 3.5 doesn't like .err the namedtuple
77+
# Error message is different in Python 2 and 3, check for words that appear in both
78+
assert 'ignored' in captured_err and 'already_set demo' in captured_err
79+
80+
5181
def test_exception_matches():
5282
assert m.exception_matches()
5383
assert m.exception_matches_base()

0 commit comments

Comments
 (0)