Skip to content
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

Merged
merged 136 commits into from
Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from 104 commits
Commits
Show all changes
136 commits
Select commit Hold shift + click to select a range
87d1f6b
error_already_set::what() is now constructed lazily
superbobry Aug 27, 2019
e289e0e
Do not attempt to normalize if no exception occurred
superbobry Aug 28, 2019
f1435c7
Extract exception name via tp_name
superbobry Aug 28, 2019
60df143
Reverted error_already_set to subclass std::runtime_error
superbobry Aug 28, 2019
c935b73
Revert "Extract exception name via tp_name"
superbobry Aug 28, 2019
b48bc72
Cosmit following @YannickJadoul's comments
superbobry Aug 29, 2019
115a757
Fixed PyPy build
superbobry Aug 29, 2019
db14dd9
Moved normalization to error_already_set ctor
superbobry Aug 29, 2019
9635e88
Merge branch 'master' of https://github.com/pybind/pybind11 into lazy…
Skylion007 Feb 13, 2022
dbb3e6b
Fix merge bugs
Skylion007 Feb 13, 2022
a692362
Fix more merge errors
Skylion007 Feb 13, 2022
ff3185e
Improve formatting
Skylion007 Feb 13, 2022
95eeaef
Improve error message in rare case
Skylion007 Feb 13, 2022
00e4852
Revert back if statements
Skylion007 Feb 13, 2022
648e670
Fix clang-tidy
Skylion007 Feb 13, 2022
244aa52
Try removing mutable
Skylion007 Feb 13, 2022
b09f14d
Does build_mode release fix it
Skylion007 Feb 13, 2022
1d1ec9e
Set to Debug to expose segfault
Skylion007 Feb 13, 2022
6d0354b
Fix remove set error string
Skylion007 Feb 13, 2022
2c5ad0b
Do not run error_string() more than once
Skylion007 Feb 14, 2022
68b349a
Trying setting the tracebackk to the value
Skylion007 Feb 14, 2022
91bf33f
guard if m_type is null
Skylion007 Feb 15, 2022
63dfbb4
Merge branch 'master' of https://github.com/pybind/pybind11 into lazy…
Skylion007 Feb 15, 2022
d231f15
Try to debug PGI
Skylion007 Feb 15, 2022
df6cb30
One last try for PGI
Skylion007 Feb 15, 2022
bdcd95a
Does reverting this fix PyPy
Skylion007 Feb 16, 2022
225dbae
Reviewer suggestions
Skylion007 Feb 20, 2022
f8d3ed2
Remove unnecessary initialization
Skylion007 Feb 20, 2022
b90bd78
Add noexcept move and explicit fail throw
Skylion007 Feb 22, 2022
3286964
Optimize error_string creation
Skylion007 Feb 22, 2022
978bf2a
Fix typo
Skylion007 Feb 22, 2022
6c97e68
Revert noexcept
Skylion007 Feb 22, 2022
6523bc2
Merge branch 'master' of https://github.com/pybind/pybind11 into lazy…
Skylion007 Mar 2, 2022
6c6971f
Fix merge conflict error
Skylion007 Mar 2, 2022
98aff18
t pushMerge branch 'master' of https://github.com/pybind/pybind11 int…
Skylion007 Apr 23, 2022
e71d9b9
Abuse assignment operator
Skylion007 Apr 24, 2022
e9a2e6d
Revert operator abuse
Skylion007 May 3, 2022
a83028c
Merge branch 'master' of https://github.com/pybind/pybind11 into lazy…
Skylion007 May 4, 2022
e0d8d0e
See if we still need debug
Skylion007 May 4, 2022
d32bc91
Remove unnecessary mutable
Skylion007 May 4, 2022
226cdde
Merge branch 'master' into lazy-error-string
May 5, 2022
f4a440a
Report "FATAL failure building pybind11::error_already_set error_stri…
May 5, 2022
8f6ab3f
Try specifying noexcept again
Skylion007 May 6, 2022
4da9968
Try explicit ctor
Skylion007 May 6, 2022
f181dac
default ctor is noexcept too
Skylion007 May 6, 2022
6e781a0
Apply reviewer suggestions, simplify code, and make helper method pri…
Skylion007 May 8, 2022
1f8f0ab
Remove unnecessary include
Skylion007 May 8, 2022
2031225
Merge branch 'master' of https://github.com/pybind/pybind11 into lazy…
Skylion007 May 8, 2022
0aee425
Clang-Tidy fix
Skylion007 May 8, 2022
1c8398f
Merge branch 'master' into lazy-error-string
May 10, 2022
a8f7a97
Merge branch 'lazy-error-string' of https://github.com/superbobry/pyb…
May 10, 2022
0691d5f
detail::obj_class_name(), fprintf with [STDERR], [STDOUT] tags, polis…
May 10, 2022
28de959
consistently check m_lazy_what.empty() also in production builds
May 11, 2022
c7a7146
Make a comment slightly less ambiguous.
May 11, 2022
de84a27
Bug fix: Remove `what();` from `restore()`.
May 11, 2022
498195a
Replace extremely opaque (unhelpful) error message with a truthful re…
May 11, 2022
80b05ba
Fix clang-tidy error [performance-move-constructor-init].
May 11, 2022
acdf332
Make expected error message less specific.
May 11, 2022
58ad49b
Various changes.
May 11, 2022
90b2453
bug fix: error_string(PyObject **, ...)
May 11, 2022
dded024
Putting back the two eager PyErr_NormalizeException() calls.
May 12, 2022
4193375
Change error_already_set() to call pybind11_fail() if the Python erro…
May 12, 2022
b020e04
Remove mutable (fixes oversight in the previous commit).
May 12, 2022
02df6c0
Normalize the exception only locally in error_string(). Python 3.6 & …
May 12, 2022
d28c155
clang-tidy: use auto
May 12, 2022
7578de9
Use `gil_scoped_acquire_local` in `error_already_set` destructor. See…
May 12, 2022
bab6f66
For Python < 3.8: `PyErr_NormalizeException` before `PyErr_WriteUnrai…
May 12, 2022
2ad2285
Go back to replacing the held Python exception with then normalized e…
May 13, 2022
e3ebb0d
Slightly rewording comment. (There were also other failures.)
May 13, 2022
8dff51d
Add 1-line comment for obj_class_name()
May 13, 2022
923725a
Benchmark code, with results in this commit message.
May 13, 2022
53f40a0
Changing call_repetitions_target_elapsed_secs to 0.1 for regular unit…
May 13, 2022
b77e703
Adding in `recursion_depth`
May 14, 2022
493d751
Optimized ctor
Skylion007 May 14, 2022
6d0ec49
Merge branch 'lazy-error-string' of https://github.com/superbobry/pyb…
Skylion007 May 14, 2022
6d9188f
Fix silly bug in recurse_first_then_call()
May 15, 2022
8f3d3a6
Add tests that have equivalent PyErr_Fetch(), PyErr_Restore() but no …
May 15, 2022
724ee3d
Add call_error_string to tests. Sample only recursion_depth 0, 100.
May 15, 2022
467ab00
Show lazy-what speed-up in percent.
May 15, 2022
eaa1a48
Include real_work in benchmarks.
May 15, 2022
dbed20a
Replace all PyErr_SetString() with generate_python_exception_with_tra…
May 16, 2022
f021543
Better organization of test loops.
May 16, 2022
125e2d0
Add test_error_already_set_copy_move
May 16, 2022
b037958
Fix bug in newly added test (discovered by clang-tidy): actually use …
May 16, 2022
2baa3bc
MSVC detects the unreachable return
May 16, 2022
4c479f4
change test_perf_error_already_set.py back to quick mode
May 17, 2022
b4326f9
Inherit from std::exception (instead of std::runtime_error, which doe…
May 17, 2022
aaec34e
Special handling under Windows.
May 17, 2022
99af318
print with leading newline
May 17, 2022
e5f028f
Merge branch 'master' into lazy-error-string
May 17, 2022
8c76360
Removing test_perf_error_already_set (copies are under https://github…
May 18, 2022
c7149d8
Avoid gil and scope overhead if there is nothing to release.
May 18, 2022
47f5445
Restore default move ctor. "member function" instead of "function" (n…
May 18, 2022
7c08e7f
Delete error_already_set copy ctor.
May 18, 2022
f746a3d
Make restore() non-const again to resolve clang-tidy failure (still e…
May 18, 2022
25e4cc9
Bring back error_already_set copy ctor, to see if that resolves the 4…
May 18, 2022
70b870a
Add noexcept to error_already_set copy & move ctors (as suggested by …
May 18, 2022
d425bb8
Trying one-by-one noexcept copy ctor for old compilers.
May 18, 2022
65aaa4c
Add back test covering copy ctor. Add another simple test that exerci…
May 18, 2022
19bb595
Exclude more older compilers from using the noexcept = default ctors.…
May 19, 2022
d07b5ba
Factor out & reuse gil_scoped_acquire_local as gil_scoped_acquire_simple
May 19, 2022
f6bf5aa
Guard gil_scoped_acquire_simple by _Py_IsFinalizing() check.
May 19, 2022
cc5d76a
what() GIL safety
May 19, 2022
2b31e4d
clang-tidy & Python 3.6 fixes
May 19, 2022
73cffe4
Merge branch 'master' into lazy-error-string
May 20, 2022
2c5dd19
Use `gil_scoped_acquire` in dtor, copy ctor, `what()`. Remove `_Py_Is…
May 20, 2022
14be38c
Remove error_scope from copy ctor.
May 21, 2022
db97ce7
Add `error_scope` to `get_internals()`, to cover the situation that `…
May 21, 2022
42d2e1a
Add `FlakyException` tests with failure triggers in `__init__` and `_…
May 23, 2022
6417a76
Tweaks to resolve Py 3.6 and PyPy CI failures.
May 24, 2022
a62b3d7
Normalize Python exception immediately in error_already_set ctor.
May 24, 2022
c54309c
Merge branch 'master' into lazy-error-string
May 24, 2022
c786796
Fix oversights based on CI failures (copy & move ctor initialization).
May 24, 2022
61bb513
Move @pytest.mark.xfail("env.PYPY") after @pytest.mark.parametrize(...)
May 24, 2022
bb3f24c
Use @pytest.mark.skipif (xfail does not work for segfaults, of course).
May 24, 2022
087ef29
Merge branch 'master' into lazy-error-string
May 31, 2022
bee5fb4
Remove unused obj_class_name_or() function (it was added only under t…
Jun 1, 2022
bea0c9f
Remove already obsolete C++ comments and code that were added only un…
Jun 1, 2022
d6d371a
Slightly better (newly added) comments.
Jun 1, 2022
eec0547
Factor out detail::error_fetch_and_normalize. Preparation for produci…
Jun 1, 2022
ec1a2c0
Copy most of error_string() code to new error_fetch_and_normalize::co…
Jun 1, 2022
c39ebd5
Remove all error_string() code from detail/type_caster_base.h. Note t…
Jun 1, 2022
286023b
Return const std::string& instead of const char * and move error_stri…
Jun 1, 2022
156b57b
Remove gil_scope_acquire from error_fetch_and_normalize, add back to …
Jun 1, 2022
6f175ac
Better handling of FlakyException __str__ failure.
Jun 1, 2022
71f33a8
Move error_fetch_and_normalize::complete_lazy_error_string() implemen…
Jun 1, 2022
0739d4c
Add error_fetch_and_normalize::release_py_object_references() and use…
Jun 1, 2022
80dfaea
Use shared_ptr for m_fetched_error => 1. non-racy, copy ctor that doe…
Jun 1, 2022
a7ba693
Add comments.
Jun 1, 2022
9854589
Trivial renaming of a newly introduced member function.
Jun 1, 2022
d2e78b0
Merge branch 'master' into lazy-error-string
Jun 1, 2022
0e50c45
Workaround for PyPy
Jun 1, 2022
4e06fb1
Bug fix (oversight). Only valgrind got this one.
Jun 1, 2022
f892cce
Use shared_ptr custom deleter for m_fetched_error in error_already_se…
Jun 2, 2022
53e29f4
Further small simplification. With the GIL held, simply deleting the …
Jun 2, 2022
8e145c0
IWYU cleanup
Jun 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions include/pybind11/detail/internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -410,11 +410,7 @@ PYBIND11_NOINLINE internals &get_internals() {

// Ensure that the GIL is held since we will need to make Python calls.
// Cannot use py::gil_scoped_acquire here since that constructor calls get_internals.
struct gil_scoped_acquire_local {
gil_scoped_acquire_local() : state(PyGILState_Ensure()) {}
~gil_scoped_acquire_local() { PyGILState_Release(state); }
const PyGILState_STATE state;
} gil;
gil_scoped_acquire_simple gil;

PYBIND11_STR_TYPE id(PYBIND11_INTERNALS_ID);
auto builtins = handle(PyEval_GetBuiltins());
Expand Down
69 changes: 36 additions & 33 deletions include/pybind11/detail/type_caster_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -470,41 +470,37 @@ PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp)
return isinstance(obj, type);
}

