-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
error_already_set::what() is now constructed lazily #1895
Changes from 104 commits
87d1f6b
e289e0e
f1435c7
60df143
c935b73
b48bc72
115a757
db14dd9
9635e88
dbb3e6b
a692362
ff3185e
95eeaef
00e4852
648e670
244aa52
b09f14d
1d1ec9e
6d0354b
2c5ad0b
68b349a
91bf33f
63dfbb4
d231f15
df6cb30
bdcd95a
225dbae
f8d3ed2
b90bd78
3286964
978bf2a
6c97e68
6523bc2
6c6971f
98aff18
e71d9b9
e9a2e6d
a83028c
e0d8d0e
d32bc91
226cdde
f4a440a
8f6ab3f
4da9968
f181dac
6e781a0
1f8f0ab
2031225
0aee425
1c8398f
a8f7a97
0691d5f
28de959
c7a7146
de84a27
498195a
80b05ba
acdf332
58ad49b
90b2453
dded024
4193375
b020e04
02df6c0
d28c155
7578de9
bab6f66
2ad2285
e3ebb0d
8dff51d
923725a
53f40a0
b77e703
493d751
6d0ec49
6d9188f
8f3d3a6
724ee3d
467ab00
eaa1a48
dbed20a
f021543
125e2d0
b037958
2baa3bc
4c479f4
b4326f9
aaec34e
99af318
e5f028f
8c76360
c7149d8
47f5445
7c08e7f
f746a3d
25e4cc9
70b870a
d425bb8
65aaa4c
19bb595
d07b5ba
f6bf5aa
cc5d76a
2b31e4d
73cffe4
2c5dd19
14be38c
db97ce7
42d2e1a
6417a76
a62b3d7
c54309c
c786796
61bb513
bb3f24c
087ef29
bee5fb4
bea0c9f
d6d371a
eec0547
ec1a2c0
c39ebd5
286023b
156b57b
6f175ac
71f33a8
0739d4c
80dfaea
a7ba693
9854589
d2e78b0
0e50c45
4e06fb1
f892cce
53e29f4
8e145c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,9 @@ | |
#include "detail/common.h" | ||
#include "buffer_info.h" | ||
|
||
#include <cstdio> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is stdio still necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Short answer: no, thanks for pointing it out! Long answer: commit message of 8e145c0 |
||
#include <exception> | ||
#include <string> | ||
#include <type_traits> | ||
#include <utility> | ||
|
||
|
@@ -361,6 +364,27 @@ T reinterpret_steal(handle h) { | |
|
||
PYBIND11_NAMESPACE_BEGIN(detail) | ||
std::string error_string(); | ||
std::string error_string(PyObject *&, PyObject *&, PyObject *&); | ||
|
||
// Equivalent to obj.__class__.__name__ (or obj.__name__ if obj is a class). | ||
inline const char *obj_class_name(PyObject *obj) { | ||
if (Py_TYPE(obj) == &PyType_Type) { | ||
return reinterpret_cast<PyTypeObject *>(obj)->tp_name; | ||
} | ||
return Py_TYPE(obj)->tp_name; | ||
} | ||
|
||
// For situations in which the more complex gil_scoped_acquire cannot be used. | ||
// Note that gil_scoped_acquire calls get_internals(), which uses gil_scoped_acquire_simple. | ||
class gil_scoped_acquire_simple { | ||
public: | ||
gil_scoped_acquire_simple() : state(PyGILState_Ensure()) {} | ||
~gil_scoped_acquire_simple() { PyGILState_Release(state); } | ||
|
||
private: | ||
const PyGILState_STATE state; | ||
}; | ||
|
||
PYBIND11_NAMESPACE_END(detail) | ||
|
||
#if defined(_MSC_VER) | ||
|
@@ -373,38 +397,156 @@ PYBIND11_NAMESPACE_END(detail) | |
/// thrown to propagate python-side errors back through C++ which can either be caught manually or | ||
/// else falls back to the function dispatcher (which then raises the captured error back to | ||
/// python). | ||
class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { | ||
class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { | ||
public: | ||
/// Constructs a new exception from the current Python error indicator, if any. The current | ||
/// Python error indicator will be cleared. | ||
error_already_set() : std::runtime_error(detail::error_string()) { | ||
/// Fetches the current Python exception (using PyErr_Fetch()), which will clear the | ||
/// current Python error indicator. | ||
error_already_set() { | ||
PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); | ||
if (!m_type) { | ||
pybind11_fail("Internal error: pybind11::detail::error_already_set called while " | ||
"Python error indicator not set."); | ||
} | ||
} | ||
|
||
error_already_set(const error_already_set &) = default; | ||
error_already_set(error_already_set &&) = default; | ||
#if ((defined(__clang__) && __clang_major__ >= 9) || (defined(__GNUC__) && __GNUC__ >= 10) \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this point, why not just explicitly write them out? You already do for else statement. Is there a benefit to defaulting them if we need the written out implementation for the other compilers anyway? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes:
|
||
|| (defined(_MSC_VER) && _MSC_VER >= 1920)) \ | ||
&& !defined(__INTEL_COMPILER) | ||
/// WARNING: The GIL must be held when the copy ctor is used! | ||
error_already_set(const error_already_set &) noexcept = default; | ||
error_already_set(error_already_set &&) noexcept = default; | ||
#else | ||
/// Workaround for old compilers: | ||
/// Copying/moving the members one-by-one to be able to specify noexcept. | ||
error_already_set(const error_already_set &e) noexcept | ||
: std::exception{e}, m_type{e.m_type}, m_value{e.m_value}, m_trace{e.m_trace}, | ||
m_lazy_what{e.m_lazy_what} {}; | ||
error_already_set(error_already_set &&e) noexcept | ||
: std::exception{std::move(e)}, m_type{std::move(e.m_type)}, m_value{std::move(e.m_value)}, | ||
m_trace{std::move(e.m_trace)}, m_lazy_what{std::move(e.m_lazy_what)} {}; | ||
#endif | ||
|
||
// Note that the dtor acquires the GIL, unless the Python interpreter is finalizing (in which | ||
// case the Python exception is leaked, to not crash the process). | ||
inline ~error_already_set() override; | ||
|
||
/// Give the currently-held error back to Python, if any. If there is currently a Python error | ||
/// already set it is cleared first. After this call, the current object no longer stores the | ||
/// error variables (but the `.what()` string is still available). | ||
/// The what() result is built lazily on demand. To build the result, it is necessary to | ||
/// acquire the Python GIL. If that is not possible because the Python interpreter is | ||
/// finalizing, the Python exception is unrecoverable and a static message is returned. Any | ||
/// other errors processing the Python exception lead to process termination. If possible, the | ||
/// original Python exception is written to stderr & stdout before the process is terminated. | ||
/// NOTE: This member function may have the side-effect of normalizing the held Python | ||
/// exception (if it is not normalized already). | ||
const char *what() const noexcept override { | ||
if (m_lazy_what.empty()) { | ||
#if PY_VERSION_HEX >= 0x03070000 | ||
if (PyGILState_Check() == 0 && _Py_IsFinalizing() != 0) { | ||
// At this point there is no way the original Python exception can still be | ||
// reported, therefore it is best to let the shutdown continue. | ||
return "Python exception is UNRECOVERABLE because the Python interpreter is " | ||
"finalizing."; | ||
} | ||
#endif | ||
std::string failure_info; | ||
detail::gil_scoped_acquire_simple gil; | ||
try { | ||
Skylion007 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); | ||
if (m_lazy_what.empty()) { // Negate condition for manual testing. | ||
failure_info = "m_lazy_what.empty()"; | ||
} | ||
// throw std::runtime_error("Uncomment for manual testing."); | ||
#ifdef _MSC_VER | ||
} catch (const std::exception &e) { | ||
failure_info = "std::exception::what(): "; | ||
try { | ||
failure_info += e.what(); | ||
} catch (...) { | ||
failure_info += "UNRECOVERABLE"; | ||
} | ||
#endif | ||
} catch (...) { | ||
#ifdef _MSC_VER | ||
failure_info = "Unknown C++ exception"; | ||
#else | ||
failure_info = "C++ exception"; // std::terminate will report the details. | ||
#endif | ||
} | ||
if (!failure_info.empty()) { | ||
// Terminating the process, to not mask the original error by errors in the error | ||
// handling. Reporting the original error on stderr & stdout. Intentionally using | ||
// the Python C API directly, to maximize reliability. | ||
std::string msg | ||
= "FATAL failure building pybind11::detail::error_already_set what() [" | ||
+ failure_info + "] while processing Python exception: "; | ||
if (m_type.ptr() == nullptr) { | ||
msg += "PYTHON_EXCEPTION_TYPE_IS_NULLPTR"; | ||
} else { | ||
const char *class_name = detail::obj_class_name(m_type.ptr()); | ||
if (class_name == nullptr) { | ||
msg += "PYTHON_EXCEPTION_CLASS_NAME_IS_NULLPTR"; | ||
} else { | ||
msg += class_name; | ||
} | ||
} | ||
msg += ": "; | ||
PyObject *val_str = PyObject_Str(m_value.ptr()); | ||
if (val_str == nullptr) { | ||
msg += "PYTHON_EXCEPTION_VALUE_IS_NULLPTR"; | ||
} else { | ||
Py_ssize_t utf8_str_size = 0; | ||
const char *utf8_str = PyUnicode_AsUTF8AndSize(val_str, &utf8_str_size); | ||
if (utf8_str == nullptr) { | ||
msg += "PYTHON_EXCEPTION_VALUE_AS_UTF8_FAILURE"; | ||
} else { | ||
msg += '"' + std::string(utf8_str, static_cast<std::size_t>(utf8_str_size)) | ||
+ '"'; | ||
} | ||
} | ||
// Intentionally using C calls to maximize reliability | ||
// (and to avoid #include <iostream>). | ||
fprintf(stderr, "\n%s [STDERR]\n", msg.c_str()); | ||
fflush(stderr); | ||
fprintf(stdout, "\n%s [STDOUT]\n", msg.c_str()); | ||
fflush(stdout); | ||
#ifdef _MSC_VER | ||
exit(-1); // Sadly. std::terminate() may pop up an interactive dialog box. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am still not sure about override std::terminate behavior like this... it's not consistent with other possible std::terminate calls and not our job to fix IMO. |
||
#else | ||
std::terminate(); | ||
#endif | ||
} | ||
} | ||
return m_lazy_what.c_str(); | ||
} | ||
|
||
/// Restores the currently-held Python error (which will clear the Python error indicator first | ||
/// if already set). | ||
/// WARNING: The GIL must be held when this member function is called! | ||
/// NOTE: This member function will not necessarily restore the original Python exception, but | ||
/// may restore the normalized exception if what() or discard_as_unraisable() were called | ||
/// prior to restore(). | ||
void restore() { | ||
PyErr_Restore(m_type.release().ptr(), m_value.release().ptr(), m_trace.release().ptr()); | ||
// As long as this type is copyable, there is no point in releasing m_type, m_value, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original idea was to have what() cached so that it still worked after err.restore was called. That's why we called what() in restore. |
||
// m_trace, but simply holding on the the references makes it possible to produce | ||
// what() even after restore(). | ||
PyErr_Restore(m_type.inc_ref().ptr(), m_value.inc_ref().ptr(), m_trace.inc_ref().ptr()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I am wondering if referring to these types after Err_Restore is UB: |
||
} | ||
|
||
/// If it is impossible to raise the currently-held error, such as in a destructor, we can | ||
/// write it out using Python's unraisable hook (`sys.unraisablehook`). The error context | ||
/// should be some object whose `repr()` helps identify the location of the error. Python | ||
/// already knows the type and value of the error, so there is no need to repeat that. After | ||
/// this call, the current object no longer stores the error variables, and neither does | ||
/// Python. | ||
/// already knows the type and value of the error, so there is no need to repeat that. | ||
/// NOTE: This member function may have the side-effect of normalizing the held Python | ||
/// exception (if it is not normalized already). | ||
void discard_as_unraisable(object err_context) { | ||
#if PY_VERSION_HEX < 0x03080000 | ||
Skylion007 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); | ||
#endif | ||
restore(); | ||
PyErr_WriteUnraisable(err_context.ptr()); | ||
} | ||
/// An alternate version of `discard_as_unraisable()`, where a string provides information on | ||
/// the location of the error. For example, `__func__` could be helpful. | ||
/// WARNING: The GIL must be held when this member function is called! | ||
void discard_as_unraisable(const char *err_context) { | ||
discard_as_unraisable(reinterpret_steal<object>(PYBIND11_FROM_STRING(err_context))); | ||
} | ||
|
@@ -425,7 +567,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { | |
const object &trace() const { return m_trace; } | ||
|
||
private: | ||
object m_type, m_value, m_trace; | ||
mutable object m_type, m_value, m_trace; | ||
mutable std::string m_lazy_what; | ||
}; | ||
#if defined(_MSC_VER) | ||
# pragma warning(pop) | ||
|
@@ -461,8 +604,7 @@ inline void raise_from(PyObject *type, const char *message) { | |
|
||
/// Sets the current Python error indicator with the chosen error, performing a 'raise from' | ||
/// from the error contained in error_already_set to indicate that the chosen error was | ||
/// caused by the original error. After this function is called error_already_set will | ||
/// no longer contain an error. | ||
/// caused by the original error. | ||
inline void raise_from(error_already_set &err, PyObject *type, const char *message) { | ||
err.restore(); | ||
raise_from(type, message); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check is still racy. For example, we can have the sequence:
_Py_IsFinalizing() != 0
_Py_IsFinalizing()
to become non-zero.gil_scoped_acquire_simple
and crashes.I don't believe there is any safe way to use Py_IsFinalizing as you are trying to use it here --- the problem is that you need a way to atomically check that
Py_IsFinalizing
is false and acquire the GIL, and furthermore ensure that_Py_IsFinalizing
remains false until the end of your block. The Python C API doesn't provide any way to do that directly at the moment --- I addressed that in python/cpython#28525 but unfortunately I haven't heard back from the Python maintainers regarding that PR in a long time.It is possible to implement a workaround, as is done in TensorStore:
https://github.com/google/tensorstore/blob/fe7aae44a788dbdfc731ea1c00b83f0d51e4f2f4/python/tensorstore/gil_safe.h#L80
It is based on registering an atexit handler so that we can delay finalization while in such a block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: The reason that we need to prevent finalization from starting while we are still in the block is that even after acquiring the GIL, if we call Python APIs, those Python APIs may directly or indirectly release and then re-acquire the GIL. If finalization starts after the GIL is released but before it is re-acquired, then we have the same problem.