PYBIND11_NOINLINE std::string error_string() {
if (!PyErr_Occurred()) {
PyErr_SetString(PyExc_RuntimeError, "Unknown internal error occurred");
return "Unknown internal error occurred";
// NOTE: This function may have the side-effect of normalizing the passed Python exception
// (if it is not normalized already), which will change some or all of the three passed
// pointers.
PYBIND11_NOINLINE std::string
error_string(PyObject *&exc_type, PyObject *&exc_value, PyObject *&exc_trace) {
if (exc_type == nullptr) {
pybind11_fail(
"Internal error: pybind11::detail::error_string() called with exc_type == nullptr");
}

error_scope scope; // Preserve error state

std::string errorString;
if (scope.type) {
errorString += handle(scope.type).attr("__name__").cast<std::string>();
errorString += ": ";
}
if (scope.value) {
errorString += (std::string) str(scope.value);
}
PyErr_NormalizeException(&exc_type, &exc_value, &exc_trace);

PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace);
auto result = handle(exc_type).attr("__name__").cast<std::string>();
result += ": ";

if (scope.trace != nullptr) {
PyException_SetTraceback(scope.value, scope.trace);
if (exc_value) {
result += str(exc_value).cast<std::string>();
}

if (exc_trace) {
#if !defined(PYPY_VERSION)
if (scope.trace) {
auto *trace = (PyTracebackObject *) scope.trace;
auto *tb = reinterpret_cast<PyTracebackObject *>(exc_trace);

/* Get the deepest trace possible */
while (trace->tb_next) {
trace = trace->tb_next;
// Get the deepest trace possible.
while (tb->tb_next) {
tb = tb->tb_next;
}

PyFrameObject *frame = trace->tb_frame;
PyFrameObject *frame = tb->tb_frame;
Py_XINCREF(frame);
errorString += "\n\nAt:\n";
result += "\n\nAt:\n";
while (frame) {
# if PY_VERSION_HEX >= 0x030900B1
PyCodeObject *f_code = PyFrame_GetCode(frame);
Expand All @@ -513,13 +509,13 @@ PYBIND11_NOINLINE std::string error_string() {
Py_INCREF(f_code);
# endif
int lineno = PyFrame_GetLineNumber(frame);
errorString += " ";
errorString += handle(f_code->co_filename).cast<std::string>();
errorString += '(';
errorString += std::to_string(lineno);
errorString += "): ";
errorString += handle(f_code->co_name).cast<std::string>();
errorString += '\n';
result += " ";
result += handle(f_code->co_filename).cast<std::string>();
result += '(';
result += std::to_string(lineno);
result += "): ";
result += handle(f_code->co_name).cast<std::string>();
result += '\n';
Py_DECREF(f_code);
# if PY_VERSION_HEX >= 0x030900B1
auto *b_frame = PyFrame_GetBack(frame);
Expand All @@ -530,10 +526,17 @@ PYBIND11_NOINLINE std::string error_string() {
Py_DECREF(frame);
frame = b_frame;
}
#endif //! defined(PYPY_VERSION)
}
#endif

return errorString;
return result;
}

// NOTE: This function may have the side-effect of normalizing the current Python exception
// (if it is not normalized already).
PYBIND11_NOINLINE std::string error_string() {
error_scope scope; // Fetch error state.
return error_string(scope.type, scope.value, scope.trace);
}

PYBIND11_NOINLINE handle get_object_handle(const void *ptr, const detail::type_info *type) {
Expand Down
27 changes: 21 additions & 6 deletions include/pybind11/pybind11.h
Original file line number Diff line number Diff line change
Expand Up @@ -2612,13 +2612,28 @@ void print(Args &&...args) {
}

error_already_set::~error_already_set() {
if (m_type) {
gil_scoped_acquire gil;
error_scope scope;
m_type.release().dec_ref();
m_value.release().dec_ref();
m_trace.release().dec_ref();
if (!(m_type || m_value || m_trace)) {
return; // Avoid gil and scope overhead if there is nothing to release.
}
#if PY_VERSION_HEX >= 0x03070000
if (PyGILState_Check() == 0 && _Py_IsFinalizing() != 0) {
Copy link
Contributor

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:

  1. Thread 2 is here and finds _Py_IsFinalizing() != 0
  2. Main thread starts the late stage of finalization and causes _Py_IsFinalizing() to become non-zero.
  3. Thread 2 uses 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.

Copy link
Contributor

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.

// Leak m_type, m_value, m_trace rather than crashing the process.
// https://docs.python.org/3/c-api/init.html#c.PyGILState_Ensure
return;
}
#endif
// Not using py::gil_scoped_acquire here since that calls get_internals,
// which is known to trigger failures in the wild (a full explanation is
// currently unknown).
// Note that py::gil_scoped_acquire acquires the GIL first in detail/internals.h
// exactly like we do here now, releases it, then acquires it again in gil.h.
// Using gil_scoped_acquire_simple cuts out the get_internals overhead and
// fixes the failures observed in the wild. See PR #1895 for more background.
detail::gil_scoped_acquire_simple gil;
error_scope scope;
m_type.release().dec_ref();
Skylion007 marked this conversation as resolved.
Show resolved Hide resolved
m_value.release().dec_ref();
m_trace.release().dec_ref();
}

PYBIND11_NAMESPACE_BEGIN(detail)
Expand Down
174 changes: 158 additions & 16 deletions include/pybind11/pytypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#include "detail/common.h"
#include "buffer_info.h"

#include <cstdio>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is stdio still necessary?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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>

Expand Down Expand Up @@ -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)
Expand All @@ -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) \
Copy link
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes:

  • Readability: Make it obvious beyond a doubt that the long version is just a workaround for old compilers.
  • Future-proofing: Do not mask bugs in new compilers. It limits the value of pybind11 as a test suite for compiler teams.

|| (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.
Copy link
Collaborator

Choose a reason for hiding this comment

The 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,
Copy link
Collaborator

Choose a reason for hiding this comment

The 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());
Copy link
Collaborator

Choose a reason for hiding this comment

The 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:
Set the error indicator from the three objects. If the error indicator is already set, it is cleared first. If the objects are NULL, the error indicator is cleared. Do not pass a NULL type and non-NULL value or traceback. The exception type should be a class. Do not pass an invalid exception type or value. (Violating these rules will cause subtle problems later.) This call takes away a reference to each object: you must own a reference to each object before the call and after the call you no longer own these references. (If you don’t understand this, don’t use this function. I warned you.)

}

/// 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)));
}
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
Loading