From 87d1f6ba08d44680337e08c2f9d7e71153ca5b24 Mon Sep 17 00:00:00 2001 From: Sergei Lebedev Date: Tue, 27 Aug 2019 10:50:47 +0100 Subject: [PATCH 001/121] error_already_set::what() is now constructed lazily Prior to this commit throwing error_already_set was expensive due to the eager construction of the error string (which required traversing the Python stack). See #1853 for more context and an alternative take on the issue. Note that error_already_set no longer inherits from std::runtime_error because the latter has no default constructor. --- include/pybind11/cast.h | 38 +++++++++++++++++++++----------------- include/pybind11/pytypes.h | 22 ++++++++++++++++++---- tests/test_exceptions.cpp | 2 +- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 67397c4ee7..8bcf817617 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -403,38 +403,36 @@ PYBIND11_NOINLINE inline bool isinstance_generic(handle obj, const std::type_inf return isinstance(obj, type); } -PYBIND11_NOINLINE inline std::string error_string() { - if (!PyErr_Occurred()) { +PYBIND11_NOINLINE inline std::string error_string(PyObject* type, PyObject* value, PyObject *trace) { + if (!type && !value && !trace) { PyErr_SetString(PyExc_RuntimeError, "Unknown internal error occurred"); return "Unknown internal error occurred"; } - error_scope scope; // Preserve error state - + // TODO(superbobry): is it safe to assume that exception has been + // normalized by the caller? std::string errorString; - if (scope.type) { - errorString += handle(scope.type).attr("__name__").cast(); + if (type) { + errorString += handle(type).attr("__name__").cast(); errorString += ": "; } - if (scope.value) - errorString += (std::string) str(scope.value); - - PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace); + if (value) + errorString += str(value).cast(); #if PY_MAJOR_VERSION >= 3 - if (scope.trace != nullptr) - PyException_SetTraceback(scope.value, scope.trace); + if (trace) + PyException_SetTraceback(value, trace); #endif #if !defined(PYPY_VERSION) - if (scope.trace) { - PyTracebackObject *trace = (PyTracebackObject *) scope.trace; + if (trace) { + PyTracebackObject *tb = (PyTracebackObject *) trace; /* Get the deepest trace possible */ - while (trace->tb_next) - trace = trace->tb_next; + while (tb->tb_next) + tb = tb->tb_next; - PyFrameObject *frame = trace->tb_frame; + PyFrameObject *frame = tb->tb_frame; errorString += "\n\nAt:\n"; while (frame) { int lineno = PyFrame_GetLineNumber(frame); @@ -450,6 +448,12 @@ PYBIND11_NOINLINE inline std::string error_string() { return errorString; } +PYBIND11_NOINLINE inline std::string error_string() { + error_scope scope; // Preserve error state + PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace); + return error_string(scope.type, scope.value, scope.trace); +} + PYBIND11_NOINLINE inline handle get_object_handle(const void *ptr, const detail::type_info *type ) { auto &instances = get_internals().registered_instances; auto range = instances.equal_range(ptr); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 78a604e1de..f8ca00b33c 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -313,17 +313,18 @@ template T reinterpret_steal(handle h) { return {h, object::stolen_ NAMESPACE_BEGIN(detail) inline std::string error_string(); +inline std::string error_string(PyObject*, PyObject*, PyObject*); NAMESPACE_END(detail) /// Fetch and hold an error which was already set in Python. An instance of this is typically /// 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 error_already_set : public std::runtime_error { +class 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()) { + error_already_set() : std::exception() { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); } @@ -332,10 +333,22 @@ class error_already_set : public std::runtime_error { inline ~error_already_set(); + virtual const char* what() const noexcept { + if (m_lazy_what.empty()) { + PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); + } + return m_lazy_what.c_str(); + } + /// 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). - void restore() { PyErr_Restore(m_type.release().ptr(), m_value.release().ptr(), m_trace.release().ptr()); } + void restore() { + what(); // Force-build `.what()`. + if (m_type || m_value || m_trace) + PyErr_Restore(m_type.release().ptr(), m_value.release().ptr(), m_trace.release().ptr()); + } // Does nothing; provided for backwards compatibility. PYBIND11_DEPRECATED("Use of error_already_set.clear() is deprecated") @@ -351,7 +364,8 @@ class error_already_set : public std::runtime_error { const object& trace() const { return m_trace; } private: - object m_type, m_value, m_trace; + mutable std::string m_lazy_what; + mutable object m_type, m_value, m_trace; }; /** \defgroup python_builtins _ diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index d30139037f..a3fb7b8e9a 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -157,7 +157,7 @@ TEST_SUBMODULE(exceptions, m) { PyErr_SetString(PyExc_ValueError, "foo"); try { throw py::error_already_set(); - } catch (const std::runtime_error& e) { + } catch (const py::error_already_set& e) { if ((err && e.what() != std::string("ValueError: foo")) || (!err && e.what() != std::string("Unknown internal error occurred"))) { From e289e0ea24c27419a67274e8de80b270b6dafd27 Mon Sep 17 00:00:00 2001 From: Sergei Lebedev Date: Wed, 28 Aug 2019 11:30:55 +0100 Subject: [PATCH 002/121] Do not attempt to normalize if no exception occurred This is not supported on PyPy-2.7 5.8.0. --- include/pybind11/pytypes.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index f8ca00b33c..76e915cca7 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -335,7 +335,8 @@ class error_already_set : public std::exception { virtual const char* what() const noexcept { if (m_lazy_what.empty()) { - PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + if (m_type || m_value || m_trace) + PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); } return m_lazy_what.c_str(); From f1435c7e6b068a1ed13ebd3db597ea3bb15aa398 Mon Sep 17 00:00:00 2001 From: Sergei Lebedev Date: Wed, 28 Aug 2019 17:52:41 +0100 Subject: [PATCH 003/121] Extract exception name via tp_name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is faster than dynamically looking up __name__ via GetAttrString. Note though that the runtime of the code throwing an error_already_set will be dominated by stack unwinding so the improvement will not be noticeable. Before: 396 ns ± 0.913 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) After: 277 ns ± 0.549 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) Benchmark: const std::string foo() { PyErr_SetString(PyExc_KeyError, ""); const std::string &s = py::detail::error_string(); PyErr_Clear(); return s; } PYBIND11_MODULE(foo, m) { m.def("foo", &::foo); } --- include/pybind11/cast.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 8bcf817617..c02708195b 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -413,7 +413,7 @@ PYBIND11_NOINLINE inline std::string error_string(PyObject* type, PyObject* valu // normalized by the caller? std::string errorString; if (type) { - errorString += handle(type).attr("__name__").cast(); + errorString += static_cast(reinterpret_cast(type)->tp_name); errorString += ": "; } if (value) From 60df1435dd32694172bda4371704720c9a69aa70 Mon Sep 17 00:00:00 2001 From: Sergei Lebedev Date: Wed, 28 Aug 2019 18:29:35 +0100 Subject: [PATCH 004/121] Reverted error_already_set to subclass std::runtime_error --- include/pybind11/pytypes.h | 18 +++++++++++------- tests/test_exceptions.cpp | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 76e915cca7..87ddf48382 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -320,11 +320,11 @@ 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 error_already_set : public std::exception { +class error_already_set : public std::runtime_error { 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::exception() { + error_already_set() : std::runtime_error("") { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); } @@ -334,12 +334,16 @@ class error_already_set : public std::exception { inline ~error_already_set(); virtual const char* what() const noexcept { - if (m_lazy_what.empty()) { - if (m_type || m_value || m_trace) - PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); - m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); + try { + if (m_lazy_what.empty()) { + if (m_type || m_value || m_trace) + PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); + } + return m_lazy_what.c_str(); + } catch (...) { + return "Unknown internal error occurred"; } - return m_lazy_what.c_str(); } /// Give the currently-held error back to Python, if any. If there is currently a Python error diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index a3fb7b8e9a..d30139037f 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -157,7 +157,7 @@ TEST_SUBMODULE(exceptions, m) { PyErr_SetString(PyExc_ValueError, "foo"); try { throw py::error_already_set(); - } catch (const py::error_already_set& e) { + } catch (const std::runtime_error& e) { if ((err && e.what() != std::string("ValueError: foo")) || (!err && e.what() != std::string("Unknown internal error occurred"))) { From c935b73ba563f1db9aeb9fb06da6ef040d2ce682 Mon Sep 17 00:00:00 2001 From: Sergei Lebedev Date: Wed, 28 Aug 2019 18:53:35 +0100 Subject: [PATCH 005/121] Revert "Extract exception name via tp_name" The implementation of __name__ is slightly more complex than that. It handles the module name prefix, and heap-allocated types. We could port it to pybind11 later on but for now it seems like an overkill. This reverts commit f1435c7e6b068a1ed13ebd3db597ea3bb15aa398. --- include/pybind11/cast.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index c02708195b..8bcf817617 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -413,7 +413,7 @@ PYBIND11_NOINLINE inline std::string error_string(PyObject* type, PyObject* valu // normalized by the caller? std::string errorString; if (type) { - errorString += static_cast(reinterpret_cast(type)->tp_name); + errorString += handle(type).attr("__name__").cast(); errorString += ": "; } if (value) From b48bc720e073588b28eb433e09daf72935f1b475 Mon Sep 17 00:00:00 2001 From: Sergei Lebedev Date: Thu, 29 Aug 2019 14:33:18 +0100 Subject: [PATCH 006/121] Cosmit following @YannickJadoul's comments Note that detail::error_string() no longer calls PyException_SetTraceback as it is unncessary for pretty-printing the exception. --- include/pybind11/cast.h | 34 +++++++++++++--------------------- include/pybind11/pytypes.h | 15 +++++++-------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 8bcf817617..8ff1871e72 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -403,40 +403,31 @@ PYBIND11_NOINLINE inline bool isinstance_generic(handle obj, const std::type_inf return isinstance(obj, type); } -PYBIND11_NOINLINE inline std::string error_string(PyObject* type, PyObject* value, PyObject *trace) { - if (!type && !value && !trace) { +PYBIND11_NOINLINE inline std::string error_string(PyObject *type, PyObject *value, PyObject *trace) { + if (!type) { PyErr_SetString(PyExc_RuntimeError, "Unknown internal error occurred"); return "Unknown internal error occurred"; } - // TODO(superbobry): is it safe to assume that exception has been - // normalized by the caller? - std::string errorString; - if (type) { - errorString += handle(type).attr("__name__").cast(); - errorString += ": "; - } - if (value) - errorString += str(value).cast(); + std::string result = handle(type).attr("__name__").cast(); + result += ": "; -#if PY_MAJOR_VERSION >= 3 - if (trace) - PyException_SetTraceback(value, trace); -#endif + if (value) + result += str(value).cast(); #if !defined(PYPY_VERSION) if (trace) { PyTracebackObject *tb = (PyTracebackObject *) trace; - /* Get the deepest trace possible */ + // Get the deepest trace possible. while (tb->tb_next) tb = tb->tb_next; PyFrameObject *frame = tb->tb_frame; - errorString += "\n\nAt:\n"; + result += "\n\nAt:\n"; while (frame) { int lineno = PyFrame_GetLineNumber(frame); - errorString += + result += " " + handle(frame->f_code->co_filename).cast() + "(" + std::to_string(lineno) + "): " + handle(frame->f_code->co_name).cast() + "\n"; @@ -445,12 +436,13 @@ PYBIND11_NOINLINE inline std::string error_string(PyObject* type, PyObject* valu } #endif - return errorString; + return result; } PYBIND11_NOINLINE inline std::string error_string() { - error_scope scope; // Preserve error state - PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace); + error_scope scope; // Preserve error state. + if (scope.type) + PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace); return error_string(scope.type, scope.value, scope.trace); } diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 87ddf48382..2945d91334 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -333,17 +333,16 @@ class error_already_set : public std::runtime_error { inline ~error_already_set(); - virtual const char* what() const noexcept { - try { - if (m_lazy_what.empty()) { - if (m_type || m_value || m_trace) + virtual const char* what() const noexcept override { + if (m_lazy_what.empty()) { + try { PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); + } catch (...) { + return "Unknown internal error occurred"; } - return m_lazy_what.c_str(); - } catch (...) { - return "Unknown internal error occurred"; } + return m_lazy_what.c_str(); } /// Give the currently-held error back to Python, if any. If there is currently a Python error @@ -351,7 +350,7 @@ class error_already_set : public std::runtime_error { /// error variables (but the `.what()` string is still available). void restore() { what(); // Force-build `.what()`. - if (m_type || m_value || m_trace) + if (m_type) PyErr_Restore(m_type.release().ptr(), m_value.release().ptr(), m_trace.release().ptr()); } From 115a757a5d18753659d495c1ceca4541b8f0e0ca Mon Sep 17 00:00:00 2001 From: Sergei Lebedev Date: Thu, 29 Aug 2019 14:56:36 +0100 Subject: [PATCH 007/121] Fixed PyPy build --- include/pybind11/cast.h | 4 ++-- include/pybind11/pytypes.h | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h index 8ff1871e72..d6af10ed88 100644 --- a/include/pybind11/cast.h +++ b/include/pybind11/cast.h @@ -415,8 +415,8 @@ PYBIND11_NOINLINE inline std::string error_string(PyObject *type, PyObject *valu if (value) result += str(value).cast(); -#if !defined(PYPY_VERSION) if (trace) { +#if !defined(PYPY_VERSION) PyTracebackObject *tb = (PyTracebackObject *) trace; // Get the deepest trace possible. @@ -433,8 +433,8 @@ PYBIND11_NOINLINE inline std::string error_string(PyObject *type, PyObject *valu handle(frame->f_code->co_name).cast() + "\n"; frame = frame->f_back; } - } #endif + } return result; } diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 2945d91334..df0cc70dfe 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -335,8 +335,10 @@ class error_already_set : public std::runtime_error { virtual const char* what() const noexcept override { if (m_lazy_what.empty()) { - try { + if (m_type) PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + + try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); } catch (...) { return "Unknown internal error occurred"; From db14dd902e8141b3dea011f32f1dc08d9287b7b6 Mon Sep 17 00:00:00 2001 From: Sergei Lebedev Date: Thu, 29 Aug 2019 17:56:58 +0100 Subject: [PATCH 008/121] Moved normalization to error_already_set ctor --- include/pybind11/pytypes.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index df0cc70dfe..d8bbd66c7a 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -326,6 +326,8 @@ class error_already_set : public std::runtime_error { /// Python error indicator will be cleared. error_already_set() : std::runtime_error("") { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + if (m_type) + PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); } error_already_set(const error_already_set &) = default; @@ -335,9 +337,6 @@ class error_already_set : public std::runtime_error { virtual const char* what() const noexcept override { if (m_lazy_what.empty()) { - if (m_type) - PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); - try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); } catch (...) { From dbb3e6bf8a681280c1572703a9545112b0346404 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 13 Feb 2022 13:16:35 -0500 Subject: [PATCH 009/121] Fix merge bugs --- include/pybind11/detail/type_caster_base.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 1213b0a061..4a22eb5cc0 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -503,8 +503,9 @@ PYBIND11_NOINLINE std::string error_string(PyObject *type, PyObject *value, PyOb int lineno = PyFrame_GetLineNumber(frame); result += " " + handle(f_code->co_filename).cast() + "(" + std::to_string(lineno) - + "): " + handle(frame->f_code->co_name).cast() + "\n"; + + "): " + handle(f_code->co_name).cast() + "\n"; frame = frame->f_back; + Py_DECREF(f_code); } #endif } From a6923626c04eb2db40409dcac9d7256e5d935e51 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 13 Feb 2022 13:23:40 -0500 Subject: [PATCH 010/121] Fix more merge errors --- include/pybind11/detail/type_caster_base.h | 4 ++-- include/pybind11/pytypes.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 4a22eb5cc0..9e1ed6c9c2 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -501,9 +501,9 @@ PYBIND11_NOINLINE std::string error_string(PyObject *type, PyObject *value, PyOb Py_INCREF(f_code); # endif int lineno = PyFrame_GetLineNumber(frame); - result += " " + handle(f_code->co_filename).cast() + "(" + result += std::string(" ") + handle(f_code->co_filename).cast() + '(' + std::to_string(lineno) - + "): " + handle(f_code->co_name).cast() + "\n"; + + "): " + handle(f_code->co_name).cast() + '\n'; frame = frame->f_back; Py_DECREF(f_code); } diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 0006895767..0773b3678a 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -363,8 +363,8 @@ T reinterpret_steal(handle h) { } PYBIND11_NAMESPACE_BEGIN(detail) -inline std::string error_string(); -inline std::string error_string(PyObject *, PyObject *, PyObject *); +std::string error_string(); +std::string error_string(PyObject *, PyObject *, PyObject *); PYBIND11_NAMESPACE_END(detail) #if defined(_MSC_VER) From ff3185ea7d0c87df6f93cc70c2885ee710eb1fd4 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 13 Feb 2022 13:25:38 -0500 Subject: [PATCH 011/121] Improve formatting --- include/pybind11/detail/type_caster_base.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 9e1ed6c9c2..97e2e18410 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -482,8 +482,8 @@ PYBIND11_NOINLINE std::string error_string(PyObject *type, PyObject *value, PyOb result += str(value).cast(); } - if (trace) { #if !defined(PYPY_VERSION) + if (trace) { auto *tb = (PyTracebackObject *) trace; // Get the deepest trace possible. @@ -507,8 +507,8 @@ PYBIND11_NOINLINE std::string error_string(PyObject *type, PyObject *value, PyOb frame = frame->f_back; Py_DECREF(f_code); } -#endif } +#endif return result; } @@ -520,6 +520,7 @@ PYBIND11_NOINLINE std::string error_string() { } return error_string(scope.type, scope.value, scope.trace); } + PYBIND11_NOINLINE handle get_object_handle(const void *ptr, const detail::type_info *type) { auto &instances = get_internals().registered_instances; auto range = instances.equal_range(ptr); From 95eeaefd1e21e4c180d65b8a2606b994e7296a14 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 13 Feb 2022 13:30:16 -0500 Subject: [PATCH 012/121] Improve error message in rare case --- include/pybind11/pytypes.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 0773b3678a..ab6cc34736 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -398,6 +398,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); } catch (...) { + PyErr_SetString(PyExc_RuntimeError, "Unknown internal error occurred"); return "Unknown internal error occurred"; } } From 00e48523b2ed4a712ebe4c50241b51af638840a1 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 13 Feb 2022 13:36:04 -0500 Subject: [PATCH 013/121] Revert back if statements --- include/pybind11/detail/type_caster_base.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 97e2e18410..b2f5890fa4 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -482,8 +482,8 @@ PYBIND11_NOINLINE std::string error_string(PyObject *type, PyObject *value, PyOb result += str(value).cast(); } -#if !defined(PYPY_VERSION) if (trace) { +#if !defined(PYPY_VERSION) auto *tb = (PyTracebackObject *) trace; // Get the deepest trace possible. @@ -507,8 +507,8 @@ PYBIND11_NOINLINE std::string error_string(PyObject *type, PyObject *value, PyOb frame = frame->f_back; Py_DECREF(f_code); } - } #endif + } return result; } From 648e6703dd9ed76a56f6ef2d71a5c6be816b2edc Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 13 Feb 2022 13:38:11 -0500 Subject: [PATCH 014/121] Fix clang-tidy --- include/pybind11/detail/type_caster_base.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index b2f5890fa4..e3a6ecbd4e 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -475,7 +475,7 @@ PYBIND11_NOINLINE std::string error_string(PyObject *type, PyObject *value, PyOb return "Unknown internal error occurred"; } - std::string result = handle(type).attr("__name__").cast(); + auto result = handle(type).attr("__name__").cast(); result += ": "; if (value) { From 244aa5262be9e0c698a7752964fbbf495f2b71d3 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 13 Feb 2022 14:18:45 -0500 Subject: [PATCH 015/121] Try removing mutable --- include/pybind11/pytypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index ab6cc34736..8678d7003c 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -449,7 +449,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { private: mutable std::string m_lazy_what; - mutable object m_type, m_value, m_trace; + object m_type, m_value, m_trace; }; #if defined(_MSC_VER) # pragma warning(pop) From b09f14d488fb641e9a19080c4136b413c322333b Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 13 Feb 2022 14:46:41 -0500 Subject: [PATCH 016/121] Does build_mode release fix it --- .github/workflows/ci.yml | 3 ++- include/pybind11/pytypes.h | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b38d228353..956f376ddf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -402,7 +402,8 @@ jobs: -DCMAKE_CXX_STANDARD=11 \ -DPYTHON_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") \ -DCMAKE_CXX_FLAGS="-Wc,--pending_instantiations=0" \ - -DPYBIND11_TEST_FILTER="test_smart_ptr.cpp;test_virtual_functions.cpp" + -DPYBIND11_TEST_FILTER="test_smart_ptr.cpp;test_virtual_functions.cpp" \ + -DCMAKE_BUILD_TYPE=Release # Building before installing Pip should produce a warning but not an error - name: Build diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 8678d7003c..ab6cc34736 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -449,7 +449,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { private: mutable std::string m_lazy_what; - object m_type, m_value, m_trace; + mutable object m_type, m_value, m_trace; }; #if defined(_MSC_VER) # pragma warning(pop) From 1d1ec9e0a58b1ee09d8e1e631e62a15d9ebee31d Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 13 Feb 2022 15:28:48 -0500 Subject: [PATCH 017/121] Set to Debug to expose segfault --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 956f376ddf..d67a3584a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -403,7 +403,7 @@ jobs: -DPYTHON_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") \ -DCMAKE_CXX_FLAGS="-Wc,--pending_instantiations=0" \ -DPYBIND11_TEST_FILTER="test_smart_ptr.cpp;test_virtual_functions.cpp" \ - -DCMAKE_BUILD_TYPE=Release + -DCMAKE_BUILD_TYPE=Debug # Building before installing Pip should produce a warning but not an error - name: Build From 6d0354b945c46ff585aa81c935d5f28f272ad570 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 13 Feb 2022 15:34:49 -0500 Subject: [PATCH 018/121] Fix remove set error string --- include/pybind11/pytypes.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index ab6cc34736..4822b2e036 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -381,7 +381,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { 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("") { + error_already_set() : std::runtime_error(""), m_lazy_what() { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); if (m_type) { PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); @@ -398,7 +398,6 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); } catch (...) { - PyErr_SetString(PyExc_RuntimeError, "Unknown internal error occurred"); return "Unknown internal error occurred"; } } From 2c5ad0bf9de467ac2128c12b0e7da3544c7044e2 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Mon, 14 Feb 2022 11:49:26 -0500 Subject: [PATCH 019/121] Do not run error_string() more than once --- .github/workflows/ci.yml | 3 +-- include/pybind11/pytypes.h | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d67a3584a8..6cdeb053f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -402,8 +402,7 @@ jobs: -DCMAKE_CXX_STANDARD=11 \ -DPYTHON_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") \ -DCMAKE_CXX_FLAGS="-Wc,--pending_instantiations=0" \ - -DPYBIND11_TEST_FILTER="test_smart_ptr.cpp;test_virtual_functions.cpp" \ - -DCMAKE_BUILD_TYPE=Debug + -DPYBIND11_TEST_FILTER="test_smart_ptr.cpp;test_virtual_functions.cpp"\ # Building before installing Pip should produce a warning but not an error - name: Build diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 4822b2e036..c92da7dad1 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -398,7 +398,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); } catch (...) { - return "Unknown internal error occurred"; + m_lazy_what = "Unknown internal error occurred"; } } return m_lazy_what.c_str(); From 68b349a234491580a5f1e907e59c516cb1cd637b Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Mon, 14 Feb 2022 12:24:44 -0500 Subject: [PATCH 020/121] Trying setting the tracebackk to the value --- include/pybind11/pytypes.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index c92da7dad1..a4ccb37417 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -385,6 +385,9 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); if (m_type) { PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + if (m_trace) { + PyException_SetTraceback(m_value.ptr(), m_trace.ptr()); + } } } From 91bf33f459eee9877cf722f245f6f20d5d128280 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Tue, 15 Feb 2022 11:30:06 -0500 Subject: [PATCH 021/121] guard if m_type is null --- include/pybind11/pytypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index a4ccb37417..fec563208b 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -397,7 +397,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { inline ~error_already_set() override; const char *what() const noexcept override { - if (m_lazy_what.empty()) { + if (m_lazy_what.empty() && m_type) { try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); } catch (...) { From d231f15996c4cfc578307a61ed9fb9b937b3bc20 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Tue, 15 Feb 2022 12:16:18 -0500 Subject: [PATCH 022/121] Try to debug PGI --- include/pybind11/detail/type_caster_base.h | 5 ++++- include/pybind11/pytypes.h | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index e3a6ecbd4e..dadbe17089 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -479,7 +479,7 @@ PYBIND11_NOINLINE std::string error_string(PyObject *type, PyObject *value, PyOb result += ": "; if (value) { - result += str(value).cast(); + result += (std::string) str(value); } if (trace) { @@ -517,6 +517,9 @@ PYBIND11_NOINLINE std::string error_string() { error_scope scope; // Preserve error state. if (scope.type) { PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace); + /*if (scope.trace != nullptr){ + PyErr_SetTraceback(scope.value, scope.trace); + }*/ } return error_string(scope.type, scope.value, scope.trace); } diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index fec563208b..73a47ec887 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -12,6 +12,7 @@ #include "detail/common.h" #include "buffer_info.h" +#include #include #include @@ -397,9 +398,15 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { inline ~error_already_set() override; const char *what() const noexcept override { - if (m_lazy_what.empty() && m_type) { + if (m_lazy_what.empty()) { try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); + } catch (const std::exception &e) { + m_lazy_what + = std::string( + "Unknown internal error occurred while constructing error_string:") + + e.what(); + return m_lazy_what.c_str(); } catch (...) { m_lazy_what = "Unknown internal error occurred"; } From df6cb301122b8def51d8aee09b5210efec0941ae Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Tue, 15 Feb 2022 13:02:44 -0500 Subject: [PATCH 023/121] One last try for PGI --- include/pybind11/pytypes.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 73a47ec887..f8f0fe33cb 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -409,6 +409,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { return m_lazy_what.c_str(); } catch (...) { m_lazy_what = "Unknown internal error occurred"; + return m_lazy_what.c_str(); } } return m_lazy_what.c_str(); From bdcd95aa7366e084ddfac4031ee7de376f46d7bf Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Wed, 16 Feb 2022 14:46:16 -0500 Subject: [PATCH 024/121] Does reverting this fix PyPy --- include/pybind11/pytypes.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index f8f0fe33cb..eda653c31d 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -13,6 +13,7 @@ #include "buffer_info.h" #include +#include #include #include @@ -382,13 +383,13 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { 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(""), m_lazy_what() { + error_already_set() : std::runtime_error("") { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); if (m_type) { PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); - if (m_trace) { + /*if (m_trace) { PyException_SetTraceback(m_value.ptr(), m_trace.ptr()); - } + }*/ } } @@ -404,7 +405,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } catch (const std::exception &e) { m_lazy_what = std::string( - "Unknown internal error occurred while constructing error_string:") + "Unknown internal error occurred while constructing error_string: ") + e.what(); return m_lazy_what.c_str(); } catch (...) { @@ -412,6 +413,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { return m_lazy_what.c_str(); } } + assert(!m_lazy_what.empty()); return m_lazy_what.c_str(); } @@ -458,7 +460,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { const object &trace() const { return m_trace; } private: - mutable std::string m_lazy_what; + mutable std::string m_lazy_what{}; mutable object m_type, m_value, m_trace; }; #if defined(_MSC_VER) From 225dbae67e8f03d1788339d30f41b9b9e43f329f Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 20 Feb 2022 12:40:27 -0500 Subject: [PATCH 025/121] Reviewer suggestions --- .github/workflows/ci.yml | 3 ++- include/pybind11/detail/type_caster_base.h | 20 +++++++++----------- include/pybind11/pytypes.h | 3 --- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab138c9be0..d6b74dfbeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -402,7 +402,8 @@ jobs: -DCMAKE_CXX_STANDARD=11 \ -DPYTHON_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") \ -DCMAKE_CXX_FLAGS="-Wc,--pending_instantiations=0" \ - -DPYBIND11_TEST_FILTER="test_smart_ptr.cpp;test_virtual_functions.cpp"\ + -DPYBIND11_TEST_FILTER="test_smart_ptr.cpp;test_virtual_functions.cpp" \ + -DCMAKE_BUILD_TYPE=Debug # Building before installing Pip should produce a warning but not an error - name: Build diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index dadbe17089..abd0c67b9e 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -469,22 +469,23 @@ PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp) return isinstance(obj, type); } -PYBIND11_NOINLINE std::string error_string(PyObject *type, PyObject *value, PyObject *trace) { - if (!type) { +PYBIND11_NOINLINE std::string +error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { + if (!exc_type) { PyErr_SetString(PyExc_RuntimeError, "Unknown internal error occurred"); return "Unknown internal error occurred"; } - auto result = handle(type).attr("__name__").cast(); + auto result = handle(exc_type).attr("__name__").cast(); result += ": "; - if (value) { - result += (std::string) str(value); + if (exc_value) { + result += (std::string) str(exc_value); } - if (trace) { + if (exc_trace) { #if !defined(PYPY_VERSION) - auto *tb = (PyTracebackObject *) trace; + auto *tb = (PyTracebackObject *) exc_trace; // Get the deepest trace possible. while (tb->tb_next) { @@ -507,7 +508,7 @@ PYBIND11_NOINLINE std::string error_string(PyObject *type, PyObject *value, PyOb frame = frame->f_back; Py_DECREF(f_code); } -#endif +#endif //! defined(PYPY_VERSION) } return result; @@ -517,9 +518,6 @@ PYBIND11_NOINLINE std::string error_string() { error_scope scope; // Preserve error state. if (scope.type) { PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace); - /*if (scope.trace != nullptr){ - PyErr_SetTraceback(scope.value, scope.trace); - }*/ } return error_string(scope.type, scope.value, scope.trace); } diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index eda653c31d..8abb85cc35 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -387,9 +387,6 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); if (m_type) { PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); - /*if (m_trace) { - PyException_SetTraceback(m_value.ptr(), m_trace.ptr()); - }*/ } } From f8d3ed22bf434b11af581e11107e478f91943a94 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 20 Feb 2022 15:01:57 -0500 Subject: [PATCH 026/121] Remove unnecessary initialization --- include/pybind11/pytypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 8abb85cc35..3fdaa46ded 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -457,7 +457,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { const object &trace() const { return m_trace; } private: - mutable std::string m_lazy_what{}; + mutable std::string m_lazy_what; mutable object m_type, m_value, m_trace; }; #if defined(_MSC_VER) From b90bd783f04d0956c287781a561dcc571e61738e Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Tue, 22 Feb 2022 11:10:10 -0500 Subject: [PATCH 027/121] Add noexcept move and explicit fail throw --- include/pybind11/pytypes.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 3fdaa46ded..b4798cc752 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -391,7 +391,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } error_already_set(const error_already_set &) = default; - error_already_set(error_already_set &&) = default; + error_already_set(error_already_set &&) noexcept = default; inline ~error_already_set() override; @@ -419,6 +419,9 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { /// error variables (but the `.what()` string is still available). void restore() { what(); // Force-build `.what()`. + if (m_lazy_what.empty()) { + pybind11_fail("Critical error building lazy error_string()."); + } if (m_type) { PyErr_Restore( m_type.release().ptr(), m_value.release().ptr(), m_trace.release().ptr()); From 3286964fea140340e75c7e25cd394f372902d517 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Tue, 22 Feb 2022 11:23:42 -0500 Subject: [PATCH 028/121] Optimize error_string creation --- include/pybind11/detail/type_caster_base.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index abd0c67b9e..1ed23fb910 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -502,9 +502,13 @@ error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { Py_INCREF(f_code); # endif int lineno = PyFrame_GetLineNumber(frame); - result += std::string(" ") + handle(f_code->co_filename).cast() + '(' - + std::to_string(lineno) - + "): " + handle(f_code->co_name).cast() + '\n'; + result += " "; + result += handle(f_code->co_filename).cast(); + result += '('; + result += std::to_string(lineno); + result += "):) "; + result += handle(f_code->co_name).cast(); + result += '\n'; frame = frame->f_back; Py_DECREF(f_code); } From 978bf2aa50b0a2f93260aec190a0873238ddbe6d Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Tue, 22 Feb 2022 11:24:40 -0500 Subject: [PATCH 029/121] Fix typo --- include/pybind11/detail/type_caster_base.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 1ed23fb910..f0d27999f1 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -506,7 +506,7 @@ error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { result += handle(f_code->co_filename).cast(); result += '('; result += std::to_string(lineno); - result += "):) "; + result += "): "; result += handle(f_code->co_name).cast(); result += '\n'; frame = frame->f_back; From 6c97e6827ededf5380323b76e8bfc8c650c9ccf2 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Tue, 22 Feb 2022 11:32:30 -0500 Subject: [PATCH 030/121] Revert noexcept --- include/pybind11/pytypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index b4798cc752..75c03d1285 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -391,7 +391,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } error_already_set(const error_already_set &) = default; - error_already_set(error_already_set &&) noexcept = default; + error_already_set(error_already_set &&) = default; inline ~error_already_set() override; From 6c6971fc300b31960644bc4d83d549bd69f4d1b8 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Wed, 2 Mar 2022 13:22:44 -0500 Subject: [PATCH 031/121] Fix merge conflict error --- include/pybind11/detail/type_caster_base.h | 1 - 1 file changed, 1 deletion(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index a7d1751ba1..bfdcf49e19 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -510,7 +510,6 @@ error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { result += "): "; result += handle(f_code->co_name).cast(); result += '\n'; - frame = frame->f_back; Py_DECREF(f_code); # if PY_VERSION_HEX >= 0x030900B1 auto *b_frame = PyFrame_GetBack(frame); From e71d9b9586eb44f17de1a103123e34cd39a32f57 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 24 Apr 2022 11:29:12 -0400 Subject: [PATCH 032/121] Abuse assignment operator --- include/pybind11/pytypes.h | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 9584443d18..8239533e9f 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -13,6 +13,7 @@ #include "buffer_info.h" #include +#include #include #include #include @@ -387,13 +388,16 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } } - error_already_set(const error_already_set &) = default; - error_already_set(error_already_set &&) = default; + error_already_set(const error_already_set &) noexcept = default; + error_already_set(error_already_set &&) noexcept = default; inline ~error_already_set() override; const char *what() const noexcept override { - if (m_lazy_what.empty()) { + auto *sup_what = std::runtime_error::what(); + if (sup_what[0] != '\0') { + return sup_what; + } else if (m_lazy_what.empty()) { try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); } catch (const std::exception &e) { @@ -407,6 +411,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { return m_lazy_what.c_str(); } } + assert(!m_lazy_what.empty()); return m_lazy_what.c_str(); } @@ -415,7 +420,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_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). void restore() { - what(); // Force-build `.what()`. + // Abuse assignment operator to cache what() + std::runtime_error::operator=(std::runtime_error(what())); // Force-build `.what()`. if (m_lazy_what.empty()) { pybind11_fail("Critical error building lazy error_string()."); } From e9a2e6d013dbf8f8fc139be35f0f4e629abbde17 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Tue, 3 May 2022 10:40:48 -0400 Subject: [PATCH 033/121] Revert operator abuse --- include/pybind11/pytypes.h | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 8239533e9f..9584443d18 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -13,7 +13,6 @@ #include "buffer_info.h" #include -#include #include #include #include @@ -388,16 +387,13 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } } - error_already_set(const error_already_set &) noexcept = default; - error_already_set(error_already_set &&) noexcept = default; + error_already_set(const error_already_set &) = default; + error_already_set(error_already_set &&) = default; inline ~error_already_set() override; const char *what() const noexcept override { - auto *sup_what = std::runtime_error::what(); - if (sup_what[0] != '\0') { - return sup_what; - } else if (m_lazy_what.empty()) { + if (m_lazy_what.empty()) { try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); } catch (const std::exception &e) { @@ -411,7 +407,6 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { return m_lazy_what.c_str(); } } - assert(!m_lazy_what.empty()); return m_lazy_what.c_str(); } @@ -420,8 +415,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_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). void restore() { - // Abuse assignment operator to cache what() - std::runtime_error::operator=(std::runtime_error(what())); // Force-build `.what()`. + what(); // Force-build `.what()`. if (m_lazy_what.empty()) { pybind11_fail("Critical error building lazy error_string()."); } From e0d8d0ec8b128745c1f61716a68b884dee62b750 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Wed, 4 May 2022 13:05:02 -0400 Subject: [PATCH 034/121] See if we still need debug --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77799f0d88..2f295614e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -407,8 +407,7 @@ jobs: -DCMAKE_CXX_STANDARD=11 \ -DPYTHON_EXECUTABLE=$(python3 -c "import sys; print(sys.executable)") \ -DCMAKE_CXX_FLAGS="-Wc,--pending_instantiations=0" \ - -DPYBIND11_TEST_FILTER="test_smart_ptr.cpp;test_virtual_functions.cpp" \ - -DCMAKE_BUILD_TYPE=Debug + -DPYBIND11_TEST_FILTER="test_smart_ptr.cpp;test_virtual_functions.cpp" # Building before installing Pip should produce a warning but not an error - name: Build From d32bc917df8504db57e22185f4043cfbe41f7387 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Wed, 4 May 2022 13:48:17 -0400 Subject: [PATCH 035/121] Remove unnecessary mutable --- include/pybind11/pytypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 9584443d18..189de296bf 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -458,7 +458,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { private: mutable std::string m_lazy_what; - mutable object m_type, m_value, m_trace; + object m_type, m_value, m_trace; }; #if defined(_MSC_VER) # pragma warning(pop) From f4a440a8f96122e4e0ee6f5391a7b73a93e1f3e2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 5 May 2022 14:20:30 -0700 Subject: [PATCH 036/121] Report "FATAL failure building pybind11::error_already_set error_string" and terminate process. --- include/pybind11/pytypes.h | 49 +++++++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 189de296bf..de9fa46276 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -13,6 +13,7 @@ #include "buffer_info.h" #include +#include #include #include #include @@ -366,6 +367,13 @@ std::string error_string(); std::string error_string(PyObject *, PyObject *, PyObject *); PYBIND11_NAMESPACE_END(detail) +inline const char *class_name(PyObject *py) { + if (Py_TYPE(py) == &PyType_Type) { + return reinterpret_cast(py)->tp_name; + } + return Py_TYPE(py)->tp_name; +} + #if defined(_MSC_VER) # pragma warning(push) # pragma warning(disable : 4275 4251) @@ -396,15 +404,40 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { if (m_lazy_what.empty()) { try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); - } catch (const std::exception &e) { - m_lazy_what - = std::string( - "Unknown internal error occurred while constructing error_string: ") - + e.what(); - return m_lazy_what.c_str(); + // Uncomment to test the catch (...) block: + // throw std::runtime_error("Something went wrong."); } catch (...) { - m_lazy_what = "Unknown internal error occurred"; - return m_lazy_what.c_str(); + // Terminating the process, to not mask the original error by errors in the error + // handling. Reporting the original error on stderr. Intentionally using the Python + // C API directly, to maximize reliability. + std::string msg + = "FATAL failure building pybind11::error_already_set error_string: "; + if (m_type.ptr() == nullptr) { + msg += "PYTHON_EXCEPTION_TYPE_IS_NULLPTR"; + } else { + const char *tp_name = class_name(m_type.ptr()); + if (tp_name == nullptr) { + msg += "PYTHON_EXCEPTION_TP_NAME_IS_NULLPTR"; + } else { + msg += tp_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(utf8_str_size)) + + '"'; + } + } + std::cerr << msg << std::endl; + std::terminate(); } } assert(!m_lazy_what.empty()); From 8f6ab3f16ff88506d61bfafe3159602193bf238f Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Fri, 6 May 2022 13:45:17 -0400 Subject: [PATCH 037/121] Try specifying noexcept again --- include/pybind11/pytypes.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index de9fa46276..afd2acbbdc 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -395,8 +395,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } } - error_already_set(const error_already_set &) = default; - error_already_set(error_already_set &&) = default; + error_already_set(const error_already_set &) noexcept = default; + error_already_set(error_already_set &&) noexcept = default; inline ~error_already_set() override; From 4da996878ed097f94f177d96e0b1333be472d2f2 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Fri, 6 May 2022 14:28:35 -0400 Subject: [PATCH 038/121] Try explicit ctor --- include/pybind11/pytypes.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index afd2acbbdc..7f66710c1f 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -395,8 +395,13 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } } - error_already_set(const error_already_set &) noexcept = default; - error_already_set(error_already_set &&) noexcept = default; + error_already_set(const error_already_set &e) noexcept + : std::runtime_error(e), + m_lazy_what{e.m_lazy_what}, m_type{e.m_type}, m_value{e.m_value}, m_trace{e.m_trace} {}; + error_already_set(error_already_set &&e) noexcept + : std::runtime_error(e), m_lazy_what{std::move(e.m_lazy_what)}, + m_type{std::move(e.m_type)}, m_value{std::move(e.m_value)}, m_trace{ + std::move(e.m_trace)} {}; inline ~error_already_set() override; From f181dace8eb45ba0525b4bc76dc2dd0f782b2c5c Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Fri, 6 May 2022 14:39:59 -0400 Subject: [PATCH 039/121] default ctor is noexcept too --- include/pybind11/pytypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 7f66710c1f..cef1e31714 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -388,7 +388,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { 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("") { + error_already_set() noexcept : std::runtime_error("") { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); if (m_type) { PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); From 6e781a0383f800d4a08c6d3013e2a99c52cb37a4 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 8 May 2022 10:42:46 -0400 Subject: [PATCH 040/121] Apply reviewer suggestions, simplify code, and make helper method private --- include/pybind11/pytypes.h | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index cef1e31714..1e996c8345 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -12,6 +12,7 @@ #include "detail/common.h" #include "buffer_info.h" +#include #include #include #include @@ -367,13 +368,6 @@ std::string error_string(); std::string error_string(PyObject *, PyObject *, PyObject *); PYBIND11_NAMESPACE_END(detail) -inline const char *class_name(PyObject *py) { - if (Py_TYPE(py) == &PyType_Type) { - return reinterpret_cast(py)->tp_name; - } - return Py_TYPE(py)->tp_name; -} - #if defined(_MSC_VER) # pragma warning(push) # pragma warning(disable : 4275 4251) @@ -388,20 +382,17 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { public: /// Constructs a new exception from the current Python error indicator, if any. The current /// Python error indicator will be cleared. - error_already_set() noexcept : std::runtime_error("") { + error_already_set() : std::runtime_error("") { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); if (m_type) { PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); } } - error_already_set(const error_already_set &e) noexcept - : std::runtime_error(e), - m_lazy_what{e.m_lazy_what}, m_type{e.m_type}, m_value{e.m_value}, m_trace{e.m_trace} {}; + error_already_set(const error_already_set &e) = default; error_already_set(error_already_set &&e) noexcept - : std::runtime_error(e), m_lazy_what{std::move(e.m_lazy_what)}, - m_type{std::move(e.m_type)}, m_value{std::move(e.m_value)}, m_trace{ - std::move(e.m_trace)} {}; + : std::runtime_error(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)} {}; inline ~error_already_set() override; @@ -420,7 +411,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { if (m_type.ptr() == nullptr) { msg += "PYTHON_EXCEPTION_TYPE_IS_NULLPTR"; } else { - const char *tp_name = class_name(m_type.ptr()); + const char *tp_name = get_class_name(m_type.ptr()); if (tp_name == nullptr) { msg += "PYTHON_EXCEPTION_TP_NAME_IS_NULLPTR"; } else { @@ -441,7 +432,11 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { + '"'; } } - std::cerr << msg << std::endl; + // Use C calls to reduce include bloat + fprintf(stderr, "%s\n", msg.c_str()); + fflush(stderr); + fprintf(stdout, "%s\n", msg.c_str()); + fflush(stdout); std::terminate(); } } @@ -495,8 +490,15 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { const object &trace() const { return m_trace; } private: - mutable std::string m_lazy_what; object m_type, m_value, m_trace; + mutable std::string m_lazy_what; + + static const char *get_class_name(PyObject *py) { + if (Py_TYPE(py) == &PyType_Type) { + return reinterpret_cast(py)->tp_name; + } + return Py_TYPE(py)->tp_name; + } }; #if defined(_MSC_VER) # pragma warning(pop) From 1f8f0ab4c54149c32e85676d0e3292c63e7b3fb7 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 8 May 2022 10:44:45 -0400 Subject: [PATCH 041/121] Remove unnecessary include --- include/pybind11/pytypes.h | 1 - 1 file changed, 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 1e996c8345..01ab00558b 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -14,7 +14,6 @@ #include #include -#include #include #include #include From 0aee425b3ba88e17c84ac0e5fb3759abbbbc4905 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sun, 8 May 2022 10:46:23 -0400 Subject: [PATCH 042/121] Clang-Tidy fix --- include/pybind11/pytypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 33333c7cb8..63ed634f56 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -388,7 +388,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } } - error_already_set(const error_already_set &e) = default; + error_already_set(const error_already_set &) = default; error_already_set(error_already_set &&e) noexcept : std::runtime_error(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)} {}; From 0691d5f58d2eb0d4f2c2c78fe94b67e8df392e1b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 10 May 2022 16:38:27 -0700 Subject: [PATCH 043/121] detail::obj_class_name(), fprintf with [STDERR], [STDOUT] tags, polish comments --- include/pybind11/pytypes.h | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 63ed634f56..5ebbf28c97 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -365,6 +365,14 @@ T reinterpret_steal(handle h) { PYBIND11_NAMESPACE_BEGIN(detail) std::string error_string(); std::string error_string(PyObject *, PyObject *, PyObject *); + +inline const char *obj_class_name(PyObject *obj) { + if (Py_TYPE(obj) == &PyType_Type) { + return reinterpret_cast(obj)->tp_name; + } + return Py_TYPE(obj)->tp_name; +} + PYBIND11_NAMESPACE_END(detail) #if defined(_MSC_VER) @@ -403,14 +411,14 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { // throw std::runtime_error("Something went wrong."); } catch (...) { // Terminating the process, to not mask the original error by errors in the error - // handling. Reporting the original error on stderr. Intentionally using the Python - // C API directly, to maximize reliability. + // 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::error_already_set error_string: "; if (m_type.ptr() == nullptr) { msg += "PYTHON_EXCEPTION_TYPE_IS_NULLPTR"; } else { - const char *tp_name = get_class_name(m_type.ptr()); + const char *tp_name = detail::obj_class_name(m_type.ptr()); if (tp_name == nullptr) { msg += "PYTHON_EXCEPTION_TP_NAME_IS_NULLPTR"; } else { @@ -431,10 +439,11 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { + '"'; } } - // Use C calls to reduce include bloat - fprintf(stderr, "%s\n", msg.c_str()); + // Intentionally using C calls to maximize reliability (and to avoid #include + // ). + fprintf(stderr, "%s [STDERR]\n", msg.c_str()); fflush(stderr); - fprintf(stdout, "%s\n", msg.c_str()); + fprintf(stdout, "%s [STDOUT]\n", msg.c_str()); fflush(stdout); std::terminate(); } @@ -491,13 +500,6 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { private: object m_type, m_value, m_trace; mutable std::string m_lazy_what; - - static const char *get_class_name(PyObject *py) { - if (Py_TYPE(py) == &PyType_Type) { - return reinterpret_cast(py)->tp_name; - } - return Py_TYPE(py)->tp_name; - } }; #if defined(_MSC_VER) # pragma warning(pop) From 28de959fa4c1a48ba8fa76951a4354561da816d3 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 10 May 2022 17:09:32 -0700 Subject: [PATCH 044/121] consistently check m_lazy_what.empty() also in production builds --- include/pybind11/pytypes.h | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 5ebbf28c97..b27619edba 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -407,8 +407,11 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { if (m_lazy_what.empty()) { try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); - // Uncomment to test the catch (...) block: - // throw std::runtime_error("Something went wrong."); + // Negate the if condition to test the catch(...) block below. + if (m_lazy_what.empty()) { + throw std::runtime_error( + "FATAL failure building pybind11::error_already_set error_string."); + } } catch (...) { // Terminating the process, to not mask the original error by errors in the error // handling. Reporting the original error on stderr & stdout. Intentionally using @@ -448,7 +451,6 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { std::terminate(); } } - assert(!m_lazy_what.empty()); return m_lazy_what.c_str(); } @@ -456,10 +458,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_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). void restore() { - what(); // Force-build `.what()`. - if (m_lazy_what.empty()) { - pybind11_fail("Critical error building lazy error_string()."); - } + what(); // Force-build m_lazy_what. if (m_type) { PyErr_Restore( m_type.release().ptr(), m_value.release().ptr(), m_trace.release().ptr()); From c7a71468751e314bdc10304796d085e0596922c2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 10 May 2022 17:47:48 -0700 Subject: [PATCH 045/121] Make a comment slightly less ambiguous. --- include/pybind11/pytypes.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index b27619edba..30f4c9867e 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -454,8 +454,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { return m_lazy_what.c_str(); } - /// 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 + /// Restores the currently-held Python error, if any (which will clear the Python error + /// indicator first if already set). After this call, the current object no longer stores the /// error variables (but the `.what()` string is still available). void restore() { what(); // Force-build m_lazy_what. From de84a27fd4b6d56c3af9acfb905a910de127a27e Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 10 May 2022 17:59:27 -0700 Subject: [PATCH 046/121] Bug fix: Remove `what();` from `restore()`. It sure would need to be guarded by `if (m_type)`, otherwise `what()` fails and masks that no error was set (see update unit test). But since `error_already_set` is copyable, there is no point in releasing m_type, m_value, m_trace, therefore we can just as well avoid the runtime overhead of force-building `m_lazy_what`, it may never be used. --- include/pybind11/pytypes.h | 8 ++++---- tests/test_exceptions.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 30f4c9867e..0330b26b5d 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -455,13 +455,13 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } /// Restores the currently-held Python error, if any (which will clear the Python error - /// indicator first if already set). After this call, the current object no longer stores the - /// error variables (but the `.what()` string is still available). + /// indicator first if already set). void restore() { - what(); // Force-build m_lazy_what. if (m_type) { + // As long as this type is copyable, there is no point in releasing m_type, m_value, + // m_trace. PyErr_Restore( - m_type.release().ptr(), m_value.release().ptr(), m_trace.release().ptr()); + m_type.inc_ref().ptr(), m_value.inc_ref().ptr(), m_trace.inc_ref().ptr()); } } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 0be61804a6..74cbf48be2 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -14,9 +14,9 @@ def test_std_exception(msg): def test_error_already_set(msg): - with pytest.raises(RuntimeError) as excinfo: + with pytest.raises(SystemError) as excinfo: m.throw_already_set(False) - assert msg(excinfo.value) == "Unknown internal error occurred" + assert "without setting an error" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: m.throw_already_set(True) From 498195ac4b68b9b705a8049a282b3d8d42dd021c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 10 May 2022 18:22:50 -0700 Subject: [PATCH 047/121] Replace extremely opaque (unhelpful) error message with a truthful reflection of what we know. --- include/pybind11/detail/type_caster_base.h | 6 ++++-- tests/test_exceptions.cpp | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 93c8238c59..cd54de285a 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -473,8 +473,10 @@ PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp) PYBIND11_NOINLINE std::string error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { if (!exc_type) { - PyErr_SetString(PyExc_RuntimeError, "Unknown internal error occurred"); - return "Unknown internal error occurred"; + static const char *msg + = "Internal error: error_string() called without a Python error available."; + PyErr_SetString(PyExc_RuntimeError, msg); + return msg; } auto result = handle(exc_type).attr("__name__").cast(); diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 3e9a3d771e..99eaed36a9 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -228,7 +228,10 @@ TEST_SUBMODULE(exceptions, m) { throw py::error_already_set(); } catch (const std::runtime_error &e) { if ((err && e.what() != std::string("ValueError: foo")) - || (!err && e.what() != std::string("Unknown internal error occurred"))) { + || (!err + && e.what() + != std::string("Internal error: error_string() called without a Python " + "error available."))) { PyErr_Clear(); throw std::runtime_error("error message mismatch"); } From 80b05ba663c608f8c2053991ceb30303e12d754a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 10 May 2022 18:39:53 -0700 Subject: [PATCH 048/121] Fix clang-tidy error [performance-move-constructor-init]. --- include/pybind11/pytypes.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 0330b26b5d..750d912ee7 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -398,8 +398,9 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { error_already_set(const error_already_set &) = default; error_already_set(error_already_set &&e) noexcept - : std::runtime_error(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)} {}; + : std::runtime_error(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)} {}; inline ~error_already_set() override; From acdf332cda44c9b394405464c7533c9243fa08b2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 10 May 2022 18:42:39 -0700 Subject: [PATCH 049/121] Make expected error message less specific. --- tests/test_exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 74cbf48be2..21520a8c12 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -16,7 +16,7 @@ def test_std_exception(msg): def test_error_already_set(msg): with pytest.raises(SystemError) as excinfo: m.throw_already_set(False) - assert "without setting an error" in str(excinfo.value) + assert "without setting" in str(excinfo.value) with pytest.raises(ValueError) as excinfo: m.throw_already_set(True) From 58ad49b05ee550bcdfa3742ca0b3488c69015855 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 11 May 2022 02:35:01 -0700 Subject: [PATCH 050/121] Various changes. --- include/pybind11/detail/type_caster_base.h | 13 +++---- include/pybind11/pybind11.h | 4 +-- include/pybind11/pytypes.h | 41 +++++++++++----------- tests/test_exceptions.cpp | 4 +-- tests/test_exceptions.py | 8 +++-- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index cd54de285a..30c96fb5ed 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -473,12 +473,12 @@ PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp) PYBIND11_NOINLINE std::string error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { if (!exc_type) { - static const char *msg - = "Internal error: error_string() called without a Python error available."; - PyErr_SetString(PyExc_RuntimeError, msg); - return msg; + pybind11_fail( + "Internal error: pybind11::detail::error_string() called with exc_type == nullptr"); } + PyErr_NormalizeException(&exc_type, &exc_value, &exc_trace); + auto result = handle(exc_type).attr("__name__").cast(); result += ": "; @@ -530,10 +530,7 @@ error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { } PYBIND11_NOINLINE std::string error_string() { - error_scope scope; // Preserve error state. - if (scope.type) { - PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace); - } + error_scope scope; // Fetch error state. return error_string(scope.type, scope.value, scope.trace); } diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index cd86152e57..8493e06d3a 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2611,8 +2611,8 @@ void print(Args &&...args) { } error_already_set::~error_already_set() { - if (m_type) { - gil_scoped_acquire gil; + gil_scoped_acquire gil; + { error_scope scope; m_type.release().dec_ref(); m_value.release().dec_ref(); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 750d912ee7..b5fb247192 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -387,13 +387,15 @@ PYBIND11_NAMESPACE_END(detail) /// python). class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { public: - /// Constructs a new exception from the current Python error indicator, if any. The current - /// Python error indicator will be cleared. + /// Constructs a new exception from the current Python error indicator, or substitutes a + /// RuntimeError("Internal error: ..."). The current Python error indicator will be cleared. error_already_set() : std::runtime_error("") { - PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); - if (m_type) { - PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + if (!PyErr_Occurred()) { + m_lazy_what = "Internal error: pybind11::detail::error_already_set called while " + "Python error indicator not set."; + PyErr_SetString(PyExc_RuntimeError, m_lazy_what.c_str()); } + PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); } error_already_set(const error_already_set &) = default; @@ -411,22 +413,22 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { // Negate the if condition to test the catch(...) block below. if (m_lazy_what.empty()) { throw std::runtime_error( - "FATAL failure building pybind11::error_already_set error_string."); + "FATAL failure building pybind11::detail::error_already_set what()"); } } catch (...) { // 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::error_already_set error_string: "; + = "FATAL failure building pybind11::detail::error_already_set what(): "; if (m_type.ptr() == nullptr) { msg += "PYTHON_EXCEPTION_TYPE_IS_NULLPTR"; } else { - const char *tp_name = detail::obj_class_name(m_type.ptr()); - if (tp_name == nullptr) { - msg += "PYTHON_EXCEPTION_TP_NAME_IS_NULLPTR"; + const char *class_name = detail::obj_class_name(m_type.ptr()); + if (class_name == nullptr) { + msg += "PYTHON_EXCEPTION_CLASS_NAME_IS_NULLPTR"; } else { - msg += tp_name; + msg += class_name; } } msg += ": "; @@ -443,8 +445,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { + '"'; } } - // Intentionally using C calls to maximize reliability (and to avoid #include - // ). + // Intentionally using C calls to maximize reliability + // (and to avoid #include ). fprintf(stderr, "%s [STDERR]\n", msg.c_str()); fflush(stderr); fprintf(stdout, "%s [STDOUT]\n", msg.c_str()); @@ -455,15 +457,12 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { return m_lazy_what.c_str(); } - /// Restores the currently-held Python error, if any (which will clear the Python error - /// indicator first if already set). + /// Restores the currently-held Python error (which will clear the Python error indicator first + // if already set). void restore() { - if (m_type) { - // As long as this type is copyable, there is no point in releasing m_type, m_value, - // m_trace. - PyErr_Restore( - m_type.inc_ref().ptr(), m_value.inc_ref().ptr(), m_trace.inc_ref().ptr()); - } + // As long as this type is copyable, there is no point in releasing m_type, m_value, + // m_trace. + PyErr_Restore(m_type.inc_ref().ptr(), m_value.inc_ref().ptr(), m_trace.inc_ref().ptr()); } /// If it is impossible to raise the currently-held error, such as in a destructor, we can diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 99eaed36a9..c821feade6 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -230,8 +230,8 @@ TEST_SUBMODULE(exceptions, m) { if ((err && e.what() != std::string("ValueError: foo")) || (!err && e.what() - != std::string("Internal error: error_string() called without a Python " - "error available."))) { + != std::string("Internal error: pybind11::detail::error_already_set " + "called while Python error indicator not set."))) { PyErr_Clear(); throw std::runtime_error("error message mismatch"); } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 21520a8c12..6eb8d976af 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -14,9 +14,13 @@ def test_std_exception(msg): def test_error_already_set(msg): - with pytest.raises(SystemError) as excinfo: + with pytest.raises(RuntimeError) as excinfo: m.throw_already_set(False) - assert "without setting" in str(excinfo.value) + assert ( + msg(excinfo.value) + == "Internal error: pybind11::detail::error_already_set called" + " while Python error indicator not set." + ) with pytest.raises(ValueError) as excinfo: m.throw_already_set(True) From 90b2453cbdf24c86c56f1a376bc32b8f1b6f4235 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 11 May 2022 03:48:08 -0700 Subject: [PATCH 051/121] bug fix: error_string(PyObject **, ...) --- include/pybind11/detail/type_caster_base.h | 18 +++++++++--------- include/pybind11/pytypes.h | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 30c96fb5ed..ff80c4b0b1 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -471,24 +471,24 @@ PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp) } PYBIND11_NOINLINE std::string -error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { - if (!exc_type) { +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"); } - PyErr_NormalizeException(&exc_type, &exc_value, &exc_trace); + PyErr_NormalizeException(exc_type, exc_value, exc_trace); - auto result = handle(exc_type).attr("__name__").cast(); + auto result = handle(*exc_type).attr("__name__").cast(); result += ": "; - if (exc_value) { - result += (std::string) str(exc_value); + if (*exc_value) { + result += (std::string) str(*exc_value); } - if (exc_trace) { + if (*exc_trace) { #if !defined(PYPY_VERSION) - auto *tb = (PyTracebackObject *) exc_trace; + auto *tb = reinterpret_cast(*exc_trace); // Get the deepest trace possible. while (tb->tb_next) { @@ -531,7 +531,7 @@ error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { PYBIND11_NOINLINE std::string error_string() { error_scope scope; // Fetch error state. - return error_string(scope.type, scope.value, scope.trace); + return error_string(&scope.type, &scope.value, &scope.trace); } PYBIND11_NOINLINE handle get_object_handle(const void *ptr, const detail::type_info *type) { diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index b5fb247192..847ca34f32 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -364,7 +364,7 @@ T reinterpret_steal(handle h) { PYBIND11_NAMESPACE_BEGIN(detail) std::string error_string(); -std::string error_string(PyObject *, PyObject *, PyObject *); +std::string error_string(PyObject **, PyObject **, PyObject **); inline const char *obj_class_name(PyObject *obj) { if (Py_TYPE(obj) == &PyType_Type) { @@ -409,7 +409,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { const char *what() const noexcept override { if (m_lazy_what.empty()) { try { - m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); + m_lazy_what = detail::error_string(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); // Negate the if condition to test the catch(...) block below. if (m_lazy_what.empty()) { throw std::runtime_error( @@ -497,7 +497,7 @@ 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) From dded0243a425ee73de2cdfae2220a458f66b5416 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 11 May 2022 23:20:30 -0700 Subject: [PATCH 052/121] Putting back the two eager PyErr_NormalizeException() calls. --- include/pybind11/detail/type_caster_base.h | 19 +++++++++---------- include/pybind11/pytypes.h | 8 +++++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index ff80c4b0b1..01312f14ee 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -471,24 +471,22 @@ PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp) } PYBIND11_NOINLINE std::string -error_string(PyObject **exc_type, PyObject **exc_value, PyObject **exc_trace) { - if (*exc_type == nullptr) { +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"); } - PyErr_NormalizeException(exc_type, exc_value, exc_trace); - - auto result = handle(*exc_type).attr("__name__").cast(); + auto result = handle(exc_type).attr("__name__").cast(); result += ": "; - if (*exc_value) { - result += (std::string) str(*exc_value); + if (exc_value) { + result += (std::string) str(exc_value); } - if (*exc_trace) { + if (exc_trace) { #if !defined(PYPY_VERSION) - auto *tb = reinterpret_cast(*exc_trace); + auto *tb = reinterpret_cast(exc_trace); // Get the deepest trace possible. while (tb->tb_next) { @@ -531,7 +529,8 @@ error_string(PyObject **exc_type, PyObject **exc_value, PyObject **exc_trace) { PYBIND11_NOINLINE std::string error_string() { error_scope scope; // Fetch error state. - return error_string(&scope.type, &scope.value, &scope.trace); + PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace); + return error_string(scope.type, scope.value, scope.trace); } PYBIND11_NOINLINE handle get_object_handle(const void *ptr, const detail::type_info *type) { diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 847ca34f32..dbd2c9fe19 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -364,7 +364,7 @@ T reinterpret_steal(handle h) { PYBIND11_NAMESPACE_BEGIN(detail) std::string error_string(); -std::string error_string(PyObject **, PyObject **, PyObject **); +std::string error_string(PyObject *, PyObject *, PyObject *); inline const char *obj_class_name(PyObject *obj) { if (Py_TYPE(obj) == &PyType_Type) { @@ -396,6 +396,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { PyErr_SetString(PyExc_RuntimeError, m_lazy_what.c_str()); } PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); } error_already_set(const error_already_set &) = default; @@ -409,7 +410,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { const char *what() const noexcept override { if (m_lazy_what.empty()) { try { - m_lazy_what = detail::error_string(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); // Negate the if condition to test the catch(...) block below. if (m_lazy_what.empty()) { throw std::runtime_error( @@ -461,7 +462,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { // if already set). void restore() { // As long as this type is copyable, there is no point in releasing m_type, m_value, - // m_trace. + // 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()); } From 4193375ed6bcdcaf9ea29754f80962ec7a3f349b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 11 May 2022 23:59:14 -0700 Subject: [PATCH 053/121] Change error_already_set() to call pybind11_fail() if the Python error indicator not set. The net result is that a std::runtime_error is thrown instead of error_already_set, but all tests pass as is. --- include/pybind11/pytypes.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index dbd2c9fe19..912c4a3cbc 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -391,9 +391,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { /// RuntimeError("Internal error: ..."). The current Python error indicator will be cleared. error_already_set() : std::runtime_error("") { if (!PyErr_Occurred()) { - m_lazy_what = "Internal error: pybind11::detail::error_already_set called while " - "Python error indicator not set."; - PyErr_SetString(PyExc_RuntimeError, m_lazy_what.c_str()); + pybind11_fail("Internal error: pybind11::detail::error_already_set called while " + "Python error indicator not set."); } PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); From b020e0427534906981f43c653cc3b7e99c9d3dce Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 12 May 2022 00:29:04 -0700 Subject: [PATCH 054/121] Remove mutable (fixes oversight in the previous commit). --- include/pybind11/pytypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 912c4a3cbc..847a7b34ab 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -498,7 +498,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { const object &trace() const { return m_trace; } private: - mutable object m_type, m_value, m_trace; + object m_type, m_value, m_trace; mutable std::string m_lazy_what; }; #if defined(_MSC_VER) From 02df6c01146f7470d37f7b9728d05d7154f7383f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 12 May 2022 01:30:20 -0700 Subject: [PATCH 055/121] Normalize the exception only locally in error_string(). Python 3.6 & 3.7 test failures expected. This is meant for benchmarking, to determine if it is worth the trouble looking into the failures. --- include/pybind11/detail/type_caster_base.h | 17 +++++++++++------ include/pybind11/pytypes.h | 1 - 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 01312f14ee..ae8994945a 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -477,16 +477,22 @@ error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { "Internal error: pybind11::detail::error_string() called with exc_type == nullptr"); } - auto result = handle(exc_type).attr("__name__").cast(); + // Normalize the exception only locally (to avoid side-effects for our caller). + object exc_type_loc = reinterpret_borrow(exc_type); + object exc_value_loc = reinterpret_borrow(exc_value); + object exc_trace_loc = reinterpret_borrow(exc_trace); + PyErr_NormalizeException(&exc_type_loc.ptr(), &exc_value_loc.ptr(), &exc_trace_loc.ptr()); + + auto result = exc_type_loc.attr("__name__").cast(); result += ": "; - if (exc_value) { - result += (std::string) str(exc_value); + if (exc_value_loc) { + result += (std::string) str(exc_value_loc); } - if (exc_trace) { + if (exc_trace_loc) { #if !defined(PYPY_VERSION) - auto *tb = reinterpret_cast(exc_trace); + auto *tb = reinterpret_cast(exc_trace_loc.ptr()); // Get the deepest trace possible. while (tb->tb_next) { @@ -529,7 +535,6 @@ error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { PYBIND11_NOINLINE std::string error_string() { error_scope scope; // Fetch error state. - PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace); return error_string(scope.type, scope.value, scope.trace); } diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 847a7b34ab..45b771ebe8 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -395,7 +395,6 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { "Python error indicator not set."); } PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); - PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); } error_already_set(const error_already_set &) = default; From d28c155c1d0dd2448b3f8fb8026c34bb7178cc79 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 12 May 2022 02:00:43 -0700 Subject: [PATCH 056/121] clang-tidy: use auto --- include/pybind11/detail/type_caster_base.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index ae8994945a..01772f9692 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -478,9 +478,9 @@ error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { } // Normalize the exception only locally (to avoid side-effects for our caller). - object exc_type_loc = reinterpret_borrow(exc_type); - object exc_value_loc = reinterpret_borrow(exc_value); - object exc_trace_loc = reinterpret_borrow(exc_trace); + auto exc_type_loc = reinterpret_borrow(exc_type); + auto exc_value_loc = reinterpret_borrow(exc_value); + auto exc_trace_loc = reinterpret_borrow(exc_trace); PyErr_NormalizeException(&exc_type_loc.ptr(), &exc_value_loc.ptr(), &exc_trace_loc.ptr()); auto result = exc_type_loc.attr("__name__").cast(); From 7578de9c48454e9f0c079c747b10f51485028d06 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 12 May 2022 13:02:36 -0700 Subject: [PATCH 057/121] Use `gil_scoped_acquire_local` in `error_already_set` destructor. See long comment. --- include/pybind11/pybind11.h | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 8493e06d3a..5c3fd9b3fb 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2611,13 +2611,22 @@ void print(Args &&...args) { } error_already_set::~error_already_set() { - gil_scoped_acquire gil; - { - error_scope scope; - m_type.release().dec_ref(); - m_value.release().dec_ref(); - m_trace.release().dec_ref(); - } + // Not using py::gil_scoped_acquire here since that calls get_internals, + // which triggers tensorflow failures (a full explanation is currently unknown). + // gil_scoped_acquire_local here is a straight copy from get_internals code. + // I.e. py::gil_scoped_acquire acquires the GIL in detail/internals.h exactly + // like we do here now, releases it, then acquires it again in gil.h. + // Using gil_scoped_acquire_local cuts out the get_internals overhead and + // fixes the tensorflow failures. + struct gil_scoped_acquire_local { + gil_scoped_acquire_local() : state(PyGILState_Ensure()) {} + ~gil_scoped_acquire_local() { PyGILState_Release(state); } + const PyGILState_STATE state; + } gil; + error_scope scope; + m_type.release().dec_ref(); + m_value.release().dec_ref(); + m_trace.release().dec_ref(); } PYBIND11_NAMESPACE_BEGIN(detail) From bab6f668e1e3a50557e924d61b9d1576e1a2d7a1 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 12 May 2022 14:26:01 -0700 Subject: [PATCH 058/121] For Python < 3.8: `PyErr_NormalizeException` before `PyErr_WriteUnraisable` --- include/pybind11/pytypes.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 45b771ebe8..cb531b43e4 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -472,7 +472,15 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { /// this call, the current object no longer stores the error variables, and neither does /// Python. void discard_as_unraisable(object err_context) { +#if PY_VERSION_HEX >= 0x03080000 restore(); +#else + auto exc_type_loc = m_type; + auto exc_value_loc = m_value; + auto exc_trace_loc = m_trace; + PyErr_NormalizeException(&exc_type_loc.ptr(), &exc_value_loc.ptr(), &exc_trace_loc.ptr()); + PyErr_Restore(exc_type_loc.ptr(), exc_value_loc.ptr(), exc_trace_loc.ptr()); +#endif PyErr_WriteUnraisable(err_context.ptr()); } /// An alternate version of `discard_as_unraisable()`, where a string provides information on From 2ad22854dca6e69dd4d7a8a7f86a68b840344325 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 13 May 2022 00:11:17 -0700 Subject: [PATCH 059/121] Go back to replacing the held Python exception with then normalized exception, if & when needed. Consistently document the side-effect. --- include/pybind11/detail/type_caster_base.h | 23 ++++++++------- include/pybind11/pytypes.h | 34 ++++++++++++---------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 01772f9692..ff2654e391 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -470,29 +470,28 @@ PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp) return isinstance(obj, type); } +// 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) { +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"); } - // Normalize the exception only locally (to avoid side-effects for our caller). - auto exc_type_loc = reinterpret_borrow(exc_type); - auto exc_value_loc = reinterpret_borrow(exc_value); - auto exc_trace_loc = reinterpret_borrow(exc_trace); - PyErr_NormalizeException(&exc_type_loc.ptr(), &exc_value_loc.ptr(), &exc_trace_loc.ptr()); + PyErr_NormalizeException(&exc_type, &exc_value, &exc_trace); - auto result = exc_type_loc.attr("__name__").cast(); + auto result = handle(exc_type).attr("__name__").cast(); result += ": "; - if (exc_value_loc) { - result += (std::string) str(exc_value_loc); + if (exc_value) { + result += str(exc_value).cast(); } - if (exc_trace_loc) { + if (exc_trace) { #if !defined(PYPY_VERSION) - auto *tb = reinterpret_cast(exc_trace_loc.ptr()); + auto *tb = reinterpret_cast(exc_trace); // Get the deepest trace possible. while (tb->tb_next) { @@ -533,6 +532,8 @@ error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { 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); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index cb531b43e4..3bde261de8 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -364,7 +364,7 @@ T reinterpret_steal(handle h) { PYBIND11_NAMESPACE_BEGIN(detail) std::string error_string(); -std::string error_string(PyObject *, PyObject *, PyObject *); +std::string error_string(PyObject *&, PyObject *&, PyObject *&); inline const char *obj_class_name(PyObject *obj) { if (Py_TYPE(obj) == &PyType_Type) { @@ -387,8 +387,8 @@ PYBIND11_NAMESPACE_END(detail) /// python). class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { public: - /// Constructs a new exception from the current Python error indicator, or substitutes a - /// RuntimeError("Internal error: ..."). The current Python error indicator will be cleared. + /// Fetches the current Python exception (using PyErr_Fetch()), which will clear the + /// current Python error indicator. error_already_set() : std::runtime_error("") { if (!PyErr_Occurred()) { pybind11_fail("Internal error: pybind11::detail::error_already_set called while " @@ -398,6 +398,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } error_already_set(const error_already_set &) = default; + + /// Moving the members one-by-one to be able to specify noexcept. error_already_set(error_already_set &&e) noexcept : std::runtime_error(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( @@ -405,6 +407,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { inline ~error_already_set() override; + /// NOTE: This 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()) { try { @@ -457,7 +461,10 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { } /// Restores the currently-held Python error (which will clear the Python error indicator first - // if already set). + /// if already set). + /// NOTE: This 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() { // As long as this type is copyable, there is no point in releasing m_type, m_value, // m_trace, but simply holding on the the references makes it possible to produce @@ -468,19 +475,14 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { /// 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 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 - restore(); -#else - auto exc_type_loc = m_type; - auto exc_value_loc = m_value; - auto exc_trace_loc = m_trace; - PyErr_NormalizeException(&exc_type_loc.ptr(), &exc_value_loc.ptr(), &exc_trace_loc.ptr()); - PyErr_Restore(exc_type_loc.ptr(), exc_value_loc.ptr(), exc_trace_loc.ptr()); +#if PY_VERSION_HEX < 0x03080000 + 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 @@ -505,7 +507,7 @@ 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) From e3ebb0dbcac2abfa6cfcaba1c50bf178959ab8a5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 13 May 2022 00:17:22 -0700 Subject: [PATCH 060/121] Slightly rewording comment. (There were also other failures.) --- include/pybind11/pybind11.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 5c3fd9b3fb..306b3f8b2b 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2612,12 +2612,13 @@ void print(Args &&...args) { error_already_set::~error_already_set() { // Not using py::gil_scoped_acquire here since that calls get_internals, - // which triggers tensorflow failures (a full explanation is currently unknown). + // which is known to trigger failures in the wild (a full explanation is + // currently unknown). // gil_scoped_acquire_local here is a straight copy from get_internals code. // I.e. py::gil_scoped_acquire acquires the GIL in detail/internals.h exactly // like we do here now, releases it, then acquires it again in gil.h. // Using gil_scoped_acquire_local cuts out the get_internals overhead and - // fixes the tensorflow failures. + // fixes the failures observed in the wild. See PR #1895 for more background. struct gil_scoped_acquire_local { gil_scoped_acquire_local() : state(PyGILState_Ensure()) {} ~gil_scoped_acquire_local() { PyGILState_Release(state); } From 8dff51d12e4af11aff415ee966070368fe606664 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 13 May 2022 00:27:11 -0700 Subject: [PATCH 061/121] Add 1-line comment for obj_class_name() --- include/pybind11/pytypes.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 3bde261de8..5ed5c0da2c 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -366,6 +366,7 @@ 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(obj)->tp_name; From 923725a6e5b6529ff8b25cff10083f9f54b1d46e Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 13 May 2022 01:07:51 -0700 Subject: [PATCH 062/121] Benchmark code, with results in this commit message. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit function #calls test time [s] μs / call master pure_unwind 729540 1.061 14.539876 err_set_unwind_err_clear 681476 1.040 15.260282 err_set_error_already_set 508038 1.049 20.640525 error_already_set_restore 555578 1.052 18.933288 pr1895_original_foo 244113 1.050 43.018168 PR / master PR #1895 pure_unwind 736981 1.054 14.295685 98.32% err_set_unwind_err_clear 685820 1.045 15.237399 99.85% err_set_error_already_set 661374 1.046 15.811879 76.61% error_already_set_restore 669881 1.048 15.645176 82.63% pr1895_original_foo 318243 1.059 33.290806 77.39% master @ commit ad146b2a1877e8ba3803f94a7837969835a297a7 Running tests in directory "/usr/local/google/home/rwgk/forked/pybind11/tests": ============================= test session starts ============================== platform linux -- Python 3.9.10, pytest-6.2.3, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /usr/local/google/home/rwgk/forked/pybind11/tests, configfile: pytest.ini collecting ... collected 5 items test_perf_error_already_set.py::test_perf[pure_unwind] PERF pure_unwind,729540,1.061,14.539876 PASSED test_perf_error_already_set.py::test_perf[err_set_unwind_err_clear] PERF err_set_unwind_err_clear,681476,1.040,15.260282 PASSED test_perf_error_already_set.py::test_perf[err_set_error_already_set] PERF err_set_error_already_set,508038,1.049,20.640525 PASSED test_perf_error_already_set.py::test_perf[error_already_set_restore] PERF error_already_set_restore,555578,1.052,18.933288 PASSED test_perf_error_already_set.py::test_perf[pr1895_original_foo] PERF pr1895_original_foo,244113,1.050,43.018168 PASSED ============================== 5 passed in 12.38s ============================== pr1895 @ commit 8dff51d12e4af11aff415ee966070368fe606664 Running tests in directory "/usr/local/google/home/rwgk/forked/pybind11/tests": ============================= test session starts ============================== platform linux -- Python 3.9.10, pytest-6.2.3, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /usr/local/google/home/rwgk/forked/pybind11/tests, configfile: pytest.ini collecting ... collected 5 items test_perf_error_already_set.py::test_perf[pure_unwind] PERF pure_unwind,736981,1.054,14.295685 PASSED test_perf_error_already_set.py::test_perf[err_set_unwind_err_clear] PERF err_set_unwind_err_clear,685820,1.045,15.237399 PASSED test_perf_error_already_set.py::test_perf[err_set_error_already_set] PERF err_set_error_already_set,661374,1.046,15.811879 PASSED test_perf_error_already_set.py::test_perf[error_already_set_restore] PERF error_already_set_restore,669881,1.048,15.645176 PASSED test_perf_error_already_set.py::test_perf[pr1895_original_foo] PERF pr1895_original_foo,318243,1.059,33.290806 PASSED ============================== 5 passed in 12.40s ============================== clang++ -o pybind11/tests/test_perf_error_already_set.os -c -std=c++17 -fPIC -fvisibility=hidden -Os -flto -Wall -Wextra -Wconversion -Wcast-qual -Wdeprecated -Wnon-virtual-dtor -Wunused-result -isystem /usr/include/python3.9 -isystem /usr/include/eigen3 -DPYBIND11_STRICT_ASSERTS_CLASS_HOLDER_VS_TYPE_CASTER_MIX -DPYBIND11_TEST_BOOST -Ipybind11/include -I/usr/local/google/home/rwgk/forked/pybind11/include -I/usr/local/google/home/rwgk/clone/pybind11/include /usr/local/google/home/rwgk/forked/pybind11/tests/test_perf_error_already_set.cpp clang++ -o lib/pybind11_tests.so -shared -fPIC -Os -flto -shared ... Debian clang version 13.0.1-3+build2 Target: x86_64-pc-linux-gnu Thread model: posix --- tests/test_perf_error_already_set.cpp | 74 +++++++++++++++++++++++++++ tests/test_perf_error_already_set.py | 58 +++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 tests/test_perf_error_already_set.cpp create mode 100644 tests/test_perf_error_already_set.py diff --git a/tests/test_perf_error_already_set.cpp b/tests/test_perf_error_already_set.cpp new file mode 100644 index 0000000000..db91491db5 --- /dev/null +++ b/tests/test_perf_error_already_set.cpp @@ -0,0 +1,74 @@ +#include "pybind11_tests.h" + +namespace test_perf_error_already_set { + +struct boost_python_error_already_set { + virtual ~boost_python_error_already_set() {} +}; + +void pure_unwind(std::size_t num_iterations) { + while (num_iterations) { + try { + throw boost_python_error_already_set(); + } catch (const boost_python_error_already_set &) { + } + num_iterations--; + } +} + +void err_set_unwind_err_clear(std::size_t num_iterations) { + while (num_iterations) { + try { + PyErr_SetString(PyExc_RuntimeError, ""); + throw boost_python_error_already_set(); + } catch (const boost_python_error_already_set &) { + PyErr_Clear(); + } + num_iterations--; + } +} + +void err_set_error_already_set(std::size_t num_iterations) { + while (num_iterations) { + try { + PyErr_SetString(PyExc_RuntimeError, ""); + throw py::error_already_set(); + } catch (const py::error_already_set &) { + } + num_iterations--; + } +} + +void error_already_set_restore(std::size_t num_iterations) { + PyErr_SetString(PyExc_RuntimeError, ""); + while (num_iterations) { + try { + throw py::error_already_set(); + } catch (py::error_already_set &e) { + e.restore(); + } + num_iterations--; + } + PyErr_Clear(); +} + +// https://github.com/pybind/pybind11/pull/1895 original PR description. +py::int_ pr1895_original_foo() { + py::dict d; + try { + return d["foo"]; + } catch (const py::error_already_set &) { + return py::int_(42); + } +} + +} // namespace test_perf_error_already_set + +TEST_SUBMODULE(perf_error_already_set, m) { + using namespace test_perf_error_already_set; + m.def("pure_unwind", pure_unwind); + m.def("err_set_unwind_err_clear", err_set_unwind_err_clear); + m.def("err_set_error_already_set", err_set_error_already_set); + m.def("error_already_set_restore", error_already_set_restore); + m.def("pr1895_original_foo", pr1895_original_foo); +} diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py new file mode 100644 index 0000000000..7115fb096f --- /dev/null +++ b/tests/test_perf_error_already_set.py @@ -0,0 +1,58 @@ +import time + +import pytest + +from pybind11_tests import perf_error_already_set as m + + +def find_call_repetitions( + callable, + time_delta_floor=1.0e-6, + target_elapsed_secs_multiplier=1.05, # Empirical. + target_elapsed_secs_tolerance=0.05, + max_iterations=100, + call_repetitions_first_pass=1000, + call_repetitions_target_elapsed_secs=1.0, +): + td_target = call_repetitions_target_elapsed_secs * target_elapsed_secs_multiplier + crd = call_repetitions_first_pass + for _ in range(max_iterations): + t0 = time.time() + callable(crd) + td = time.time() - t0 + crd = max(1, int(td_target * crd / max(td, time_delta_floor))) + if abs(td - td_target) / td_target < target_elapsed_secs_tolerance: + return crd + raise RuntimeError("find_call_repetitions failure: max_iterations exceeded.") + + +def pr1895_original_foo(num_iterations): + assert num_iterations >= 0 + while num_iterations: + m.pr1895_original_foo() + num_iterations -= 1 + + +@pytest.mark.parametrize( + "perf_name", + [ + "pure_unwind", + "err_set_unwind_err_clear", + "err_set_error_already_set", + "error_already_set_restore", + "pr1895_original_foo", + ], +) +def test_perf(perf_name): + print(flush=True) + if perf_name == "pr1895_original_foo": + callable = pr1895_original_foo + else: + callable = getattr(m, perf_name) + call_repetitions = find_call_repetitions(callable) + t0 = time.time() + callable(call_repetitions) + secs = time.time() - t0 + u_secs = secs * 10e6 + per_call = u_secs / call_repetitions + print(f"PERF {perf_name},{call_repetitions},{secs:.3f},{per_call:.6f}", flush=True) From 53f40a0d5c5a1cc771559c2b65bc3ef1ed4e5767 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 13 May 2022 01:36:53 -0700 Subject: [PATCH 063/121] Changing call_repetitions_target_elapsed_secs to 0.1 for regular unit testing. --- tests/test_perf_error_already_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py index 7115fb096f..a1d8f54609 100644 --- a/tests/test_perf_error_already_set.py +++ b/tests/test_perf_error_already_set.py @@ -12,7 +12,7 @@ def find_call_repetitions( target_elapsed_secs_tolerance=0.05, max_iterations=100, call_repetitions_first_pass=1000, - call_repetitions_target_elapsed_secs=1.0, + call_repetitions_target_elapsed_secs=0.1, # 1.0 for real benchmarking. ): td_target = call_repetitions_target_elapsed_secs * target_elapsed_secs_multiplier crd = call_repetitions_first_pass From b77e70361e454185e6fad52c9972363661f0435c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 14 May 2022 09:58:16 -0700 Subject: [PATCH 064/121] Adding in `recursion_depth` --- tests/test_perf_error_already_set.py | 29 +++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py index a1d8f54609..0f29e25879 100644 --- a/tests/test_perf_error_already_set.py +++ b/tests/test_perf_error_already_set.py @@ -5,20 +5,27 @@ from pybind11_tests import perf_error_already_set as m +def recurse_first_then_call(depth, callable, call_repetitions): + if depth: + recurse_first_then_call(depth - 1, callable, call_repetitions) + callable(call_repetitions) + + def find_call_repetitions( + recursion_depth, callable, time_delta_floor=1.0e-6, target_elapsed_secs_multiplier=1.05, # Empirical. target_elapsed_secs_tolerance=0.05, max_iterations=100, call_repetitions_first_pass=1000, - call_repetitions_target_elapsed_secs=0.1, # 1.0 for real benchmarking. + call_repetitions_target_elapsed_secs=1.0, # 1.0 for real benchmarking. ): td_target = call_repetitions_target_elapsed_secs * target_elapsed_secs_multiplier crd = call_repetitions_first_pass for _ in range(max_iterations): t0 = time.time() - callable(crd) + recurse_first_then_call(recursion_depth, callable, crd) td = time.time() - t0 crd = max(1, int(td_target * crd / max(td, time_delta_floor))) if abs(td - td_target) / td_target < target_elapsed_secs_tolerance: @@ -49,10 +56,14 @@ def test_perf(perf_name): callable = pr1895_original_foo else: callable = getattr(m, perf_name) - call_repetitions = find_call_repetitions(callable) - t0 = time.time() - callable(call_repetitions) - secs = time.time() - t0 - u_secs = secs * 10e6 - per_call = u_secs / call_repetitions - print(f"PERF {perf_name},{call_repetitions},{secs:.3f},{per_call:.6f}", flush=True) + for recursion_depth in (0, 8, 64, 512): + call_repetitions = find_call_repetitions(recursion_depth, callable) + t0 = time.time() + recurse_first_then_call(recursion_depth, callable, call_repetitions) + secs = time.time() - t0 + u_secs = secs * 10e6 + per_call = u_secs / call_repetitions + print( + f"PERF {perf_name},{recursion_depth},{call_repetitions},{secs:.3f},{per_call:.6f}", + flush=True, + ) From 493d7515aa31afbb04c77b5f134b0d24db0a3fb1 Mon Sep 17 00:00:00 2001 From: Aaron Gokaslan Date: Sat, 14 May 2022 13:11:07 -0400 Subject: [PATCH 065/121] Optimized ctor --- include/pybind11/pytypes.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 5ed5c0da2c..61e1f28a3d 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -391,11 +391,11 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { /// Fetches the current Python exception (using PyErr_Fetch()), which will clear the /// current Python error indicator. error_already_set() : std::runtime_error("") { - if (!PyErr_Occurred()) { + 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."); } - PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); } error_already_set(const error_already_set &) = default; From 6d9188f3a15b3ccf84daa305831b4f3649d1ce19 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 15 May 2022 01:44:45 -0700 Subject: [PATCH 066/121] Fix silly bug in recurse_first_then_call() --- tests/test_perf_error_already_set.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py index 0f29e25879..7766557f82 100644 --- a/tests/test_perf_error_already_set.py +++ b/tests/test_perf_error_already_set.py @@ -8,7 +8,8 @@ def recurse_first_then_call(depth, callable, call_repetitions): if depth: recurse_first_then_call(depth - 1, callable, call_repetitions) - callable(call_repetitions) + else: + callable(call_repetitions) def find_call_repetitions( From 8f3d3a671585bdb5aa69a195e98f536daac5c097 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 15 May 2022 01:47:30 -0700 Subject: [PATCH 067/121] Add tests that have equivalent PyErr_Fetch(), PyErr_Restore() but no try-catch. --- tests/test_perf_error_already_set.cpp | 31 +++++++++++++++++++++++++++ tests/test_perf_error_already_set.py | 3 +++ 2 files changed, 34 insertions(+) diff --git a/tests/test_perf_error_already_set.cpp b/tests/test_perf_error_already_set.cpp index db91491db5..53f395eb03 100644 --- a/tests/test_perf_error_already_set.cpp +++ b/tests/test_perf_error_already_set.cpp @@ -28,6 +28,14 @@ void err_set_unwind_err_clear(std::size_t num_iterations) { } } +void err_set_err_clear(std::size_t num_iterations) { + while (num_iterations) { + PyErr_SetString(PyExc_RuntimeError, ""); + PyErr_Clear(); + num_iterations--; + } +} + void err_set_error_already_set(std::size_t num_iterations) { while (num_iterations) { try { @@ -39,6 +47,15 @@ void err_set_error_already_set(std::size_t num_iterations) { } } +void err_set_err_fetch(std::size_t num_iterations) { + PyObject *exc_type, *exc_value, *exc_trace; + while (num_iterations) { + PyErr_SetString(PyExc_RuntimeError, ""); + PyErr_Fetch(&exc_type, &exc_value, &exc_trace); + num_iterations--; + } +} + void error_already_set_restore(std::size_t num_iterations) { PyErr_SetString(PyExc_RuntimeError, ""); while (num_iterations) { @@ -52,6 +69,17 @@ void error_already_set_restore(std::size_t num_iterations) { PyErr_Clear(); } +void err_fetch_err_restore(std::size_t num_iterations) { + PyErr_SetString(PyExc_RuntimeError, ""); + PyObject *exc_type, *exc_value, *exc_trace; + while (num_iterations) { + PyErr_Fetch(&exc_type, &exc_value, &exc_trace); + PyErr_Restore(exc_type, exc_value, exc_trace); + num_iterations--; + } + PyErr_Clear(); +} + // https://github.com/pybind/pybind11/pull/1895 original PR description. py::int_ pr1895_original_foo() { py::dict d; @@ -68,7 +96,10 @@ TEST_SUBMODULE(perf_error_already_set, m) { using namespace test_perf_error_already_set; m.def("pure_unwind", pure_unwind); m.def("err_set_unwind_err_clear", err_set_unwind_err_clear); + m.def("err_set_err_clear", err_set_err_clear); m.def("err_set_error_already_set", err_set_error_already_set); + m.def("err_set_err_fetch", err_set_err_fetch); m.def("error_already_set_restore", error_already_set_restore); + m.def("err_fetch_err_restore", err_fetch_err_restore); m.def("pr1895_original_foo", pr1895_original_foo); } diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py index 7766557f82..a28ea79d08 100644 --- a/tests/test_perf_error_already_set.py +++ b/tests/test_perf_error_already_set.py @@ -46,8 +46,11 @@ def pr1895_original_foo(num_iterations): [ "pure_unwind", "err_set_unwind_err_clear", + "err_set_err_clear", "err_set_error_already_set", + "err_set_err_fetch", "error_already_set_restore", + "err_fetch_err_restore", "pr1895_original_foo", ], ) From 724ee3d7d7c065f1327160ca72f438307968bbc5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 15 May 2022 02:19:18 -0700 Subject: [PATCH 068/121] Add call_error_string to tests. Sample only recursion_depth 0, 100. --- tests/test_perf_error_already_set.cpp | 32 ++++++++++++++----- tests/test_perf_error_already_set.py | 45 ++++++++++++++++++--------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/tests/test_perf_error_already_set.cpp b/tests/test_perf_error_already_set.cpp index 53f395eb03..a9208cf729 100644 --- a/tests/test_perf_error_already_set.cpp +++ b/tests/test_perf_error_already_set.cpp @@ -16,52 +16,67 @@ void pure_unwind(std::size_t num_iterations) { } } -void err_set_unwind_err_clear(std::size_t num_iterations) { +void err_set_unwind_err_clear(std::size_t num_iterations, bool call_error_string) { while (num_iterations) { try { PyErr_SetString(PyExc_RuntimeError, ""); throw boost_python_error_already_set(); } catch (const boost_python_error_already_set &) { + if (call_error_string) { + py::detail::error_string(); + } PyErr_Clear(); } num_iterations--; } } -void err_set_err_clear(std::size_t num_iterations) { +void err_set_err_clear(std::size_t num_iterations, bool call_error_string) { while (num_iterations) { PyErr_SetString(PyExc_RuntimeError, ""); + if (call_error_string) { + py::detail::error_string(); + } PyErr_Clear(); num_iterations--; } } -void err_set_error_already_set(std::size_t num_iterations) { +void err_set_error_already_set(std::size_t num_iterations, bool call_error_string) { while (num_iterations) { try { PyErr_SetString(PyExc_RuntimeError, ""); throw py::error_already_set(); - } catch (const py::error_already_set &) { + } catch (const py::error_already_set &e) { + if (call_error_string) { + e.what(); + } } num_iterations--; } } -void err_set_err_fetch(std::size_t num_iterations) { +void err_set_err_fetch(std::size_t num_iterations, bool call_error_string) { PyObject *exc_type, *exc_value, *exc_trace; while (num_iterations) { PyErr_SetString(PyExc_RuntimeError, ""); PyErr_Fetch(&exc_type, &exc_value, &exc_trace); + if (call_error_string) { + py::detail::error_string(exc_type, exc_value, exc_trace); + } num_iterations--; } } -void error_already_set_restore(std::size_t num_iterations) { +void error_already_set_restore(std::size_t num_iterations, bool call_error_string) { PyErr_SetString(PyExc_RuntimeError, ""); while (num_iterations) { try { throw py::error_already_set(); } catch (py::error_already_set &e) { + if (call_error_string) { + e.what(); + } e.restore(); } num_iterations--; @@ -69,11 +84,14 @@ void error_already_set_restore(std::size_t num_iterations) { PyErr_Clear(); } -void err_fetch_err_restore(std::size_t num_iterations) { +void err_fetch_err_restore(std::size_t num_iterations, bool call_error_string) { PyErr_SetString(PyExc_RuntimeError, ""); PyObject *exc_type, *exc_value, *exc_trace; while (num_iterations) { PyErr_Fetch(&exc_type, &exc_value, &exc_trace); + if (call_error_string) { + py::detail::error_string(exc_type, exc_value, exc_trace); + } PyErr_Restore(exc_type, exc_value, exc_trace); num_iterations--; } diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py index a28ea79d08..27649b0fb6 100644 --- a/tests/test_perf_error_already_set.py +++ b/tests/test_perf_error_already_set.py @@ -5,16 +5,22 @@ from pybind11_tests import perf_error_already_set as m -def recurse_first_then_call(depth, callable, call_repetitions): +def recurse_first_then_call(depth, callable, call_repetitions, call_error_string): if depth: - recurse_first_then_call(depth - 1, callable, call_repetitions) + recurse_first_then_call( + depth - 1, callable, call_repetitions, call_error_string + ) else: - callable(call_repetitions) + if call_error_string is None: + callable(call_repetitions) + else: + callable(call_repetitions, call_error_string) def find_call_repetitions( recursion_depth, callable, + call_error_string, time_delta_floor=1.0e-6, target_elapsed_secs_multiplier=1.05, # Empirical. target_elapsed_secs_tolerance=0.05, @@ -26,7 +32,7 @@ def find_call_repetitions( crd = call_repetitions_first_pass for _ in range(max_iterations): t0 = time.time() - recurse_first_then_call(recursion_depth, callable, crd) + recurse_first_then_call(recursion_depth, callable, crd, call_error_string) td = time.time() - t0 crd = max(1, int(td_target * crd / max(td, time_delta_floor))) if abs(td - td_target) / td_target < target_elapsed_secs_tolerance: @@ -60,14 +66,23 @@ def test_perf(perf_name): callable = pr1895_original_foo else: callable = getattr(m, perf_name) - for recursion_depth in (0, 8, 64, 512): - call_repetitions = find_call_repetitions(recursion_depth, callable) - t0 = time.time() - recurse_first_then_call(recursion_depth, callable, call_repetitions) - secs = time.time() - t0 - u_secs = secs * 10e6 - per_call = u_secs / call_repetitions - print( - f"PERF {perf_name},{recursion_depth},{call_repetitions},{secs:.3f},{per_call:.6f}", - flush=True, - ) + if perf_name in ("pure_unwind", "pr1895_original_foo"): + call_error_string_args = [None] + else: + call_error_string_args = [False, True] + for call_error_string in call_error_string_args: + for recursion_depth in (0, 100): + call_repetitions = find_call_repetitions( + recursion_depth, callable, call_error_string + ) + t0 = time.time() + recurse_first_then_call( + recursion_depth, callable, call_repetitions, call_error_string + ) + secs = time.time() - t0 + u_secs = secs * 10e6 + per_call = u_secs / call_repetitions + print( + f"PERF {perf_name},{recursion_depth},{call_repetitions},{call_error_string},{secs:.3f},{per_call:.6f}", + flush=True, + ) From 467ab00a35050b8a11bfa874f922043c0d5ef957 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 15 May 2022 02:43:10 -0700 Subject: [PATCH 069/121] Show lazy-what speed-up in percent. --- tests/test_perf_error_already_set.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py index 27649b0fb6..2d4142f735 100644 --- a/tests/test_perf_error_already_set.py +++ b/tests/test_perf_error_already_set.py @@ -70,7 +70,9 @@ def test_perf(perf_name): call_error_string_args = [None] else: call_error_string_args = [False, True] - for call_error_string in call_error_string_args: + first_per_call_results = None + for ix_outer, call_error_string in enumerate(call_error_string_args): + per_call_results = [] for recursion_depth in (0, 100): call_repetitions = find_call_repetitions( recursion_depth, callable, call_error_string @@ -82,7 +84,16 @@ def test_perf(perf_name): secs = time.time() - t0 u_secs = secs * 10e6 per_call = u_secs / call_repetitions + if first_per_call_results is None: + relative_to_first = "" + else: + rel_percent = first_per_call_results[ix_outer] / per_call * 100 + relative_to_first = f",{rel_percent:.2f}%" print( - f"PERF {perf_name},{recursion_depth},{call_repetitions},{call_error_string},{secs:.3f},{per_call:.6f}", + f"PERF {perf_name},{recursion_depth},{call_repetitions},{call_error_string}," + f"{secs:.3f}s,{per_call:.6f}μs{relative_to_first}", flush=True, ) + per_call_results.append(per_call) + if first_per_call_results is None: + first_per_call_results = tuple(per_call_results) From eaa1a48015442a7d6652a067c54939ef932e6782 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 15 May 2022 03:14:17 -0700 Subject: [PATCH 070/121] Include real_work in benchmarks. --- tests/test_perf_error_already_set.cpp | 33 ++++++++++++++++---- tests/test_perf_error_already_set.py | 44 +++++++++++++++++---------- 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/tests/test_perf_error_already_set.cpp b/tests/test_perf_error_already_set.cpp index a9208cf729..cd9f0c7af0 100644 --- a/tests/test_perf_error_already_set.cpp +++ b/tests/test_perf_error_already_set.cpp @@ -16,8 +16,18 @@ void pure_unwind(std::size_t num_iterations) { } } -void err_set_unwind_err_clear(std::size_t num_iterations, bool call_error_string) { +void do_real_work(std::size_t num_iterations) { while (num_iterations) { + std::sqrt(static_cast(num_iterations % 1000000)); + num_iterations--; + } +} + +void err_set_unwind_err_clear(std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + while (num_iterations) { + do_real_work(real_work); try { PyErr_SetString(PyExc_RuntimeError, ""); throw boost_python_error_already_set(); @@ -31,8 +41,9 @@ void err_set_unwind_err_clear(std::size_t num_iterations, bool call_error_string } } -void err_set_err_clear(std::size_t num_iterations, bool call_error_string) { +void err_set_err_clear(std::size_t num_iterations, bool call_error_string, std::size_t real_work) { while (num_iterations) { + do_real_work(real_work); PyErr_SetString(PyExc_RuntimeError, ""); if (call_error_string) { py::detail::error_string(); @@ -42,8 +53,11 @@ void err_set_err_clear(std::size_t num_iterations, bool call_error_string) { } } -void err_set_error_already_set(std::size_t num_iterations, bool call_error_string) { +void err_set_error_already_set(std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { while (num_iterations) { + do_real_work(real_work); try { PyErr_SetString(PyExc_RuntimeError, ""); throw py::error_already_set(); @@ -56,9 +70,10 @@ void err_set_error_already_set(std::size_t num_iterations, bool call_error_strin } } -void err_set_err_fetch(std::size_t num_iterations, bool call_error_string) { +void err_set_err_fetch(std::size_t num_iterations, bool call_error_string, std::size_t real_work) { PyObject *exc_type, *exc_value, *exc_trace; while (num_iterations) { + do_real_work(real_work); PyErr_SetString(PyExc_RuntimeError, ""); PyErr_Fetch(&exc_type, &exc_value, &exc_trace); if (call_error_string) { @@ -68,9 +83,12 @@ void err_set_err_fetch(std::size_t num_iterations, bool call_error_string) { } } -void error_already_set_restore(std::size_t num_iterations, bool call_error_string) { +void error_already_set_restore(std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { PyErr_SetString(PyExc_RuntimeError, ""); while (num_iterations) { + do_real_work(real_work); try { throw py::error_already_set(); } catch (py::error_already_set &e) { @@ -84,10 +102,13 @@ void error_already_set_restore(std::size_t num_iterations, bool call_error_strin PyErr_Clear(); } -void err_fetch_err_restore(std::size_t num_iterations, bool call_error_string) { +void err_fetch_err_restore(std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { PyErr_SetString(PyExc_RuntimeError, ""); PyObject *exc_type, *exc_value, *exc_trace; while (num_iterations) { + do_real_work(real_work); PyErr_Fetch(&exc_type, &exc_value, &exc_trace); if (call_error_string) { py::detail::error_string(exc_type, exc_value, exc_trace); diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py index 2d4142f735..b1b5d76b68 100644 --- a/tests/test_perf_error_already_set.py +++ b/tests/test_perf_error_already_set.py @@ -5,22 +5,25 @@ from pybind11_tests import perf_error_already_set as m -def recurse_first_then_call(depth, callable, call_repetitions, call_error_string): +def recurse_first_then_call( + depth, callable, call_repetitions, call_error_string, real_work +): if depth: recurse_first_then_call( - depth - 1, callable, call_repetitions, call_error_string + depth - 1, callable, call_repetitions, call_error_string, real_work ) else: if call_error_string is None: callable(call_repetitions) else: - callable(call_repetitions, call_error_string) + callable(call_repetitions, call_error_string, real_work) def find_call_repetitions( recursion_depth, callable, call_error_string, + real_work, time_delta_floor=1.0e-6, target_elapsed_secs_multiplier=1.05, # Empirical. target_elapsed_secs_tolerance=0.05, @@ -32,7 +35,9 @@ def find_call_repetitions( crd = call_repetitions_first_pass for _ in range(max_iterations): t0 = time.time() - recurse_first_then_call(recursion_depth, callable, crd, call_error_string) + recurse_first_then_call( + recursion_depth, callable, crd, call_error_string, real_work + ) td = time.time() - t0 crd = max(1, int(td_target * crd / max(td, time_delta_floor))) if abs(td - td_target) / td_target < target_elapsed_secs_tolerance: @@ -67,33 +72,40 @@ def test_perf(perf_name): else: callable = getattr(m, perf_name) if perf_name in ("pure_unwind", "pr1895_original_foo"): - call_error_string_args = [None] + extra_params = [(None, None)] else: - call_error_string_args = [False, True] - first_per_call_results = None - for ix_outer, call_error_string in enumerate(call_error_string_args): + real_work = 10000 + extra_params = [(False, 0), (True, 0), (False, real_work), (True, real_work)] + prev_per_call_results = None + for (call_error_string, real_work) in extra_params: per_call_results = [] - for recursion_depth in (0, 100): + for ix_rd, recursion_depth in enumerate((0, 100)): call_repetitions = find_call_repetitions( - recursion_depth, callable, call_error_string + recursion_depth, callable, call_error_string, real_work ) t0 = time.time() recurse_first_then_call( - recursion_depth, callable, call_repetitions, call_error_string + recursion_depth, + callable, + call_repetitions, + call_error_string, + real_work, ) secs = time.time() - t0 u_secs = secs * 10e6 per_call = u_secs / call_repetitions - if first_per_call_results is None: + if prev_per_call_results is None: relative_to_first = "" else: - rel_percent = first_per_call_results[ix_outer] / per_call * 100 + rel_percent = prev_per_call_results[ix_rd] / per_call * 100 relative_to_first = f",{rel_percent:.2f}%" print( f"PERF {perf_name},{recursion_depth},{call_repetitions},{call_error_string}," - f"{secs:.3f}s,{per_call:.6f}μs{relative_to_first}", + f"{real_work},{secs:.3f}s,{per_call:.6f}μs{relative_to_first}", flush=True, ) per_call_results.append(per_call) - if first_per_call_results is None: - first_per_call_results = tuple(per_call_results) + if prev_per_call_results is None: + prev_per_call_results = tuple(per_call_results) + else: + prev_per_call_results = None From dbed20a5d3d7080aa964372820c9c7054c585a74 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 16 May 2022 06:01:45 -0700 Subject: [PATCH 071/121] Replace all PyErr_SetString() with generate_python_exception_with_traceback() --- tests/test_perf_error_already_set.cpp | 97 ++++++++++++++++++++++----- tests/test_perf_error_already_set.py | 11 ++- 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/tests/test_perf_error_already_set.cpp b/tests/test_perf_error_already_set.cpp index cd9f0c7af0..cbbd7ce2b2 100644 --- a/tests/test_perf_error_already_set.cpp +++ b/tests/test_perf_error_already_set.cpp @@ -16,6 +16,14 @@ void pure_unwind(std::size_t num_iterations) { } } +void generate_python_exception_with_traceback(const py::object &callable_raising_exception) { + try { + callable_raising_exception(); + } catch (py::error_already_set &e) { + e.restore(); + } +} + void do_real_work(std::size_t num_iterations) { while (num_iterations) { std::sqrt(static_cast(num_iterations % 1000000)); @@ -23,13 +31,14 @@ void do_real_work(std::size_t num_iterations) { } } -void err_set_unwind_err_clear(std::size_t num_iterations, +void err_set_unwind_err_clear(const py::object &callable_raising_exception, + std::size_t num_iterations, bool call_error_string, std::size_t real_work) { while (num_iterations) { do_real_work(real_work); try { - PyErr_SetString(PyExc_RuntimeError, ""); + generate_python_exception_with_traceback(callable_raising_exception); throw boost_python_error_already_set(); } catch (const boost_python_error_already_set &) { if (call_error_string) { @@ -41,10 +50,13 @@ void err_set_unwind_err_clear(std::size_t num_iterations, } } -void err_set_err_clear(std::size_t num_iterations, bool call_error_string, std::size_t real_work) { +void err_set_err_clear(const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { while (num_iterations) { do_real_work(real_work); - PyErr_SetString(PyExc_RuntimeError, ""); + generate_python_exception_with_traceback(callable_raising_exception); if (call_error_string) { py::detail::error_string(); } @@ -53,13 +65,14 @@ void err_set_err_clear(std::size_t num_iterations, bool call_error_string, std:: } } -void err_set_error_already_set(std::size_t num_iterations, +void err_set_error_already_set(const py::object &callable_raising_exception, + std::size_t num_iterations, bool call_error_string, std::size_t real_work) { while (num_iterations) { do_real_work(real_work); try { - PyErr_SetString(PyExc_RuntimeError, ""); + generate_python_exception_with_traceback(callable_raising_exception); throw py::error_already_set(); } catch (const py::error_already_set &e) { if (call_error_string) { @@ -70,11 +83,14 @@ void err_set_error_already_set(std::size_t num_iterations, } } -void err_set_err_fetch(std::size_t num_iterations, bool call_error_string, std::size_t real_work) { +void err_set_err_fetch(const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { PyObject *exc_type, *exc_value, *exc_trace; while (num_iterations) { do_real_work(real_work); - PyErr_SetString(PyExc_RuntimeError, ""); + generate_python_exception_with_traceback(callable_raising_exception); PyErr_Fetch(&exc_type, &exc_value, &exc_trace); if (call_error_string) { py::detail::error_string(exc_type, exc_value, exc_trace); @@ -83,10 +99,11 @@ void err_set_err_fetch(std::size_t num_iterations, bool call_error_string, std:: } } -void error_already_set_restore(std::size_t num_iterations, +void error_already_set_restore(const py::object &callable_raising_exception, + std::size_t num_iterations, bool call_error_string, std::size_t real_work) { - PyErr_SetString(PyExc_RuntimeError, ""); + generate_python_exception_with_traceback(callable_raising_exception); while (num_iterations) { do_real_work(real_work); try { @@ -102,10 +119,11 @@ void error_already_set_restore(std::size_t num_iterations, PyErr_Clear(); } -void err_fetch_err_restore(std::size_t num_iterations, +void err_fetch_err_restore(const py::object &callable_raising_exception, + std::size_t num_iterations, bool call_error_string, std::size_t real_work) { - PyErr_SetString(PyExc_RuntimeError, ""); + generate_python_exception_with_traceback(callable_raising_exception); PyObject *exc_type, *exc_value, *exc_trace; while (num_iterations) { do_real_work(real_work); @@ -134,11 +152,54 @@ py::int_ pr1895_original_foo() { TEST_SUBMODULE(perf_error_already_set, m) { using namespace test_perf_error_already_set; m.def("pure_unwind", pure_unwind); - m.def("err_set_unwind_err_clear", err_set_unwind_err_clear); - m.def("err_set_err_clear", err_set_err_clear); - m.def("err_set_error_already_set", err_set_error_already_set); - m.def("err_set_err_fetch", err_set_err_fetch); - m.def("error_already_set_restore", error_already_set_restore); - m.def("err_fetch_err_restore", err_fetch_err_restore); + m.def("err_set_unwind_err_clear", + // Is there an easier way to get an exception with traceback? + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + err_set_unwind_err_clear( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("err_set_err_clear", + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + err_set_err_clear( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("err_set_error_already_set", + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + err_set_error_already_set( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("err_set_err_fetch", + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + err_set_err_fetch( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("error_already_set_restore", + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + error_already_set_restore( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); + m.def("err_fetch_err_restore", + [m](const py::object &callable_raising_exception, + std::size_t num_iterations, + bool call_error_string, + std::size_t real_work) { + err_fetch_err_restore( + callable_raising_exception, num_iterations, call_error_string, real_work); + }); m.def("pr1895_original_foo", pr1895_original_foo); } diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py index b1b5d76b68..0f49b8f9b2 100644 --- a/tests/test_perf_error_already_set.py +++ b/tests/test_perf_error_already_set.py @@ -5,6 +5,10 @@ from pybind11_tests import perf_error_already_set as m +def raise_runtime_error_from_python(): + raise RuntimeError("Raised from Python.") + + def recurse_first_then_call( depth, callable, call_repetitions, call_error_string, real_work ): @@ -16,7 +20,12 @@ def recurse_first_then_call( if call_error_string is None: callable(call_repetitions) else: - callable(call_repetitions, call_error_string, real_work) + callable( + raise_runtime_error_from_python, + call_repetitions, + call_error_string, + real_work, + ) def find_call_repetitions( From f0215430797b62e26a9c5f6e07164779b871eee5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 16 May 2022 06:28:58 -0700 Subject: [PATCH 072/121] Better organization of test loops. --- tests/test_perf_error_already_set.py | 69 +++++++++++++--------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py index 0f49b8f9b2..9328ff99d3 100644 --- a/tests/test_perf_error_already_set.py +++ b/tests/test_perf_error_already_set.py @@ -81,40 +81,37 @@ def test_perf(perf_name): else: callable = getattr(m, perf_name) if perf_name in ("pure_unwind", "pr1895_original_foo"): - extra_params = [(None, None)] + real_work_list = [None] + call_error_string_list = [None] else: - real_work = 10000 - extra_params = [(False, 0), (True, 0), (False, real_work), (True, real_work)] - prev_per_call_results = None - for (call_error_string, real_work) in extra_params: - per_call_results = [] - for ix_rd, recursion_depth in enumerate((0, 100)): - call_repetitions = find_call_repetitions( - recursion_depth, callable, call_error_string, real_work - ) - t0 = time.time() - recurse_first_then_call( - recursion_depth, - callable, - call_repetitions, - call_error_string, - real_work, - ) - secs = time.time() - t0 - u_secs = secs * 10e6 - per_call = u_secs / call_repetitions - if prev_per_call_results is None: - relative_to_first = "" - else: - rel_percent = prev_per_call_results[ix_rd] / per_call * 100 - relative_to_first = f",{rel_percent:.2f}%" - print( - f"PERF {perf_name},{recursion_depth},{call_repetitions},{call_error_string}," - f"{real_work},{secs:.3f}s,{per_call:.6f}μs{relative_to_first}", - flush=True, - ) - per_call_results.append(per_call) - if prev_per_call_results is None: - prev_per_call_results = tuple(per_call_results) - else: - prev_per_call_results = None + real_work_list = [0, 10000] + call_error_string_list = [False, True] + for real_work in real_work_list: + for recursion_depth in [0, 100]: + first_per_call = None + for call_error_string in call_error_string_list: + call_repetitions = find_call_repetitions( + recursion_depth, callable, call_error_string, real_work + ) + t0 = time.time() + recurse_first_then_call( + recursion_depth, + callable, + call_repetitions, + call_error_string, + real_work, + ) + secs = time.time() - t0 + u_secs = secs * 10e6 + per_call = u_secs / call_repetitions + if first_per_call is None: + relative_to_first = "" + first_per_call = per_call + else: + rel_percent = first_per_call / per_call * 100 + relative_to_first = f",{rel_percent:.2f}%" + print( + f"PERF {perf_name},{recursion_depth},{call_repetitions},{call_error_string}," + f"{real_work},{secs:.3f}s,{per_call:.6f}μs{relative_to_first}", + flush=True, + ) From 125e2d076f3051e51336f26ac530b580a8741c50 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 16 May 2022 16:24:50 -0700 Subject: [PATCH 073/121] Add test_error_already_set_copy_move --- tests/test_exceptions.cpp | 15 +++++++++++++++ tests/test_exceptions.py | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index c821feade6..432bc1efa4 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -302,4 +302,19 @@ TEST_SUBMODULE(exceptions, m) { std::throw_with_nested(std::runtime_error("Outer Exception")); } }); + + m.def("move_error_already_set", [](bool use_move) { + try { + PyErr_SetString(PyExc_RuntimeError, use_move ? "To be moved." : "To be copied."); + throw py::error_already_set(); + } catch (const py::error_already_set &caught) { + if (use_move) { + py::error_already_set moved_to{std::move(caught)}; + return std::string(moved_to.what()); // Both destructors run. + } + py::error_already_set copied_to{caught}; + return std::string(copied_to.what()); // Both destructors run. + } + return std::string("Unreachable."); + }); } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 6eb8d976af..4e551cd840 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -274,3 +274,8 @@ def test_local_translator(msg): m.throws_local_simple_error() assert not isinstance(excinfo.value, cm.LocalSimpleException) assert msg(excinfo.value) == "this mod" + + +@pytest.mark.parametrize("use_move, expected", ((False, "copied."), (True, "moved."))) +def test_error_already_set_copy_move(use_move, expected): + assert m.move_error_already_set(use_move) == "RuntimeError: To be " + expected From b0379589b0989ed211fd072413873e0df0ee74da Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 16 May 2022 16:49:54 -0700 Subject: [PATCH 074/121] Fix bug in newly added test (discovered by clang-tidy): actually use move ctor --- tests/test_exceptions.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 432bc1efa4..1e3a39aba4 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -307,11 +307,12 @@ TEST_SUBMODULE(exceptions, m) { try { PyErr_SetString(PyExc_RuntimeError, use_move ? "To be moved." : "To be copied."); throw py::error_already_set(); - } catch (const py::error_already_set &caught) { + } catch (py::error_already_set &caught) { if (use_move) { py::error_already_set moved_to{std::move(caught)}; return std::string(moved_to.what()); // Both destructors run. } + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) py::error_already_set copied_to{caught}; return std::string(copied_to.what()); // Both destructors run. } From 2baa3bc77a4bb6f864d27236c46bfbf6ebd626a7 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 16 May 2022 16:55:58 -0700 Subject: [PATCH 075/121] MSVC detects the unreachable return --- tests/test_exceptions.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 1e3a39aba4..58144a36d1 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -316,6 +316,8 @@ TEST_SUBMODULE(exceptions, m) { py::error_already_set copied_to{caught}; return std::string(copied_to.what()); // Both destructors run. } +#if !defined(_MSC_VER) // MSVC detects that this is unreachable. return std::string("Unreachable."); +#endif }); } From 4c479f4c72a61bc46da3f27ef15b792d95b32db1 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 16 May 2022 17:24:47 -0700 Subject: [PATCH 076/121] change test_perf_error_already_set.py back to quick mode --- tests/test_perf_error_already_set.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py index 9328ff99d3..4c591e2df7 100644 --- a/tests/test_perf_error_already_set.py +++ b/tests/test_perf_error_already_set.py @@ -37,8 +37,8 @@ def find_call_repetitions( target_elapsed_secs_multiplier=1.05, # Empirical. target_elapsed_secs_tolerance=0.05, max_iterations=100, - call_repetitions_first_pass=1000, - call_repetitions_target_elapsed_secs=1.0, # 1.0 for real benchmarking. + call_repetitions_first_pass=100, + call_repetitions_target_elapsed_secs=0.1, # 1.0 for real benchmarking. ): td_target = call_repetitions_target_elapsed_secs * target_elapsed_secs_multiplier crd = call_repetitions_first_pass From b4326f903a796fb90685ac6630c84754158ecb78 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 16 May 2022 17:28:04 -0700 Subject: [PATCH 077/121] Inherit from std::exception (instead of std::runtime_error, which does not make sense anymore with the lazy what) --- include/pybind11/pytypes.h | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 61e1f28a3d..4671a7c55a 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -386,11 +386,11 @@ 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: /// Fetches the current Python exception (using PyErr_Fetch()), which will clear the /// current Python error indicator. - error_already_set() : std::runtime_error("") { + 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 " @@ -402,9 +402,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::runtime_error { /// Moving the members one-by-one to be able to specify noexcept. error_already_set(error_already_set &&e) noexcept - : std::runtime_error(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)} {}; + : 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)} {}; inline ~error_already_set() override; From aaec34e95af2855ad8736965bcbeb14be6eca0c7 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 17 May 2022 14:44:02 -0700 Subject: [PATCH 078/121] Special handling under Windows. --- include/pybind11/pytypes.h | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 4671a7c55a..1eb264b12c 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -411,19 +411,36 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// (if it is not normalized already). const char *what() const noexcept override { if (m_lazy_what.empty()) { + std::string failure_info; try { m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); - // Negate the if condition to test the catch(...) block below. - if (m_lazy_what.empty()) { - throw std::runtime_error( - "FATAL failure building pybind11::detail::error_already_set what()"); + 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(): "; + = "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 { @@ -454,7 +471,11 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { fflush(stderr); fprintf(stdout, "%s [STDOUT]\n", msg.c_str()); fflush(stdout); +#ifdef _MSC_VER + exit(-1); // Sadly. std::terminate() may pop up an interactive dialog box. +#else std::terminate(); +#endif } } return m_lazy_what.c_str(); From 99af31822b0b30001a58124708c986b152575663 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 17 May 2022 15:35:23 -0700 Subject: [PATCH 079/121] print with leading newline --- include/pybind11/pytypes.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 1eb264b12c..43d6cbcb4e 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -467,9 +467,9 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } // Intentionally using C calls to maximize reliability // (and to avoid #include ). - fprintf(stderr, "%s [STDERR]\n", msg.c_str()); + fprintf(stderr, "\n%s [STDERR]\n", msg.c_str()); fflush(stderr); - fprintf(stdout, "%s [STDOUT]\n", msg.c_str()); + 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. From 8c763600876d28049db830e7f0fc808fe67fbbde Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 17 May 2022 17:54:54 -0700 Subject: [PATCH 080/121] Removing test_perf_error_already_set (copies are under https://github.com/rwgk/rwgk_tbx/commit/7765113fbb659e1ea004c5ba24fb578244bc6cfd). --- tests/test_perf_error_already_set.cpp | 205 -------------------------- tests/test_perf_error_already_set.py | 117 --------------- 2 files changed, 322 deletions(-) delete mode 100644 tests/test_perf_error_already_set.cpp delete mode 100644 tests/test_perf_error_already_set.py diff --git a/tests/test_perf_error_already_set.cpp b/tests/test_perf_error_already_set.cpp deleted file mode 100644 index cbbd7ce2b2..0000000000 --- a/tests/test_perf_error_already_set.cpp +++ /dev/null @@ -1,205 +0,0 @@ -#include "pybind11_tests.h" - -namespace test_perf_error_already_set { - -struct boost_python_error_already_set { - virtual ~boost_python_error_already_set() {} -}; - -void pure_unwind(std::size_t num_iterations) { - while (num_iterations) { - try { - throw boost_python_error_already_set(); - } catch (const boost_python_error_already_set &) { - } - num_iterations--; - } -} - -void generate_python_exception_with_traceback(const py::object &callable_raising_exception) { - try { - callable_raising_exception(); - } catch (py::error_already_set &e) { - e.restore(); - } -} - -void do_real_work(std::size_t num_iterations) { - while (num_iterations) { - std::sqrt(static_cast(num_iterations % 1000000)); - num_iterations--; - } -} - -void err_set_unwind_err_clear(const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - while (num_iterations) { - do_real_work(real_work); - try { - generate_python_exception_with_traceback(callable_raising_exception); - throw boost_python_error_already_set(); - } catch (const boost_python_error_already_set &) { - if (call_error_string) { - py::detail::error_string(); - } - PyErr_Clear(); - } - num_iterations--; - } -} - -void err_set_err_clear(const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - while (num_iterations) { - do_real_work(real_work); - generate_python_exception_with_traceback(callable_raising_exception); - if (call_error_string) { - py::detail::error_string(); - } - PyErr_Clear(); - num_iterations--; - } -} - -void err_set_error_already_set(const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - while (num_iterations) { - do_real_work(real_work); - try { - generate_python_exception_with_traceback(callable_raising_exception); - throw py::error_already_set(); - } catch (const py::error_already_set &e) { - if (call_error_string) { - e.what(); - } - } - num_iterations--; - } -} - -void err_set_err_fetch(const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - PyObject *exc_type, *exc_value, *exc_trace; - while (num_iterations) { - do_real_work(real_work); - generate_python_exception_with_traceback(callable_raising_exception); - PyErr_Fetch(&exc_type, &exc_value, &exc_trace); - if (call_error_string) { - py::detail::error_string(exc_type, exc_value, exc_trace); - } - num_iterations--; - } -} - -void error_already_set_restore(const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - generate_python_exception_with_traceback(callable_raising_exception); - while (num_iterations) { - do_real_work(real_work); - try { - throw py::error_already_set(); - } catch (py::error_already_set &e) { - if (call_error_string) { - e.what(); - } - e.restore(); - } - num_iterations--; - } - PyErr_Clear(); -} - -void err_fetch_err_restore(const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - generate_python_exception_with_traceback(callable_raising_exception); - PyObject *exc_type, *exc_value, *exc_trace; - while (num_iterations) { - do_real_work(real_work); - PyErr_Fetch(&exc_type, &exc_value, &exc_trace); - if (call_error_string) { - py::detail::error_string(exc_type, exc_value, exc_trace); - } - PyErr_Restore(exc_type, exc_value, exc_trace); - num_iterations--; - } - PyErr_Clear(); -} - -// https://github.com/pybind/pybind11/pull/1895 original PR description. -py::int_ pr1895_original_foo() { - py::dict d; - try { - return d["foo"]; - } catch (const py::error_already_set &) { - return py::int_(42); - } -} - -} // namespace test_perf_error_already_set - -TEST_SUBMODULE(perf_error_already_set, m) { - using namespace test_perf_error_already_set; - m.def("pure_unwind", pure_unwind); - m.def("err_set_unwind_err_clear", - // Is there an easier way to get an exception with traceback? - [m](const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - err_set_unwind_err_clear( - callable_raising_exception, num_iterations, call_error_string, real_work); - }); - m.def("err_set_err_clear", - [m](const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - err_set_err_clear( - callable_raising_exception, num_iterations, call_error_string, real_work); - }); - m.def("err_set_error_already_set", - [m](const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - err_set_error_already_set( - callable_raising_exception, num_iterations, call_error_string, real_work); - }); - m.def("err_set_err_fetch", - [m](const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - err_set_err_fetch( - callable_raising_exception, num_iterations, call_error_string, real_work); - }); - m.def("error_already_set_restore", - [m](const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - error_already_set_restore( - callable_raising_exception, num_iterations, call_error_string, real_work); - }); - m.def("err_fetch_err_restore", - [m](const py::object &callable_raising_exception, - std::size_t num_iterations, - bool call_error_string, - std::size_t real_work) { - err_fetch_err_restore( - callable_raising_exception, num_iterations, call_error_string, real_work); - }); - m.def("pr1895_original_foo", pr1895_original_foo); -} diff --git a/tests/test_perf_error_already_set.py b/tests/test_perf_error_already_set.py deleted file mode 100644 index 4c591e2df7..0000000000 --- a/tests/test_perf_error_already_set.py +++ /dev/null @@ -1,117 +0,0 @@ -import time - -import pytest - -from pybind11_tests import perf_error_already_set as m - - -def raise_runtime_error_from_python(): - raise RuntimeError("Raised from Python.") - - -def recurse_first_then_call( - depth, callable, call_repetitions, call_error_string, real_work -): - if depth: - recurse_first_then_call( - depth - 1, callable, call_repetitions, call_error_string, real_work - ) - else: - if call_error_string is None: - callable(call_repetitions) - else: - callable( - raise_runtime_error_from_python, - call_repetitions, - call_error_string, - real_work, - ) - - -def find_call_repetitions( - recursion_depth, - callable, - call_error_string, - real_work, - time_delta_floor=1.0e-6, - target_elapsed_secs_multiplier=1.05, # Empirical. - target_elapsed_secs_tolerance=0.05, - max_iterations=100, - call_repetitions_first_pass=100, - call_repetitions_target_elapsed_secs=0.1, # 1.0 for real benchmarking. -): - td_target = call_repetitions_target_elapsed_secs * target_elapsed_secs_multiplier - crd = call_repetitions_first_pass - for _ in range(max_iterations): - t0 = time.time() - recurse_first_then_call( - recursion_depth, callable, crd, call_error_string, real_work - ) - td = time.time() - t0 - crd = max(1, int(td_target * crd / max(td, time_delta_floor))) - if abs(td - td_target) / td_target < target_elapsed_secs_tolerance: - return crd - raise RuntimeError("find_call_repetitions failure: max_iterations exceeded.") - - -def pr1895_original_foo(num_iterations): - assert num_iterations >= 0 - while num_iterations: - m.pr1895_original_foo() - num_iterations -= 1 - - -@pytest.mark.parametrize( - "perf_name", - [ - "pure_unwind", - "err_set_unwind_err_clear", - "err_set_err_clear", - "err_set_error_already_set", - "err_set_err_fetch", - "error_already_set_restore", - "err_fetch_err_restore", - "pr1895_original_foo", - ], -) -def test_perf(perf_name): - print(flush=True) - if perf_name == "pr1895_original_foo": - callable = pr1895_original_foo - else: - callable = getattr(m, perf_name) - if perf_name in ("pure_unwind", "pr1895_original_foo"): - real_work_list = [None] - call_error_string_list = [None] - else: - real_work_list = [0, 10000] - call_error_string_list = [False, True] - for real_work in real_work_list: - for recursion_depth in [0, 100]: - first_per_call = None - for call_error_string in call_error_string_list: - call_repetitions = find_call_repetitions( - recursion_depth, callable, call_error_string, real_work - ) - t0 = time.time() - recurse_first_then_call( - recursion_depth, - callable, - call_repetitions, - call_error_string, - real_work, - ) - secs = time.time() - t0 - u_secs = secs * 10e6 - per_call = u_secs / call_repetitions - if first_per_call is None: - relative_to_first = "" - first_per_call = per_call - else: - rel_percent = first_per_call / per_call * 100 - relative_to_first = f",{rel_percent:.2f}%" - print( - f"PERF {perf_name},{recursion_depth},{call_repetitions},{call_error_string}," - f"{real_work},{secs:.3f}s,{per_call:.6f}μs{relative_to_first}", - flush=True, - ) From c7149d810e4b85ea80463e66d8c09d1c4a6dc9da Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 17 May 2022 18:04:03 -0700 Subject: [PATCH 081/121] Avoid gil and scope overhead if there is nothing to release. --- include/pybind11/pybind11.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index d50cdfd522..9be29dbac8 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2612,6 +2612,9 @@ void print(Args &&...args) { } error_already_set::~error_already_set() { + if (!(m_type || m_value || m_trace)) { + return; // Avoid gil and scope overhead if there is nothing to release. + } // 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). From 47f5445cb4651c34ba361d2a10fd1ea3ae2923d1 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 17 May 2022 18:21:10 -0700 Subject: [PATCH 082/121] Restore default move ctor. "member function" instead of "function" (note that "method" is Python terminology). --- include/pybind11/pytypes.h | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 43d6cbcb4e..80c1e7a6eb 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -399,16 +399,12 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } error_already_set(const error_already_set &) = default; - - /// Moving the members one-by-one to be able to specify noexcept. - 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)} {}; + error_already_set(error_already_set &&) = default; inline ~error_already_set() override; - /// NOTE: This function may have the side-effect of normalizing the held Python exception - /// (if it is not normalized already). + /// 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()) { std::string failure_info; @@ -483,10 +479,10 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// Restores the currently-held Python error (which will clear the Python error indicator first /// if already set). - /// NOTE: This function will not necessarily restore the original Python exception, but may - /// restore the normalized exception if what() or discard_as_unraisable() were 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() { + void restore() const { // As long as this type is copyable, there is no point in releasing m_type, m_value, // m_trace, but simply holding on the the references makes it possible to produce // what() even after restore(). @@ -497,8 +493,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// 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. - /// NOTE: This function may have the side-effect of normalizing the held Python exception - /// (if it is not normalized already). + /// 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 PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); @@ -565,9 +561,8 @@ 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. -inline void raise_from(error_already_set &err, PyObject *type, const char *message) { +/// caused by the original error. +inline void raise_from(const error_already_set &err, PyObject *type, const char *message) { err.restore(); raise_from(type, message); } From 7c08e7fe9ee0f1f01732263b15361e58932f8b42 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 17 May 2022 18:28:45 -0700 Subject: [PATCH 083/121] Delete error_already_set copy ctor. --- include/pybind11/pytypes.h | 2 +- tests/test_exceptions.cpp | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 80c1e7a6eb..88882b2c6a 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -398,7 +398,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } } - error_already_set(const error_already_set &) = default; + error_already_set(const error_already_set &) = delete; error_already_set(error_already_set &&) = default; inline ~error_already_set() override; diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 58144a36d1..1ff63938cf 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -312,9 +312,7 @@ TEST_SUBMODULE(exceptions, m) { py::error_already_set moved_to{std::move(caught)}; return std::string(moved_to.what()); // Both destructors run. } - // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) - py::error_already_set copied_to{caught}; - return std::string(copied_to.what()); // Both destructors run. + return std::string(caught.what()); // TODO: Remove this code path. } #if !defined(_MSC_VER) // MSVC detects that this is unreachable. return std::string("Unreachable."); From f746a3dde848a6f95f0243220e5014ae2b09ea88 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 17 May 2022 21:58:56 -0700 Subject: [PATCH 084/121] Make restore() non-const again to resolve clang-tidy failure (still experimenting). --- include/pybind11/pytypes.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 88882b2c6a..dbbf8d7d3b 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -482,7 +482,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// 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() const { + void restore() { // As long as this type is copyable, there is no point in releasing m_type, m_value, // m_trace, but simply holding on the the references makes it possible to produce // what() even after restore(). @@ -562,7 +562,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. -inline void raise_from(const error_already_set &err, PyObject *type, const char *message) { +inline void raise_from(error_already_set &err, PyObject *type, const char *message) { err.restore(); raise_from(type, message); } From 25e4cc926c3b56726ad0a1d1a69ad4b0c589d6d6 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 17 May 2022 22:31:30 -0700 Subject: [PATCH 085/121] Bring back error_already_set copy ctor, to see if that resolves the 4 MSVC test failures. --- include/pybind11/pytypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index dbbf8d7d3b..16abdb78df 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -398,7 +398,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } } - error_already_set(const error_already_set &) = delete; + error_already_set(const error_already_set &) = default; error_already_set(error_already_set &&) = default; inline ~error_already_set() override; From 70b870a6bd77e9b40a68f683ef3a59e6cb8297fe Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 May 2022 02:21:13 -0700 Subject: [PATCH 086/121] Add noexcept to error_already_set copy & move ctors (as suggested by @skylion007 IIUC). --- include/pybind11/pytypes.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 16abdb78df..583a42902b 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -398,8 +398,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } } - error_already_set(const error_already_set &) = default; - error_already_set(error_already_set &&) = default; + error_already_set(const error_already_set &) noexcept = default; + error_already_set(error_already_set &&) noexcept = default; inline ~error_already_set() override; From d425bb8452f59ce8ff999edf209bed26dd27a5c2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 May 2022 14:52:38 -0700 Subject: [PATCH 087/121] Trying one-by-one noexcept copy ctor for old compilers. --- include/pybind11/pytypes.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 583a42902b..1b7657b4c6 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -398,8 +398,19 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } } +#if (defined(__clang__) && __clang_major__ >= 9) || (defined(__GNUC__) && __GNUC__ >= 6) \ + || (defined(_MSC_VER) && _MSC_VER >= 1920) error_already_set(const error_already_set &) noexcept = default; error_already_set(error_already_set &&) noexcept = default; +#else + /// 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 inline ~error_already_set() override; From 65aaa4ce8f55caa413b04ff83aa2ca4ef60fd109 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 May 2022 15:36:04 -0700 Subject: [PATCH 088/121] Add back test covering copy ctor. Add another simple test that exercises the copy ctor. --- tests/test_exceptions.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 1ff63938cf..d9fd81fb4a 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -269,7 +269,11 @@ TEST_SUBMODULE(exceptions, m) { if (ex.matches(exc_type)) { py::print(ex.what()); } else { - throw; + // Simply `throw;` also works and is better, but using `throw ex;` + // here to cover that situation (as observed in the wild). + // Needs the copy ctor. The C++ standard guarantees that it is available: + // C++17 18.1.5. + throw ex; } } }); @@ -312,7 +316,9 @@ TEST_SUBMODULE(exceptions, m) { py::error_already_set moved_to{std::move(caught)}; return std::string(moved_to.what()); // Both destructors run. } - return std::string(caught.what()); // TODO: Remove this code path. + // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + py::error_already_set copied_to{caught}; + return std::string(copied_to.what()); // Both destructors run. } #if !defined(_MSC_VER) // MSVC detects that this is unreachable. return std::string("Unreachable."); From 19bb5954de67d74a05b88d89d02e9d08fa943f05 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 May 2022 17:54:13 -0700 Subject: [PATCH 089/121] Exclude more older compilers from using the noexcept = default ctors. (The tests in the previous commit exposed that those are broken.) --- include/pybind11/pytypes.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 1b7657b4c6..fd2d6a4db2 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -398,8 +398,9 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } } -#if (defined(__clang__) && __clang_major__ >= 9) || (defined(__GNUC__) && __GNUC__ >= 6) \ - || (defined(_MSC_VER) && _MSC_VER >= 1920) +#if ((defined(__clang__) && __clang_major__ >= 9) || (defined(__GNUC__) && __GNUC__ >= 10) \ + || (defined(_MSC_VER) && _MSC_VER >= 1920)) \ + && !defined(__INTEL_COMPILER) error_already_set(const error_already_set &) noexcept = default; error_already_set(error_already_set &&) noexcept = default; #else From d07b5ba75e3387e5739e16ca5c4ec552da8ebc67 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 May 2022 18:00:37 -0700 Subject: [PATCH 090/121] Factor out & reuse gil_scoped_acquire_local as gil_scoped_acquire_simple --- include/pybind11/detail/internals.h | 17 ++++++++++++----- include/pybind11/pybind11.h | 13 ++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 3f83113bd7..3209c4ff94 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -37,6 +37,17 @@ using ExceptionTranslator = void (*)(std::exception_ptr); PYBIND11_NAMESPACE_BEGIN(detail) +// 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; +}; + // Forward declarations inline PyTypeObject *make_static_property_type(); inline PyTypeObject *make_default_metaclass(); @@ -410,11 +421,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()); diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 9be29dbac8..4ec43d0b8a 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2618,16 +2618,11 @@ error_already_set::~error_already_set() { // 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). - // gil_scoped_acquire_local here is a straight copy from get_internals code. - // I.e. py::gil_scoped_acquire acquires the GIL in detail/internals.h exactly - // like we do here now, releases it, then acquires it again in gil.h. - // Using gil_scoped_acquire_local cuts out the get_internals overhead and + // 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. - struct gil_scoped_acquire_local { - gil_scoped_acquire_local() : state(PyGILState_Ensure()) {} - ~gil_scoped_acquire_local() { PyGILState_Release(state); } - const PyGILState_STATE state; - } gil; + detail::gil_scoped_acquire_simple gil; error_scope scope; m_type.release().dec_ref(); m_value.release().dec_ref(); From f6bf5aa32f3685ea85d915e026df952b776f71ec Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 May 2022 18:09:37 -0700 Subject: [PATCH 091/121] Guard gil_scoped_acquire_simple by _Py_IsFinalizing() check. --- include/pybind11/pybind11.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 4ec43d0b8a..00596485f4 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2615,6 +2615,11 @@ error_already_set::~error_already_set() { if (!(m_type || m_value || m_trace)) { return; // Avoid gil and scope overhead if there is nothing to release. } + if (_Py_IsFinalizing()) { + // 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; + } // 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). From cc5d76ad0d660e922db4bd9d971d4d5ee4373d0f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 May 2022 19:29:23 -0700 Subject: [PATCH 092/121] what() GIL safety --- include/pybind11/detail/internals.h | 11 ----------- include/pybind11/pybind11.h | 2 +- include/pybind11/pytypes.h | 29 +++++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 3209c4ff94..4dfc8dc6c4 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -37,17 +37,6 @@ using ExceptionTranslator = void (*)(std::exception_ptr); PYBIND11_NAMESPACE_BEGIN(detail) -// 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; -}; - // Forward declarations inline PyTypeObject *make_static_property_type(); inline PyTypeObject *make_default_metaclass(); diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 00596485f4..928f755c3f 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2615,7 +2615,7 @@ error_already_set::~error_already_set() { if (!(m_type || m_value || m_trace)) { return; // Avoid gil and scope overhead if there is nothing to release. } - if (_Py_IsFinalizing()) { + if (!PyGILState_Check() && _Py_IsFinalizing()) { // 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; diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index fd2d6a4db2..cf7a97dbb2 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -374,6 +374,17 @@ inline const char *obj_class_name(PyObject *obj) { 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) @@ -401,9 +412,11 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { #if ((defined(__clang__) && __clang_major__ >= 9) || (defined(__GNUC__) && __GNUC__ >= 10) \ || (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}, @@ -413,13 +426,27 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { 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; + /// 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 (!PyGILState_Check() && _Py_IsFinalizing()) { + // 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."; + } std::string failure_info; + detail::gil_scoped_acquire_simple gil; try { 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. @@ -491,6 +518,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// 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(). @@ -516,6 +544,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } /// 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(PYBIND11_FROM_STRING(err_context))); } From 2b31e4d08e44de0816de975c78c554844cbd5102 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 18 May 2022 22:46:28 -0700 Subject: [PATCH 093/121] clang-tidy & Python 3.6 fixes --- include/pybind11/pybind11.h | 4 +++- include/pybind11/pytypes.h | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 928f755c3f..12758fcc0a 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2615,11 +2615,13 @@ error_already_set::~error_already_set() { if (!(m_type || m_value || m_trace)) { return; // Avoid gil and scope overhead if there is nothing to release. } - if (!PyGILState_Check() && _Py_IsFinalizing()) { +#if PY_VERSION_HEX >= 0x03070000 + if (PyGILState_Check() == 0 && _Py_IsFinalizing() != 0) { // 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). diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index cf7a97dbb2..348004d950 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -439,12 +439,14 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// exception (if it is not normalized already). const char *what() const noexcept override { if (m_lazy_what.empty()) { - if (!PyGILState_Check() && _Py_IsFinalizing()) { +#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 { From 2c5dd19b13fde4568ef5b8cc7065c370ae4de690 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 May 2022 16:19:12 -0700 Subject: [PATCH 094/121] Use `gil_scoped_acquire` in dtor, copy ctor, `what()`. Remove `_Py_IsFinalizing()` checks (they are racy: https://github.com/python/cpython/pull/28525). --- include/pybind11/pybind11.h | 98 ++++++++++++++++++++++++++----- include/pybind11/pytypes.h | 111 ++++++------------------------------ 2 files changed, 99 insertions(+), 110 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 12758fcc0a..204067ba26 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2615,27 +2615,95 @@ error_already_set::~error_already_set() { 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) { - // 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; + gil_scoped_acquire gil; error_scope scope; m_type.release().dec_ref(); m_value.release().dec_ref(); m_trace.release().dec_ref(); } +error_already_set::error_already_set(const error_already_set &e) noexcept + : std::exception{e}, m_lazy_what{e.m_lazy_what} { + gil_scoped_acquire gil; + error_scope scope; + m_type = e.m_type; + m_value = e.m_value; + m_trace = e.m_trace; +} + +const char *error_already_set::what() const noexcept { + if (m_lazy_what.empty()) { + std::string failure_info; + gil_scoped_acquire gil; + error_scope scope; + try { + 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(utf8_str_size)) + + '"'; + } + } + // Intentionally using C calls to maximize reliability + // (and to avoid #include ). + 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. +#else + std::terminate(); +#endif + } + } + return m_lazy_what.c_str(); +} + PYBIND11_NAMESPACE_BEGIN(detail) inline function get_type_override(const void *this_ptr, const type_info *this_type, const char *name) { diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 348004d950..92515000b2 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -409,114 +409,35 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } } + /// WARNING: This copy constructor needs to acquire the Python GIL. This can lead to + /// crashes (undefined behavior) if the Python interpreter is finalizing. + inline error_already_set(const error_already_set &e) noexcept; + #if ((defined(__clang__) && __clang_major__ >= 9) || (defined(__GNUC__) && __GNUC__ >= 10) \ || (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} {}; + /// Moving the members one-by-one to be able to specify noexcept. 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)} {}; + : std::exception{std::move(e)}, m_lazy_what{std::move(e.m_lazy_what)}, + m_type{std::move(e.m_type)}, m_value{std::move(e.m_value)}, m_trace{ + std::move(e.m_trace)} {} #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). + /// WARNING: This destructor needs to acquire the Python GIL. This can lead to + /// crashes (undefined behavior) if the Python interpreter is finalizing. inline ~error_already_set() override; - /// 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 + /// The what() result is built lazily on demand. + /// Any 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 { - 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(utf8_str_size)) - + '"'; - } - } - // Intentionally using C calls to maximize reliability - // (and to avoid #include ). - 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. -#else - std::terminate(); -#endif - } - } - return m_lazy_what.c_str(); - } + /// WARNING: This member function needs to acquire the Python GIL. This can lead to + /// crashes (undefined behavior) if the Python interpreter is finalizing. + inline const char *what() const noexcept override; /// Restores the currently-held Python error (which will clear the Python error indicator first /// if already set). @@ -567,8 +488,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { const object &trace() const { return m_trace; } private: - mutable object m_type, m_value, m_trace; mutable std::string m_lazy_what; + mutable object m_type, m_value, m_trace; }; #if defined(_MSC_VER) # pragma warning(pop) From 14be38cbfce56b0176368c239f887bde8fd1897e Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 May 2022 17:25:01 -0700 Subject: [PATCH 095/121] Remove error_scope from copy ctor. --- include/pybind11/pybind11.h | 1 - 1 file changed, 1 deletion(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 204067ba26..e67eaf4959 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2625,7 +2625,6 @@ error_already_set::~error_already_set() { error_already_set::error_already_set(const error_already_set &e) noexcept : std::exception{e}, m_lazy_what{e.m_lazy_what} { gil_scoped_acquire gil; - error_scope scope; m_type = e.m_type; m_value = e.m_value; m_trace = e.m_trace; From db97ce781d46f31f73ba8a9f5d96696199afb551 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Fri, 20 May 2022 19:54:18 -0700 Subject: [PATCH 096/121] Add `error_scope` to `get_internals()`, to cover the situation that `get_internals()` is called from the `error_already_set` dtor while a new Python error is in flight already. Also backing out `gil_scoped_acquire_simple` change. --- include/pybind11/detail/internals.h | 7 ++++++- include/pybind11/pytypes.h | 11 ----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/include/pybind11/detail/internals.h b/include/pybind11/detail/internals.h index 4dfc8dc6c4..86d487225c 100644 --- a/include/pybind11/detail/internals.h +++ b/include/pybind11/detail/internals.h @@ -410,7 +410,12 @@ 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. - gil_scoped_acquire_simple gil; + struct gil_scoped_acquire_local { + gil_scoped_acquire_local() : state(PyGILState_Ensure()) {} + ~gil_scoped_acquire_local() { PyGILState_Release(state); } + const PyGILState_STATE state; + } gil_scope; + error_scope err_scope; PYBIND11_STR_TYPE id(PYBIND11_INTERNALS_ID); auto builtins = handle(PyEval_GetBuiltins()); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 92515000b2..23f4ecc531 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -374,17 +374,6 @@ inline const char *obj_class_name(PyObject *obj) { 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) From 42d2e1ad972faf5924bdb0a164a1e0d2fc8eb7a7 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 23 May 2022 08:40:01 -0700 Subject: [PATCH 097/121] Add `FlakyException` tests with failure triggers in `__init__` and `__str__` THIS IS STILL A WORK IN PROGRESS. This commit is only an important resting point. This commit is a first attempt at addressing the observation that `PyErr_NormalizeException()` completely replaces the original exception if `__init__` fails. This can be very confusing even in small applications, and extremely confusing in large ones. --- include/pybind11/pybind11.h | 40 +++++++++++++++++++++---------------- include/pybind11/pytypes.h | 13 ++++++++++++ tests/test_exceptions.cpp | 13 ++++++++++++ tests/test_exceptions.py | 37 ++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 17 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index e67eaf4959..bb7dc8b0b7 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2632,16 +2632,26 @@ error_already_set::error_already_set(const error_already_set &e) noexcept const char *error_already_set::what() const noexcept { if (m_lazy_what.empty()) { + bool exc_type_mutated = false; std::string failure_info; gil_scoped_acquire gil; error_scope scope; + // PyErr_NormalizeException() may change the exception type if there are cascading + // failures. This can potentially be extremely confusing. + const char *exc_type_name_orig = detail::obj_class_name_or( + m_type.ptr(), "PY_EXC_ORIG_TYPE_IS_NULLPTR", "PY_EXC_ORIG_TYPE_NAME_IS_NULLPTR"); + PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + const char *exc_type_name_norm = detail::obj_class_name_or( + m_type.ptr(), "PY_EXC_NORM_TYPE_IS_NULLPTR", "PY_EXC_NORM_TYPE_NAME_IS_NULLPTR"); + if (std::strcmp(exc_type_name_orig, exc_type_name_norm) != 0) { + exc_type_mutated = true; + } try { 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 { @@ -2649,29 +2659,25 @@ const char *error_already_set::what() const noexcept { } 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()) { + if (exc_type_mutated || !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"; + std::string msg = "FATAL failure building pybind11::detail::error_already_set what() "; + if (!failure_info.empty()) { + msg += "[" + failure_info + "] "; + } + msg += "while processing Python exception: "; + if (exc_type_mutated) { + msg += "ORIGINAL "; + msg += exc_type_name_orig; + msg += " REPLACED BY "; + msg += exc_type_name_norm; } 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 += exc_type_name_orig; } msg += ": "; PyObject *val_str = PyObject_Str(m_value.ptr()); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 23f4ecc531..0a6f9ab921 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -374,6 +374,19 @@ inline const char *obj_class_name(PyObject *obj) { return Py_TYPE(obj)->tp_name; } +inline const char *obj_class_name_or(PyObject *obj, + const char *subst_if_type_is_nullptr, + const char *subst_if_name_is_nullptr) { + if (obj == nullptr) { + return subst_if_type_is_nullptr; + } + const char *name = obj_class_name(obj); + if (name == nullptr) { + return subst_if_name_is_nullptr; + } + return name; +} + PYBIND11_NAMESPACE_END(detail) #if defined(_MSC_VER) diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index d9fd81fb4a..3234f07aa9 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -105,6 +105,16 @@ struct PythonAlreadySetInDestructor { py::str s; }; +void raise_exception(const py::object &exc_type, const py::object &exc_value) { + PyErr_SetObject(exc_type.ptr(), exc_value.ptr()); + throw py::error_already_set(); +} + +std::string error_already_set_what(const py::object &exc_type, const py::object &exc_value) { + PyErr_SetObject(exc_type.ptr(), exc_value.ptr()); + return py::error_already_set().what(); +} + TEST_SUBMODULE(exceptions, m) { m.def("throw_std_exception", []() { throw std::runtime_error("This exception was intentionally thrown."); }); @@ -324,4 +334,7 @@ TEST_SUBMODULE(exceptions, m) { return std::string("Unreachable."); #endif }); + + m.def("raise_exception", raise_exception); + m.def("error_already_set_what", error_already_set_what); } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4e551cd840..b27c7a06e9 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -279,3 +279,40 @@ def test_local_translator(msg): @pytest.mark.parametrize("use_move, expected", ((False, "copied."), (True, "moved."))) def test_error_already_set_copy_move(use_move, expected): assert m.move_error_already_set(use_move) == "RuntimeError: To be " + expected + + +class FlakyException(Exception): + def __init__(self, failure_point): + if failure_point == "failure_point_init": + raise ValueError("triggered_failure_point_init") + self.failure_point = failure_point + + def __str__(self): + if self.failure_point == "failure_point_str": + raise ValueError("triggered_failure_point_str") + return "FlakyException.__str__" + + +def test_flaky_exception_happy(): + with pytest.raises(FlakyException) as excinfo: + m.raise_exception(FlakyException, ("happy",)) + assert str(excinfo.value) == "FlakyException.__str__" + w = m.error_already_set_what(FlakyException, "happy") + assert w == "FlakyException: FlakyException.__str__" + + +def test_flaky_exception_failure_point_init(): + with pytest.raises(ValueError) as excinfo: + m.raise_exception(FlakyException, ("failure_point_init",)) + assert str(excinfo.value) == "triggered_failure_point_init" + # w = m.error_already_set_what(FlakyException, ("failure_point_init",)) + + +def test_flaky_exception_failure_point_str(): + with pytest.raises(FlakyException) as excinfo_init: + m.raise_exception(FlakyException, ("failure_point_str",)) + assert repr(excinfo_init.value) == "FlakyException('failure_point_str')" + with pytest.raises(ValueError) as excinfo_str: + str(excinfo_init.value) + assert str(excinfo_str.value) == "triggered_failure_point_str" + # w = m.error_already_set_what(FlakyException, ("failure_point_str",)) From 6417a76353252c7572a67ddc7c72ba71d30d1401 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Mon, 23 May 2022 17:16:46 -0700 Subject: [PATCH 098/121] Tweaks to resolve Py 3.6 and PyPy CI failures. --- tests/test_exceptions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b27c7a06e9..87d168f4a5 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -301,6 +301,7 @@ def test_flaky_exception_happy(): assert w == "FlakyException: FlakyException.__str__" +@pytest.mark.xfail("env.PYPY") def test_flaky_exception_failure_point_init(): with pytest.raises(ValueError) as excinfo: m.raise_exception(FlakyException, ("failure_point_init",)) @@ -311,7 +312,10 @@ def test_flaky_exception_failure_point_init(): def test_flaky_exception_failure_point_str(): with pytest.raises(FlakyException) as excinfo_init: m.raise_exception(FlakyException, ("failure_point_str",)) - assert repr(excinfo_init.value) == "FlakyException('failure_point_str')" + if sys.version_info < (3, 7): + assert repr(excinfo_init.value) == "FlakyException('failure_point_str',)" + else: + assert repr(excinfo_init.value) == "FlakyException('failure_point_str')" with pytest.raises(ValueError) as excinfo_str: str(excinfo_init.value) assert str(excinfo_str.value) == "triggered_failure_point_str" From a62b3d726e4b058661b03747fa026a32e436117b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 24 May 2022 00:12:33 -0700 Subject: [PATCH 099/121] Normalize Python exception immediately in error_already_set ctor. For background see: https://github.com/pybind/pybind11/pull/1895#issuecomment-1135304081 --- include/pybind11/detail/type_caster_base.h | 22 +++---- include/pybind11/pybind11.h | 76 ++++------------------ include/pybind11/pytypes.h | 48 +++++++++++++- tests/test_exceptions.cpp | 2 +- tests/test_exceptions.py | 25 ++++--- 5 files changed, 84 insertions(+), 89 deletions(-) diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index ff2654e391..886f6f9cc7 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -470,28 +470,28 @@ PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp) return isinstance(obj, type); } -// 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) { +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"); } - PyErr_NormalizeException(&exc_type, &exc_value, &exc_trace); + auto exc_type_norm = reinterpret_borrow(exc_type); + auto exc_value_norm = reinterpret_borrow(exc_value); + auto exc_trace_norm = reinterpret_borrow(exc_trace); + PyErr_NormalizeException(&exc_type_norm.ptr(), &exc_value_norm.ptr(), &exc_trace_norm.ptr()); - auto result = handle(exc_type).attr("__name__").cast(); + auto result = exc_type_norm.attr("__name__").cast(); result += ": "; - if (exc_value) { - result += str(exc_value).cast(); + if (exc_value_norm) { + result += str(exc_value_norm).cast(); } - if (exc_trace) { + if (exc_trace_norm) { #if !defined(PYPY_VERSION) - auto *tb = reinterpret_cast(exc_trace); + auto *tb = reinterpret_cast(exc_trace_norm.ptr()); // Get the deepest trace possible. while (tb->tb_next) { @@ -532,8 +532,6 @@ error_string(PyObject *&exc_type, PyObject *&exc_value, PyObject *&exc_trace) { 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); diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index bb7dc8b0b7..6bde9eb416 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2631,80 +2631,26 @@ error_already_set::error_already_set(const error_already_set &e) noexcept } const char *error_already_set::what() const noexcept { - if (m_lazy_what.empty()) { - bool exc_type_mutated = false; + if (!m_lazy_what_completed) { std::string failure_info; gil_scoped_acquire gil; error_scope scope; - // PyErr_NormalizeException() may change the exception type if there are cascading - // failures. This can potentially be extremely confusing. - const char *exc_type_name_orig = detail::obj_class_name_or( - m_type.ptr(), "PY_EXC_ORIG_TYPE_IS_NULLPTR", "PY_EXC_ORIG_TYPE_NAME_IS_NULLPTR"); - PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); - const char *exc_type_name_norm = detail::obj_class_name_or( - m_type.ptr(), "PY_EXC_NORM_TYPE_IS_NULLPTR", "PY_EXC_NORM_TYPE_NAME_IS_NULLPTR"); - if (std::strcmp(exc_type_name_orig, exc_type_name_norm) != 0) { - exc_type_mutated = true; - } try { - 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()"; + std::string err_str = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); + if (err_str.empty()) { + m_lazy_what += ": "; + } else { + m_lazy_what = err_str; // Replace completely. } - // throw std::runtime_error("Uncomment for manual testing."); } catch (const std::exception &e) { - failure_info = "std::exception::what(): "; + m_lazy_what += ": CASCADING failure: std::exception::what(): "; try { - failure_info += e.what(); - } catch (...) { - failure_info += "UNRECOVERABLE"; - } - } catch (...) { - failure_info = "Unknown C++ exception"; - } - if (exc_type_mutated || !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() "; - if (!failure_info.empty()) { - msg += "[" + failure_info + "] "; - } - msg += "while processing Python exception: "; - if (exc_type_mutated) { - msg += "ORIGINAL "; - msg += exc_type_name_orig; - msg += " REPLACED BY "; - msg += exc_type_name_norm; - } else { - msg += exc_type_name_orig; + m_lazy_what += e.what(); + } catch (const std::exception &) { + m_lazy_what += "UNRECOVERABLE"; } - 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(utf8_str_size)) - + '"'; - } - } - // Intentionally using C calls to maximize reliability - // (and to avoid #include ). - 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. -#else - std::terminate(); -#endif } + m_lazy_what_completed = true; } return m_lazy_what.c_str(); } diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 0a6f9ab921..e7cb76f176 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -364,7 +364,7 @@ T reinterpret_steal(handle h) { PYBIND11_NAMESPACE_BEGIN(detail) std::string error_string(); -std::string error_string(PyObject *&, PyObject *&, PyObject *&); +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) { @@ -406,9 +406,50 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { 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 " + pybind11_fail("Internal error: pybind11::error_already_set called while " "Python error indicator not set."); } + const char *exc_type_name_orig = detail::obj_class_name(m_type.ptr()); + if (exc_type_name_orig == nullptr) { + pybind11_fail("Internal error: pybind11::error_already_set failed to obtain the name " + "of the original active exception type."); + } + m_lazy_what = exc_type_name_orig; + // PyErr_NormalizeException() may change the exception type if there are cascading + // failures. This can potentially be extremely confusing. + PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); + if (m_type.ptr() == nullptr) { + pybind11_fail("Internal error: pybind11::error_already_set failed to normalize the " + "active exception."); + } + const char *exc_type_name_norm = detail::obj_class_name(m_type.ptr()); + if (exc_type_name_orig == nullptr) { + pybind11_fail("Internal error: pybind11::error_already_set failed to obtain the name " + "of the normalized active exception type."); + } + if (exc_type_name_norm != m_lazy_what) { + std::string msg = "pybind11::error_already_set: MISMATCH of original and normalized " + "active exception types: "; + msg += "ORIGINAL "; + msg += m_lazy_what; + msg += " REPLACED BY "; + msg += exc_type_name_norm; + std::string msg2; + try { + msg2 = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); + } catch (const std::exception &e) { + msg2 = "CASCADING failure: std::exception::what(): "; + try { + msg2 += e.what(); + } catch (const std::exception &) { + msg2 += "UNRECOVERABLE"; + } + } + if (!msg2.empty()) { + msg += ":\n" + msg2; + } + pybind11_fail(msg); + } } /// WARNING: This copy constructor needs to acquire the Python GIL. This can lead to @@ -490,8 +531,9 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { const object &trace() const { return m_trace; } private: + object m_type, m_value, m_trace; mutable std::string m_lazy_what; - mutable object m_type, m_value, m_trace; + mutable bool m_lazy_what_completed = false; }; #if defined(_MSC_VER) # pragma warning(pop) diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 3234f07aa9..0f7f77e323 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -240,7 +240,7 @@ TEST_SUBMODULE(exceptions, m) { if ((err && e.what() != std::string("ValueError: foo")) || (!err && e.what() - != std::string("Internal error: pybind11::detail::error_already_set " + != std::string("Internal error: pybind11::error_already_set " "called while Python error indicator not set."))) { PyErr_Clear(); throw std::runtime_error("error message mismatch"); diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 87d168f4a5..a3ddf1d767 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -17,8 +17,7 @@ def test_error_already_set(msg): with pytest.raises(RuntimeError) as excinfo: m.throw_already_set(False) assert ( - msg(excinfo.value) - == "Internal error: pybind11::detail::error_already_set called" + msg(excinfo.value) == "Internal error: pybind11::error_already_set called" " while Python error indicator not set." ) @@ -302,11 +301,16 @@ def test_flaky_exception_happy(): @pytest.mark.xfail("env.PYPY") -def test_flaky_exception_failure_point_init(): - with pytest.raises(ValueError) as excinfo: - m.raise_exception(FlakyException, ("failure_point_init",)) - assert str(excinfo.value) == "triggered_failure_point_init" - # w = m.error_already_set_what(FlakyException, ("failure_point_init",)) +@pytest.mark.parametrize("func", (m.raise_exception, m.error_already_set_what)) +def test_flaky_exception_failure_point_init(func): + with pytest.raises(RuntimeError) as excinfo: + func(FlakyException, ("failure_point_init",)) + lines = str(excinfo.value).splitlines() + assert lines[:2] == [ + "pybind11::error_already_set: MISMATCH of original and normalized active exception types:" + " ORIGINAL FlakyException REPLACED BY ValueError:", + "ValueError: triggered_failure_point_init", + ] def test_flaky_exception_failure_point_str(): @@ -319,4 +323,9 @@ def test_flaky_exception_failure_point_str(): with pytest.raises(ValueError) as excinfo_str: str(excinfo_init.value) assert str(excinfo_str.value) == "triggered_failure_point_str" - # w = m.error_already_set_what(FlakyException, ("failure_point_str",)) + w = m.error_already_set_what(FlakyException, ("failure_point_str",)) + lines = w.splitlines() + assert ( + lines[0] + == "FlakyException: CASCADING failure: std::exception::what(): ValueError: triggered_failure_point_str" + ) From c7867969a3dc6c12a72185f5d7c5a5a55e5f6b0a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 24 May 2022 01:07:02 -0700 Subject: [PATCH 100/121] Fix oversights based on CI failures (copy & move ctor initialization). --- include/pybind11/pybind11.h | 3 ++- include/pybind11/pytypes.h | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 6bde9eb416..bef016cf54 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2623,7 +2623,8 @@ error_already_set::~error_already_set() { } error_already_set::error_already_set(const error_already_set &e) noexcept - : std::exception{e}, m_lazy_what{e.m_lazy_what} { + : std::exception{e}, m_lazy_what{e.m_lazy_what}, m_lazy_what_completed{ + e.m_lazy_what_completed} { gil_scoped_acquire gil; m_type = e.m_type; m_value = e.m_value; diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index e7cb76f176..cac41186e4 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -464,9 +464,9 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// Workaround for old compilers: /// Moving the members one-by-one to be able to specify noexcept. error_already_set(error_already_set &&e) noexcept - : std::exception{std::move(e)}, m_lazy_what{std::move(e.m_lazy_what)}, - m_type{std::move(e.m_type)}, m_value{std::move(e.m_value)}, m_trace{ - std::move(e.m_trace)} {} + : 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)}, + m_lazy_what_completed{std::move(e.m_lazy_what_completed)} {} #endif /// WARNING: This destructor needs to acquire the Python GIL. This can lead to From 61bb5137f2cf78a34e297977b1b0bb82c2731d0f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 24 May 2022 01:42:01 -0700 Subject: [PATCH 101/121] Move @pytest.mark.xfail("env.PYPY") after @pytest.mark.parametrize(...) --- tests/test_exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index a3ddf1d767..1626b5e95d 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -300,8 +300,8 @@ def test_flaky_exception_happy(): assert w == "FlakyException: FlakyException.__str__" -@pytest.mark.xfail("env.PYPY") @pytest.mark.parametrize("func", (m.raise_exception, m.error_already_set_what)) +@pytest.mark.xfail("env.PYPY") def test_flaky_exception_failure_point_init(func): with pytest.raises(RuntimeError) as excinfo: func(FlakyException, ("failure_point_init",)) From bb3f24cfbe8e2cbab65090f572e653f7d8691c7a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 24 May 2022 02:15:30 -0700 Subject: [PATCH 102/121] Use @pytest.mark.skipif (xfail does not work for segfaults, of course). --- tests/test_exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 1626b5e95d..93d76769a8 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -300,8 +300,8 @@ def test_flaky_exception_happy(): assert w == "FlakyException: FlakyException.__str__" +@pytest.mark.skipif("env.PYPY", reason="PyErr_NormalizeException Segmentation fault") @pytest.mark.parametrize("func", (m.raise_exception, m.error_already_set_what)) -@pytest.mark.xfail("env.PYPY") def test_flaky_exception_failure_point_init(func): with pytest.raises(RuntimeError) as excinfo: func(FlakyException, ("failure_point_init",)) From bee5fb4780fefa5b19678a2a14b1f24d1114d1e6 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 31 May 2022 18:53:22 -0700 Subject: [PATCH 103/121] Remove unused obj_class_name_or() function (it was added only under this PR). --- include/pybind11/pytypes.h | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index c0b6ea6830..561f175751 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -394,19 +394,6 @@ inline const char *obj_class_name(PyObject *obj) { return Py_TYPE(obj)->tp_name; } -inline const char *obj_class_name_or(PyObject *obj, - const char *subst_if_type_is_nullptr, - const char *subst_if_name_is_nullptr) { - if (obj == nullptr) { - return subst_if_type_is_nullptr; - } - const char *name = obj_class_name(obj); - if (name == nullptr) { - return subst_if_name_is_nullptr; - } - return name; -} - PYBIND11_NAMESPACE_END(detail) #if defined(_MSC_VER) From bea0c9fc74cae8ccd1fba5833180481ac7372040 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 31 May 2022 19:12:53 -0700 Subject: [PATCH 104/121] Remove already obsolete C++ comments and code that were added only under this PR. --- include/pybind11/pytypes.h | 9 --------- tests/test_exceptions.cpp | 5 ----- 2 files changed, 14 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 561f175751..ba156ce8fe 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -481,10 +481,6 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { inline ~error_already_set() override; /// The what() result is built lazily on demand. - /// Any 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). /// WARNING: This member function needs to acquire the Python GIL. This can lead to /// crashes (undefined behavior) if the Python interpreter is finalizing. inline const char *what() const noexcept override; @@ -505,12 +501,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// 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. - /// 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 - PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); -#endif restore(); PyErr_WriteUnraisable(err_context.ptr()); } diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 90588567f7..0e0de094ec 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -105,11 +105,6 @@ struct PythonAlreadySetInDestructor { py::str s; }; -void raise_exception(const py::object &exc_type, const py::object &exc_value) { - PyErr_SetObject(exc_type.ptr(), exc_value.ptr()); - throw py::error_already_set(); -} - std::string error_already_set_what(const py::object &exc_type, const py::object &exc_value) { PyErr_SetObject(exc_type.ptr(), exc_value.ptr()); return py::error_already_set().what(); From d6d371ace424e13548c3a1e035090b78e3727477 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Tue, 31 May 2022 19:16:06 -0700 Subject: [PATCH 105/121] Slightly better (newly added) comments. --- include/pybind11/pytypes.h | 1 + tests/test_exceptions.cpp | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index ba156ce8fe..e697a3634a 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -459,6 +459,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } } + /// The C++ standard explicitly prohibits deleting this copy ctor: C++17 18.1.5. /// WARNING: This copy constructor needs to acquire the Python GIL. This can lead to /// crashes (undefined behavior) if the Python interpreter is finalizing. inline error_already_set(const error_already_set &e) noexcept; diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 0e0de094ec..1fc2e0ee86 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -276,9 +276,7 @@ TEST_SUBMODULE(exceptions, m) { } else { // Simply `throw;` also works and is better, but using `throw ex;` // here to cover that situation (as observed in the wild). - // Needs the copy ctor. The C++ standard guarantees that it is available: - // C++17 18.1.5. - throw ex; + throw ex; // Invokes the copy ctor. } } }); From eec0547cdce7ba1edb64f79c1d12459e9599ccf2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 02:43:51 -0700 Subject: [PATCH 106/121] Factor out detail::error_fetch_and_normalize. Preparation for producing identical results from error_already_set::what() and detail::error_string(). Note that this is a very conservative refactoring. It would be much better to first move detail::error_string into detail/error_string.h --- include/pybind11/pybind11.h | 35 +++++----- include/pybind11/pytypes.h | 133 +++++++++++++++++++----------------- 2 files changed, 89 insertions(+), 79 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 1bbb101fb4..230976aa0e 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2625,7 +2625,9 @@ void print(Args &&...args) { detail::print(c.args(), c.kwargs()); } -error_already_set::~error_already_set() { +PYBIND11_NAMESPACE_BEGIN(detail) + +inline error_fetch_and_normalize::~error_fetch_and_normalize() { if (!(m_type || m_value || m_trace)) { return; // Avoid gil and scope overhead if there is nothing to release. } @@ -2636,41 +2638,40 @@ error_already_set::~error_already_set() { m_trace.release().dec_ref(); } -error_already_set::error_already_set(const error_already_set &e) noexcept - : std::exception{e}, m_lazy_what{e.m_lazy_what}, m_lazy_what_completed{ - e.m_lazy_what_completed} { +inline error_fetch_and_normalize::error_fetch_and_normalize(const error_fetch_and_normalize &other) + : m_lazy_error_string{other.m_lazy_error_string}, m_lazy_error_string_completed{ + other.m_lazy_error_string_completed} { gil_scoped_acquire gil; - m_type = e.m_type; - m_value = e.m_value; - m_trace = e.m_trace; + m_type = other.m_type; + m_value = other.m_value; + m_trace = other.m_trace; } -const char *error_already_set::what() const noexcept { - if (!m_lazy_what_completed) { +inline const char *error_fetch_and_normalize::error_string(const char *) const { + if (!m_lazy_error_string_completed) { std::string failure_info; gil_scoped_acquire gil; error_scope scope; try { std::string err_str = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); if (err_str.empty()) { - m_lazy_what += ": "; + m_lazy_error_string += ": "; } else { - m_lazy_what = err_str; // Replace completely. + m_lazy_error_string = err_str; // Replace completely. } } catch (const std::exception &e) { - m_lazy_what += ": CASCADING failure: std::exception::what(): "; + m_lazy_error_string += ": CASCADING failure: std::exception::what(): "; try { - m_lazy_what += e.what(); + m_lazy_error_string += e.what(); } catch (const std::exception &) { - m_lazy_what += "UNRECOVERABLE"; + m_lazy_error_string += "UNRECOVERABLE"; } } - m_lazy_what_completed = true; + m_lazy_error_string_completed = true; } - return m_lazy_what.c_str(); + return m_lazy_error_string.c_str(); } -PYBIND11_NAMESPACE_BEGIN(detail) inline function get_type_override(const void *this_ptr, const type_info *this_type, const char *name) { handle self = get_object_handle(this_ptr, this_type); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index e697a3634a..cf1e943fd3 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -394,51 +394,41 @@ inline const char *obj_class_name(PyObject *obj) { return Py_TYPE(obj)->tp_name; } -PYBIND11_NAMESPACE_END(detail) - -#if defined(_MSC_VER) -# pragma warning(push) -# pragma warning(disable : 4275 4251) -// warning C4275: An exported class was derived from a class that wasn't exported. -// Can be ignored when derived from a STL class. -#endif -/// Fetch and hold an error which was already set in Python. An instance of this is typically -/// 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::exception { -public: - /// Fetches the current Python exception (using PyErr_Fetch()), which will clear the - /// current Python error indicator. - error_already_set() { +struct error_fetch_and_normalize { + explicit error_fetch_and_normalize(const char *called) { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); if (!m_type) { - pybind11_fail("Internal error: pybind11::error_already_set called while " - "Python error indicator not set."); + pybind11_fail("Internal error: " + std::string(called) + + " called while " + "Python error indicator not set."); } const char *exc_type_name_orig = detail::obj_class_name(m_type.ptr()); if (exc_type_name_orig == nullptr) { - pybind11_fail("Internal error: pybind11::error_already_set failed to obtain the name " - "of the original active exception type."); + pybind11_fail("Internal error: " + std::string(called) + + " failed to obtain the name " + "of the original active exception type."); } - m_lazy_what = exc_type_name_orig; + m_lazy_error_string = exc_type_name_orig; // PyErr_NormalizeException() may change the exception type if there are cascading // failures. This can potentially be extremely confusing. PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); if (m_type.ptr() == nullptr) { - pybind11_fail("Internal error: pybind11::error_already_set failed to normalize the " - "active exception."); + pybind11_fail("Internal error: " + std::string(called) + + " failed to normalize the " + "active exception."); } const char *exc_type_name_norm = detail::obj_class_name(m_type.ptr()); if (exc_type_name_orig == nullptr) { - pybind11_fail("Internal error: pybind11::error_already_set failed to obtain the name " - "of the normalized active exception type."); + pybind11_fail("Internal error: " + std::string(called) + + " failed to obtain the name " + "of the normalized active exception type."); } - if (exc_type_name_norm != m_lazy_what) { - std::string msg = "pybind11::error_already_set: MISMATCH of original and normalized " - "active exception types: "; + if (exc_type_name_norm != m_lazy_error_string) { + std::string msg = std::string(called) + + ": MISMATCH of original and normalized " + "active exception types: "; msg += "ORIGINAL "; - msg += m_lazy_what; + msg += m_lazy_error_string; msg += " REPLACED BY "; msg += exc_type_name_norm; std::string msg2; @@ -459,44 +449,67 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { } } - /// The C++ standard explicitly prohibits deleting this copy ctor: C++17 18.1.5. + /// WARNING: This destructor needs to acquire the Python GIL. This can lead to + /// crashes (undefined behavior) if the Python interpreter is finalizing. + ~error_fetch_and_normalize(); + /// WARNING: This copy constructor needs to acquire the Python GIL. This can lead to /// crashes (undefined behavior) if the Python interpreter is finalizing. - inline error_already_set(const error_already_set &e) noexcept; + error_fetch_and_normalize(const error_fetch_and_normalize &); -#if ((defined(__clang__) && __clang_major__ >= 9) || (defined(__GNUC__) && __GNUC__ >= 10) \ - || (defined(_MSC_VER) && _MSC_VER >= 1920)) \ - && !defined(__INTEL_COMPILER) - error_already_set(error_already_set &&) noexcept = default; -#else - /// Workaround for old compilers: - /// Moving the members one-by-one to be able to specify noexcept. - 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)}, - m_lazy_what_completed{std::move(e.m_lazy_what_completed)} {} + error_fetch_and_normalize(error_fetch_and_normalize &&) = default; + + const char *error_string(const char *called) const; + + void restore() { + // As long as this type is copyable, there is no point in releasing m_type, m_value, + // 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()); + } + + bool matches(handle exc) const { + return (PyErr_GivenExceptionMatches(m_type.ptr(), exc.ptr()) != 0); + } + + object m_type, m_value, m_trace; + mutable std::string m_lazy_error_string; + mutable bool m_lazy_error_string_completed = false; +}; + +PYBIND11_NAMESPACE_END(detail) + +#if defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4275 4251) +// warning C4275: An exported class was derived from a class that wasn't exported. +// Can be ignored when derived from a STL class. #endif +/// Fetch and hold an error which was already set in Python. An instance of this is typically +/// 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::exception { +public: + /// Fetches the current Python exception (using PyErr_Fetch()), which will clear the + /// current Python error indicator. + error_already_set() : m_fetched_error{"pybind11::error_already_set"} {} - /// WARNING: This destructor needs to acquire the Python GIL. This can lead to - /// crashes (undefined behavior) if the Python interpreter is finalizing. - inline ~error_already_set() override; + /// Note: The C++ standard explicitly prohibits deleting the copy ctor: C++17 18.1.5. /// The what() result is built lazily on demand. /// WARNING: This member function needs to acquire the Python GIL. This can lead to /// crashes (undefined behavior) if the Python interpreter is finalizing. - inline const char *what() const noexcept override; + inline const char *what() const noexcept override { + return m_fetched_error.error_string("pybind11::error_already_set"); + } /// Restores the currently-held Python error (which will clear the Python error indicator first /// if already set). /// NOTE: This member function will always restore the normalized exception, which may or may /// not be the original Python exception. /// WARNING: The GIL must be held when this member function is called! - void restore() { - // As long as this type is copyable, there is no point in releasing m_type, m_value, - // 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()); - } + void restore() { m_fetched_error.restore(); } /// 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 @@ -520,18 +533,14 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// Check if the currently trapped error type matches the given Python exception class (or a /// subclass thereof). May also be passed a tuple to search for any exception class matches in /// the given tuple. - bool matches(handle exc) const { - return (PyErr_GivenExceptionMatches(m_type.ptr(), exc.ptr()) != 0); - } + bool matches(handle exc) const { return m_fetched_error.matches(exc); } - const object &type() const { return m_type; } - const object &value() const { return m_value; } - const object &trace() const { return m_trace; } + const object &type() const { return m_fetched_error.m_type; } + const object &value() const { return m_fetched_error.m_value; } + const object &trace() const { return m_fetched_error.m_trace; } private: - object m_type, m_value, m_trace; - mutable std::string m_lazy_what; - mutable bool m_lazy_what_completed = false; + detail::error_fetch_and_normalize m_fetched_error; }; #if defined(_MSC_VER) # pragma warning(pop) From ec1a2c03529041c681d3796292a965d2f281f0e1 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 03:18:24 -0700 Subject: [PATCH 107/121] Copy most of error_string() code to new error_fetch_and_normalize::complete_lazy_error_string() --- include/pybind11/pybind11.h | 76 ++++++++++++++++++++++++++++++------- include/pybind11/pytypes.h | 4 ++ 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 230976aa0e..8c9e7ed808 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2647,26 +2647,74 @@ inline error_fetch_and_normalize::error_fetch_and_normalize(const error_fetch_an m_trace = other.m_trace; } -inline const char *error_fetch_and_normalize::error_string(const char *) const { - if (!m_lazy_error_string_completed) { - std::string failure_info; - gil_scoped_acquire gil; - error_scope scope; +inline std::string error_fetch_and_normalize::complete_lazy_error_string() const { + std::string result; + if (m_value) { try { - std::string err_str = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); - if (err_str.empty()) { - m_lazy_error_string += ": "; - } else { - m_lazy_error_string = err_str; // Replace completely. - } + result = str(m_value).cast(); } catch (const std::exception &e) { - m_lazy_error_string += ": CASCADING failure: std::exception::what(): "; + result = "CASCADING failure: std::exception::what(): "; try { - m_lazy_error_string += e.what(); + result += e.what(); } catch (const std::exception &) { - m_lazy_error_string += "UNRECOVERABLE"; + result += "UNRECOVERABLE"; } } + } else { + result += ""; + } + if (result.empty()) { + result = ""; + } + + if (m_trace) { +#if !defined(PYPY_VERSION) + auto *tb = reinterpret_cast(m_trace.ptr()); + + // Get the deepest trace possible. + while (tb->tb_next) { + tb = tb->tb_next; + } + + PyFrameObject *frame = tb->tb_frame; + Py_XINCREF(frame); + result += "\n\nAt:\n"; + while (frame) { +# if PY_VERSION_HEX >= 0x030900B1 + PyCodeObject *f_code = PyFrame_GetCode(frame); +# else + PyCodeObject *f_code = frame->f_code; + Py_INCREF(f_code); +# endif + int lineno = PyFrame_GetLineNumber(frame); + result += " "; + result += handle(f_code->co_filename).cast(); + result += '('; + result += std::to_string(lineno); + result += "): "; + result += handle(f_code->co_name).cast(); + result += '\n'; + Py_DECREF(f_code); +# if PY_VERSION_HEX >= 0x030900B1 + auto *b_frame = PyFrame_GetBack(frame); +# else + auto *b_frame = frame->f_back; + Py_XINCREF(b_frame); +# endif + Py_DECREF(frame); + frame = b_frame; + } +#endif //! defined(PYPY_VERSION) + } + return ": " + result; +} + +inline const char *error_fetch_and_normalize::error_string(const char *) const { + if (!m_lazy_error_string_completed) { + std::string failure_info; + gil_scoped_acquire gil; + error_scope scope; + m_lazy_error_string += complete_lazy_error_string(); m_lazy_error_string_completed = true; } return m_lazy_error_string.c_str(); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index cf1e943fd3..b0f8b0a7e6 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -459,6 +459,10 @@ struct error_fetch_and_normalize { error_fetch_and_normalize(error_fetch_and_normalize &&) = default; +private: + std::string complete_lazy_error_string() const; + +public: const char *error_string(const char *called) const; void restore() { From c39ebd5137ce0d1506841e221ab7c88f73047389 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 03:56:01 -0700 Subject: [PATCH 108/121] Remove all error_string() code from detail/type_caster_base.h. Note that this commit includes a subtle bug fix: previously error_string() restored the Python error, which will upset pybind11_fail(). This never was a problem in practice because the two PyType_Ready() calls in detail/class.h do not usually fail. --- include/pybind11/detail/class.h | 6 +- include/pybind11/detail/type_caster_base.h | 69 ---------------------- include/pybind11/pybind11.h | 10 +++- include/pybind11/pytypes.h | 24 +------- tests/test_exceptions.py | 11 ++-- 5 files changed, 19 insertions(+), 101 deletions(-) diff --git a/include/pybind11/detail/class.h b/include/pybind11/detail/class.h index e52ec1d3e6..1ddf1393bb 100644 --- a/include/pybind11/detail/class.h +++ b/include/pybind11/detail/class.h @@ -455,6 +455,8 @@ extern "C" inline void pybind11_object_dealloc(PyObject *self) { #endif } +std::string error_string(); + /** Create the type which can be used as a common base for all classes. This is needed in order to satisfy Python's requirements for multiple inheritance. Return value: New reference. */ @@ -490,7 +492,7 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) { type->tp_weaklistoffset = offsetof(instance, weakrefs); if (PyType_Ready(type) < 0) { - pybind11_fail("PyType_Ready failed in make_object_base_type():" + error_string()); + pybind11_fail("PyType_Ready failed in make_object_base_type(): " + error_string()); } setattr((PyObject *) type, "__module__", str("pybind11_builtins")); @@ -707,7 +709,7 @@ inline PyObject *make_new_python_type(const type_record &rec) { } if (PyType_Ready(type) < 0) { - pybind11_fail(std::string(rec.name) + ": PyType_Ready failed (" + error_string() + ")!"); + pybind11_fail(std::string(rec.name) + ": PyType_Ready failed: " + error_string()); } assert(!rec.dynamic_attr || PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC)); diff --git a/include/pybind11/detail/type_caster_base.h b/include/pybind11/detail/type_caster_base.h index 85f7bbacec..21f69c289f 100644 --- a/include/pybind11/detail/type_caster_base.h +++ b/include/pybind11/detail/type_caster_base.h @@ -470,75 +470,6 @@ PYBIND11_NOINLINE bool isinstance_generic(handle obj, const std::type_info &tp) return isinstance(obj, type); } -PYBIND11_NOINLINE std::string -error_string(PyObject *exc_type, PyObject *exc_value, PyObject *exc_trace) { - auto exc_type_norm = reinterpret_borrow(exc_type); - auto exc_value_norm = reinterpret_borrow(exc_value); - auto exc_trace_norm = reinterpret_borrow(exc_trace); - PyErr_NormalizeException(&exc_type_norm.ptr(), &exc_value_norm.ptr(), &exc_trace_norm.ptr()); - if (exc_trace_norm) { - PyException_SetTraceback(exc_value_norm.ptr(), exc_trace_norm.ptr()); - } - - auto result = exc_type_norm.attr("__name__").cast(); - result += ": "; - - if (exc_value_norm) { - result += str(exc_value_norm).cast(); - } - - if (exc_trace_norm) { -#if !defined(PYPY_VERSION) - auto *tb = reinterpret_cast(exc_trace_norm.ptr()); - - // Get the deepest trace possible. - while (tb->tb_next) { - tb = tb->tb_next; - } - - PyFrameObject *frame = tb->tb_frame; - Py_XINCREF(frame); - result += "\n\nAt:\n"; - while (frame) { -# if PY_VERSION_HEX >= 0x030900B1 - PyCodeObject *f_code = PyFrame_GetCode(frame); -# else - PyCodeObject *f_code = frame->f_code; - Py_INCREF(f_code); -# endif - int lineno = PyFrame_GetLineNumber(frame); - result += " "; - result += handle(f_code->co_filename).cast(); - result += '('; - result += std::to_string(lineno); - result += "): "; - result += handle(f_code->co_name).cast(); - result += '\n'; - Py_DECREF(f_code); -# if PY_VERSION_HEX >= 0x030900B1 - auto *b_frame = PyFrame_GetBack(frame); -# else - auto *b_frame = frame->f_back; - Py_XINCREF(b_frame); -# endif - Py_DECREF(frame); - frame = b_frame; - } -#endif //! defined(PYPY_VERSION) - } - - return result; -} - -PYBIND11_NOINLINE std::string error_string() { - error_scope scope; // Fetch error state (will be restored when this function returns). - if (scope.type == nullptr) { - pybind11_fail("Internal error: pybind11::detail::error_string() called while Python error " - "indicator not set."); - } - return error_string(scope.type, scope.value, scope.trace); -} - PYBIND11_NOINLINE handle get_object_handle(const void *ptr, const detail::type_info *type) { auto &instances = get_internals().registered_instances; auto range = instances.equal_range(ptr); diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 8c9e7ed808..e5f8770753 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2706,20 +2706,24 @@ inline std::string error_fetch_and_normalize::complete_lazy_error_string() const } #endif //! defined(PYPY_VERSION) } - return ": " + result; + return result; } -inline const char *error_fetch_and_normalize::error_string(const char *) const { +inline const char *error_fetch_and_normalize::error_string() const { if (!m_lazy_error_string_completed) { std::string failure_info; gil_scoped_acquire gil; error_scope scope; - m_lazy_error_string += complete_lazy_error_string(); + m_lazy_error_string += ": " + complete_lazy_error_string(); m_lazy_error_string_completed = true; } return m_lazy_error_string.c_str(); } +inline std::string error_string() { + return error_fetch_and_normalize("pybind11::detail::error_string").error_string(); +} + inline function get_type_override(const void *this_ptr, const type_info *this_type, const char *name) { handle self = get_object_handle(this_ptr, this_type); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index b0f8b0a7e6..81cacab476 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -383,9 +383,6 @@ 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) { @@ -431,20 +428,7 @@ struct error_fetch_and_normalize { msg += m_lazy_error_string; msg += " REPLACED BY "; msg += exc_type_name_norm; - std::string msg2; - try { - msg2 = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr()); - } catch (const std::exception &e) { - msg2 = "CASCADING failure: std::exception::what(): "; - try { - msg2 += e.what(); - } catch (const std::exception &) { - msg2 += "UNRECOVERABLE"; - } - } - if (!msg2.empty()) { - msg += ":\n" + msg2; - } + msg += ": " + complete_lazy_error_string(); pybind11_fail(msg); } } @@ -463,7 +447,7 @@ struct error_fetch_and_normalize { std::string complete_lazy_error_string() const; public: - const char *error_string(const char *called) const; + const char *error_string() const; void restore() { // As long as this type is copyable, there is no point in releasing m_type, m_value, @@ -504,9 +488,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// The what() result is built lazily on demand. /// WARNING: This member function needs to acquire the Python GIL. This can lead to /// crashes (undefined behavior) if the Python interpreter is finalizing. - inline const char *what() const noexcept override { - return m_fetched_error.error_string("pybind11::error_already_set"); - } + inline const char *what() const noexcept override { return m_fetched_error.error_string(); } /// Restores the currently-held Python error (which will clear the Python error indicator first /// if already set). diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index fd83f48dbb..7ebe8286f7 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -314,17 +314,16 @@ def test_flaky_exception_failure_point_init(): m.error_already_set_what(FlakyException, ("failure_point_init",)) lines = str(excinfo.value).splitlines() # PyErr_NormalizeException replaces the original FlakyException with ValueError: - assert lines[:4] == [ + assert lines[:3] == [ "pybind11::error_already_set: MISMATCH of original and normalized active exception types:" - " ORIGINAL FlakyException REPLACED BY ValueError:", - "ValueError: triggered_failure_point_init", + " ORIGINAL FlakyException REPLACED BY ValueError: triggered_failure_point_init", "", "At:", ] # Checking the first two lines of the traceback as formatted in error_string(): - assert "test_exceptions.py(" in lines[4] - assert lines[4].endswith("): __init__") - assert lines[5].endswith("): test_flaky_exception_failure_point_init") + assert "test_exceptions.py(" in lines[3] + assert lines[3].endswith("): __init__") + assert lines[4].endswith("): test_flaky_exception_failure_point_init") def test_flaky_exception_failure_point_str(): From 286023b7fe26ddb24e6e088a1a8b537dffa44bb5 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 04:08:24 -0700 Subject: [PATCH 109/121] Return const std::string& instead of const char * and move error_string() to pytypes.h --- include/pybind11/pybind11.h | 8 ++------ include/pybind11/pytypes.h | 10 ++++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index e5f8770753..2e3e348479 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2709,7 +2709,7 @@ inline std::string error_fetch_and_normalize::complete_lazy_error_string() const return result; } -inline const char *error_fetch_and_normalize::error_string() const { +inline std::string const &error_fetch_and_normalize::error_string() const { if (!m_lazy_error_string_completed) { std::string failure_info; gil_scoped_acquire gil; @@ -2717,11 +2717,7 @@ inline const char *error_fetch_and_normalize::error_string() const { m_lazy_error_string += ": " + complete_lazy_error_string(); m_lazy_error_string_completed = true; } - return m_lazy_error_string.c_str(); -} - -inline std::string error_string() { - return error_fetch_and_normalize("pybind11::detail::error_string").error_string(); + return m_lazy_error_string; } inline function diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 81cacab476..4a94e5ffab 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -447,7 +447,7 @@ struct error_fetch_and_normalize { std::string complete_lazy_error_string() const; public: - const char *error_string() const; + std::string const &error_string() const; void restore() { // As long as this type is copyable, there is no point in releasing m_type, m_value, @@ -465,6 +465,10 @@ struct error_fetch_and_normalize { mutable bool m_lazy_error_string_completed = false; }; +inline std::string error_string() { + return error_fetch_and_normalize("pybind11::detail::error_string").error_string(); +} + PYBIND11_NAMESPACE_END(detail) #if defined(_MSC_VER) @@ -488,7 +492,9 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// The what() result is built lazily on demand. /// WARNING: This member function needs to acquire the Python GIL. This can lead to /// crashes (undefined behavior) if the Python interpreter is finalizing. - inline const char *what() const noexcept override { return m_fetched_error.error_string(); } + inline const char *what() const noexcept override { + return m_fetched_error.error_string().c_str(); + } /// Restores the currently-held Python error (which will clear the Python error indicator first /// if already set). From 156b57b3b4c2f688f25c4910b746eb646c3164f6 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 09:54:53 -0700 Subject: [PATCH 110/121] Remove gil_scope_acquire from error_fetch_and_normalize, add back to error_already_set --- include/pybind11/pybind11.h | 55 +++++++++++++++++-------------------- include/pybind11/pytypes.h | 39 +++++++++++++++----------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 2e3e348479..6a458aa1e9 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2627,30 +2627,11 @@ void print(Args &&...args) { PYBIND11_NAMESPACE_BEGIN(detail) -inline error_fetch_and_normalize::~error_fetch_and_normalize() { - if (!(m_type || m_value || m_trace)) { - return; // Avoid gil and scope overhead if there is nothing to release. - } - gil_scoped_acquire gil; - error_scope scope; - m_type.release().dec_ref(); - m_value.release().dec_ref(); - m_trace.release().dec_ref(); -} - -inline error_fetch_and_normalize::error_fetch_and_normalize(const error_fetch_and_normalize &other) - : m_lazy_error_string{other.m_lazy_error_string}, m_lazy_error_string_completed{ - other.m_lazy_error_string_completed} { - gil_scoped_acquire gil; - m_type = other.m_type; - m_value = other.m_value; - m_trace = other.m_trace; -} - -inline std::string error_fetch_and_normalize::complete_lazy_error_string() const { +PYBIND11_NOINLINE std::string error_fetch_and_normalize::complete_lazy_error_string() const { std::string result; if (m_value) { try { + // TODO: use C API directly for robustness. result = str(m_value).cast(); } catch (const std::exception &e) { result = "CASCADING failure: std::exception::what(): "; @@ -2661,7 +2642,7 @@ inline std::string error_fetch_and_normalize::complete_lazy_error_string() const } } } else { - result += ""; + result = ""; } if (result.empty()) { result = ""; @@ -2709,17 +2690,31 @@ inline std::string error_fetch_and_normalize::complete_lazy_error_string() const return result; } -inline std::string const &error_fetch_and_normalize::error_string() const { - if (!m_lazy_error_string_completed) { - std::string failure_info; - gil_scoped_acquire gil; - error_scope scope; - m_lazy_error_string += ": " + complete_lazy_error_string(); - m_lazy_error_string_completed = true; +PYBIND11_NAMESPACE_END(detail) + +inline error_already_set::~error_already_set() { + auto e = m_fetched_error; + if (!(e.m_type || e.m_value || e.m_trace)) { + return; // Avoid gil and scope overhead if there is nothing to release. } - return m_lazy_error_string; + gil_scoped_acquire gil; + error_scope scope; + e.m_type.release().dec_ref(); + e.m_value.release().dec_ref(); + e.m_trace.release().dec_ref(); } +inline error_already_set::error_already_set(const error_already_set &other) + : m_fetched_error(gil_scoped_acquire(), other.m_fetched_error) {} + +inline const char *error_already_set::what() const noexcept { + gil_scoped_acquire gil; + error_scope scope; + return m_fetched_error.error_string().c_str(); +} + +PYBIND11_NAMESPACE_BEGIN(detail) + inline function get_type_override(const void *this_ptr, const type_info *this_type, const char *name) { handle self = get_object_handle(this_ptr, this_type); diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 4a94e5ffab..bd2e2fdd12 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -433,21 +433,21 @@ struct error_fetch_and_normalize { } } - /// WARNING: This destructor needs to acquire the Python GIL. This can lead to - /// crashes (undefined behavior) if the Python interpreter is finalizing. - ~error_fetch_and_normalize(); - - /// WARNING: This copy constructor needs to acquire the Python GIL. This can lead to - /// crashes (undefined behavior) if the Python interpreter is finalizing. - error_fetch_and_normalize(const error_fetch_and_normalize &); + template + error_fetch_and_normalize(const GilScopedAcquire &, const error_fetch_and_normalize &other) + : m_type{other.m_type}, m_value{other.m_value}, m_trace{other.m_trace}, + m_lazy_error_string{other.m_lazy_error_string}, + m_lazy_error_string_completed{other.m_lazy_error_string_completed} {} - error_fetch_and_normalize(error_fetch_and_normalize &&) = default; - -private: std::string complete_lazy_error_string() const; -public: - std::string const &error_string() const; + std::string const &error_string() const { + if (!m_lazy_error_string_completed) { + m_lazy_error_string += ": " + complete_lazy_error_string(); + m_lazy_error_string_completed = true; + } + return m_lazy_error_string; + } void restore() { // As long as this type is copyable, there is no point in releasing m_type, m_value, @@ -487,14 +487,21 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// current Python error indicator. error_already_set() : m_fetched_error{"pybind11::error_already_set"} {} - /// Note: The C++ standard explicitly prohibits deleting the copy ctor: C++17 18.1.5. + /// WARNING: This destructor needs to acquire the Python GIL. This can lead to + /// crashes (undefined behavior) if the Python interpreter is finalizing. + ~error_already_set(); + + /// The C++ standard explicitly prohibits deleting this copy ctor: C++17 18.1.5. + /// WARNING: This copy constructor needs to acquire the Python GIL. This can lead to + /// crashes (undefined behavior) if the Python interpreter is finalizing. + error_already_set(const error_already_set &); + + error_already_set(error_already_set &&) = default; /// The what() result is built lazily on demand. /// WARNING: This member function needs to acquire the Python GIL. This can lead to /// crashes (undefined behavior) if the Python interpreter is finalizing. - inline const char *what() const noexcept override { - return m_fetched_error.error_string().c_str(); - } + const char *what() const noexcept override; /// Restores the currently-held Python error (which will clear the Python error indicator first /// if already set). From 6f175ac0cf9d100ddca6c1e45aa5d38e701ab351 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 10:30:20 -0700 Subject: [PATCH 111/121] Better handling of FlakyException __str__ failure. --- include/pybind11/pybind11.h | 30 +++++++++++++++++++----------- tests/test_exceptions.py | 11 +++++++---- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 6a458aa1e9..4e6e798f0c 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2629,25 +2629,23 @@ PYBIND11_NAMESPACE_BEGIN(detail) PYBIND11_NOINLINE std::string error_fetch_and_normalize::complete_lazy_error_string() const { std::string result; + std::string message_error_string; if (m_value) { - try { - // TODO: use C API directly for robustness. - result = str(m_value).cast(); - } catch (const std::exception &e) { - result = "CASCADING failure: std::exception::what(): "; - try { - result += e.what(); - } catch (const std::exception &) { - result += "UNRECOVERABLE"; - } + auto value_str = reinterpret_borrow(PyObject_Str(m_value.ptr())); + if (!value_str) { + message_error_string = detail::error_string(); + result = ""; + } else { + result = value_str.cast(); } } else { - result = ""; + result = ""; } if (result.empty()) { result = ""; } + bool have_trace = false; if (m_trace) { #if !defined(PYPY_VERSION) auto *tb = reinterpret_cast(m_trace.ptr()); @@ -2685,8 +2683,18 @@ PYBIND11_NOINLINE std::string error_fetch_and_normalize::complete_lazy_error_str Py_DECREF(frame); frame = b_frame; } + + have_trace = true; #endif //! defined(PYPY_VERSION) } + + if (!message_error_string.empty()) { + if (!have_trace) { + result += '\n'; + } + result += "\nMESSAGE UNAVAILABLE DUE TO EXCEPTION: " + message_error_string; + } + return result; } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 7ebe8286f7..392a3ad3ed 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -332,10 +332,13 @@ def test_flaky_exception_failure_point_str(): ) assert not py_err_set_after_what lines = what.splitlines() - assert ( - lines[0] - == "FlakyException: CASCADING failure: std::exception::what(): ValueError: triggered_failure_point_str" - ) + assert lines[:5] == [ + "FlakyException: ", + "", + "MESSAGE UNAVAILABLE DUE TO EXCEPTION: ValueError: triggered_failure_point_str", + "", + "At:", + ] def test_cross_module_interleaved_error_already_set(): From 71f33a868246df6f94c77b46af890f6c56f285f8 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 10:39:07 -0700 Subject: [PATCH 112/121] Move error_fetch_and_normalize::complete_lazy_error_string() implementation from pybind11.h to pytypes.h --- include/pybind11/pybind11.h | 75 ------------------------------------- include/pybind11/pytypes.h | 73 +++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 76 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 4e6e798f0c..e4bdfdce63 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2625,81 +2625,6 @@ void print(Args &&...args) { detail::print(c.args(), c.kwargs()); } -PYBIND11_NAMESPACE_BEGIN(detail) - -PYBIND11_NOINLINE std::string error_fetch_and_normalize::complete_lazy_error_string() const { - std::string result; - std::string message_error_string; - if (m_value) { - auto value_str = reinterpret_borrow(PyObject_Str(m_value.ptr())); - if (!value_str) { - message_error_string = detail::error_string(); - result = ""; - } else { - result = value_str.cast(); - } - } else { - result = ""; - } - if (result.empty()) { - result = ""; - } - - bool have_trace = false; - if (m_trace) { -#if !defined(PYPY_VERSION) - auto *tb = reinterpret_cast(m_trace.ptr()); - - // Get the deepest trace possible. - while (tb->tb_next) { - tb = tb->tb_next; - } - - PyFrameObject *frame = tb->tb_frame; - Py_XINCREF(frame); - result += "\n\nAt:\n"; - while (frame) { -# if PY_VERSION_HEX >= 0x030900B1 - PyCodeObject *f_code = PyFrame_GetCode(frame); -# else - PyCodeObject *f_code = frame->f_code; - Py_INCREF(f_code); -# endif - int lineno = PyFrame_GetLineNumber(frame); - result += " "; - result += handle(f_code->co_filename).cast(); - result += '('; - result += std::to_string(lineno); - result += "): "; - result += handle(f_code->co_name).cast(); - result += '\n'; - Py_DECREF(f_code); -# if PY_VERSION_HEX >= 0x030900B1 - auto *b_frame = PyFrame_GetBack(frame); -# else - auto *b_frame = frame->f_back; - Py_XINCREF(b_frame); -# endif - Py_DECREF(frame); - frame = b_frame; - } - - have_trace = true; -#endif //! defined(PYPY_VERSION) - } - - if (!message_error_string.empty()) { - if (!have_trace) { - result += '\n'; - } - result += "\nMESSAGE UNAVAILABLE DUE TO EXCEPTION: " + message_error_string; - } - - return result; -} - -PYBIND11_NAMESPACE_END(detail) - inline error_already_set::~error_already_set() { auto e = m_fetched_error; if (!(e.m_type || e.m_value || e.m_trace)) { diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index bd2e2fdd12..c5b5e748a5 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -391,6 +391,8 @@ inline const char *obj_class_name(PyObject *obj) { return Py_TYPE(obj)->tp_name; } +std::string error_string(); + struct error_fetch_and_normalize { explicit error_fetch_and_normalize(const char *called) { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); @@ -439,7 +441,76 @@ struct error_fetch_and_normalize { m_lazy_error_string{other.m_lazy_error_string}, m_lazy_error_string_completed{other.m_lazy_error_string_completed} {} - std::string complete_lazy_error_string() const; + std::string complete_lazy_error_string() const { + std::string result; + std::string message_error_string; + if (m_value) { + auto value_str = reinterpret_borrow(PyObject_Str(m_value.ptr())); + if (!value_str) { + message_error_string = detail::error_string(); + result = ""; + } else { + result = value_str.cast(); + } + } else { + result = ""; + } + if (result.empty()) { + result = ""; + } + + bool have_trace = false; + if (m_trace) { +#if !defined(PYPY_VERSION) + auto *tb = reinterpret_cast(m_trace.ptr()); + + // Get the deepest trace possible. + while (tb->tb_next) { + tb = tb->tb_next; + } + + PyFrameObject *frame = tb->tb_frame; + Py_XINCREF(frame); + result += "\n\nAt:\n"; + while (frame) { +# if PY_VERSION_HEX >= 0x030900B1 + PyCodeObject *f_code = PyFrame_GetCode(frame); +# else + PyCodeObject *f_code = frame->f_code; + Py_INCREF(f_code); +# endif + int lineno = PyFrame_GetLineNumber(frame); + result += " "; + result += handle(f_code->co_filename).cast(); + result += '('; + result += std::to_string(lineno); + result += "): "; + result += handle(f_code->co_name).cast(); + result += '\n'; + Py_DECREF(f_code); +# if PY_VERSION_HEX >= 0x030900B1 + auto *b_frame = PyFrame_GetBack(frame); +# else + auto *b_frame = frame->f_back; + Py_XINCREF(b_frame); +# endif + Py_DECREF(frame); + frame = b_frame; + } + + have_trace = true; +#endif //! defined(PYPY_VERSION) + } + + if (!message_error_string.empty()) { + if (!have_trace) { + result += '\n'; + } + result += "\nMESSAGE UNAVAILABLE DUE TO EXCEPTION: " + message_error_string; + } + + return result; + } std::string const &error_string() const { if (!m_lazy_error_string_completed) { From 0739d4c04120488a9901670dcecd2c536ea65586 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 10:55:20 -0700 Subject: [PATCH 113/121] Add error_fetch_and_normalize::release_py_object_references() and use from error_already_set dtor. --- include/pybind11/pybind11.h | 7 ++----- include/pybind11/pytypes.h | 8 ++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index e4bdfdce63..cef8f38c9f 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2626,15 +2626,12 @@ void print(Args &&...args) { } inline error_already_set::~error_already_set() { - auto e = m_fetched_error; - if (!(e.m_type || e.m_value || e.m_trace)) { + if (!m_fetched_error.has_py_object_references()) { return; // Avoid gil and scope overhead if there is nothing to release. } gil_scoped_acquire gil; error_scope scope; - e.m_type.release().dec_ref(); - e.m_value.release().dec_ref(); - e.m_trace.release().dec_ref(); + m_fetched_error.release_py_object_references(); } inline error_already_set::error_already_set(const error_already_set &other) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index c5b5e748a5..cf46a136c1 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -531,6 +531,14 @@ struct error_fetch_and_normalize { return (PyErr_GivenExceptionMatches(m_type.ptr(), exc.ptr()) != 0); } + bool has_py_object_references() const { return m_type || m_value || m_trace; } + + void release_py_object_references() { + m_type.release().dec_ref(); + m_value.release().dec_ref(); + m_trace.release().dec_ref(); + } + object m_type, m_value, m_trace; mutable std::string m_lazy_error_string; mutable bool m_lazy_error_string_completed = false; From 80dfaea1b93ae2222f079d95e9d740d8089bd2c1 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 12:01:40 -0700 Subject: [PATCH 114/121] Use shared_ptr for m_fetched_error => 1. non-racy, copy ctor that does not need the GIL; 2. enables guard against duplicate restore() calls. --- include/pybind11/pybind11.h | 9 +++----- include/pybind11/pytypes.h | 46 ++++++++++++++++++++----------------- tests/test_exceptions.cpp | 28 ++++++++-------------- tests/test_exceptions.py | 15 ++++++++---- 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index cef8f38c9f..1f7a0631f4 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2626,21 +2626,18 @@ void print(Args &&...args) { } inline error_already_set::~error_already_set() { - if (!m_fetched_error.has_py_object_references()) { + if (m_fetched_error.use_count() != 1 || !m_fetched_error->has_py_object_references()) { return; // Avoid gil and scope overhead if there is nothing to release. } gil_scoped_acquire gil; error_scope scope; - m_fetched_error.release_py_object_references(); + m_fetched_error->release_py_object_references(); } -inline error_already_set::error_already_set(const error_already_set &other) - : m_fetched_error(gil_scoped_acquire(), other.m_fetched_error) {} - inline const char *error_already_set::what() const noexcept { gil_scoped_acquire gil; error_scope scope; - return m_fetched_error.error_string().c_str(); + return m_fetched_error->error_string().c_str(); } PYBIND11_NAMESPACE_BEGIN(detail) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index cf46a136c1..7106c1fc0b 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -435,11 +435,8 @@ struct error_fetch_and_normalize { } } - template - error_fetch_and_normalize(const GilScopedAcquire &, const error_fetch_and_normalize &other) - : m_type{other.m_type}, m_value{other.m_value}, m_trace{other.m_trace}, - m_lazy_error_string{other.m_lazy_error_string}, - m_lazy_error_string_completed{other.m_lazy_error_string_completed} {} + error_fetch_and_normalize(const error_fetch_and_normalize &) = delete; + error_fetch_and_normalize(error_fetch_and_normalize &&) = delete; std::string complete_lazy_error_string() const { std::string result; @@ -521,10 +518,13 @@ struct error_fetch_and_normalize { } void restore() { - // As long as this type is copyable, there is no point in releasing m_type, m_value, - // m_trace, but simply holding on the the references makes it possible to produce - // what() even after restore(). + if (m_restore_called) { + pybind11_fail("Internal error: pybind11::detail::error_fetch_and_normalize::restore() " + "called a second time. ORIGINAL ERROR: " + + error_string()); + } PyErr_Restore(m_type.inc_ref().ptr(), m_value.inc_ref().ptr(), m_trace.inc_ref().ptr()); + m_restore_called = true; } bool matches(handle exc) const { @@ -542,6 +542,7 @@ struct error_fetch_and_normalize { object m_type, m_value, m_trace; mutable std::string m_lazy_error_string; mutable bool m_lazy_error_string_completed = false; + mutable bool m_restore_called = false; }; inline std::string error_string() { @@ -564,18 +565,21 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { public: /// Fetches the current Python exception (using PyErr_Fetch()), which will clear the /// current Python error indicator. - error_already_set() : m_fetched_error{"pybind11::error_already_set"} {} + error_already_set() + : m_fetched_error{ + std::make_shared("pybind11::error_already_set")} {} /// WARNING: This destructor needs to acquire the Python GIL. This can lead to /// crashes (undefined behavior) if the Python interpreter is finalizing. - ~error_already_set(); + ~error_already_set() override; - /// The C++ standard explicitly prohibits deleting this copy ctor: C++17 18.1.5. - /// WARNING: This copy constructor needs to acquire the Python GIL. This can lead to - /// crashes (undefined behavior) if the Python interpreter is finalizing. - error_already_set(const error_already_set &); + // This copy ctor does not need the GIL because it simply increments a shared_ptr use_count. + error_already_set(const error_already_set &) noexcept = default; - error_already_set(error_already_set &&) = default; + // This move ctor cannot easily be deleted (some compilers need it). + // It is the responsibility of the caller to not use the moved-from object. + // For simplicity, guarding ifs are omitted. + error_already_set(error_already_set &&) noexcept = default; /// The what() result is built lazily on demand. /// WARNING: This member function needs to acquire the Python GIL. This can lead to @@ -587,7 +591,7 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// NOTE: This member function will always restore the normalized exception, which may or may /// not be the original Python exception. /// WARNING: The GIL must be held when this member function is called! - void restore() { m_fetched_error.restore(); } + void restore() { m_fetched_error->restore(); } /// 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 @@ -611,14 +615,14 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// Check if the currently trapped error type matches the given Python exception class (or a /// subclass thereof). May also be passed a tuple to search for any exception class matches in /// the given tuple. - bool matches(handle exc) const { return m_fetched_error.matches(exc); } + bool matches(handle exc) const { return m_fetched_error->matches(exc); } - const object &type() const { return m_fetched_error.m_type; } - const object &value() const { return m_fetched_error.m_value; } - const object &trace() const { return m_fetched_error.m_trace; } + const object &type() const { return m_fetched_error->m_type; } + const object &value() const { return m_fetched_error->m_value; } + const object &trace() const { return m_fetched_error->m_trace; } private: - detail::error_fetch_and_normalize m_fetched_error; + std::shared_ptr m_fetched_error; }; #if defined(_MSC_VER) # pragma warning(pop) diff --git a/tests/test_exceptions.cpp b/tests/test_exceptions.cpp index 1fc2e0ee86..3ec999d1dc 100644 --- a/tests/test_exceptions.cpp +++ b/tests/test_exceptions.cpp @@ -310,24 +310,6 @@ TEST_SUBMODULE(exceptions, m) { } }); - m.def("move_error_already_set", [](bool use_move) { - try { - PyErr_SetString(PyExc_RuntimeError, use_move ? "To be moved." : "To be copied."); - throw py::error_already_set(); - } catch (py::error_already_set &caught) { - if (use_move) { - py::error_already_set moved_to{std::move(caught)}; - return std::string(moved_to.what()); // Both destructors run. - } - // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) - py::error_already_set copied_to{caught}; - return std::string(copied_to.what()); // Both destructors run. - } -#if !defined(_MSC_VER) // MSVC detects that this is unreachable. - return std::string("Unreachable."); -#endif - }); - m.def("error_already_set_what", [](const py::object &exc_type, const py::object &exc_value) { PyErr_SetObject(exc_type.ptr(), exc_value.ptr()); std::string what = py::error_already_set().what(); @@ -342,4 +324,14 @@ TEST_SUBMODULE(exceptions, m) { = reinterpret_cast(PyLong_AsVoidPtr(cm.attr("funcaddr").ptr())); interleaved_error_already_set(); }); + + m.def("test_error_already_set_double_restore", [](bool dry_run) { + PyErr_SetString(PyExc_ValueError, "Random error."); + py::error_already_set e; + e.restore(); + PyErr_Clear(); + if (!dry_run) { + e.restore(); + } + }); } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 392a3ad3ed..639fc05fca 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -275,11 +275,6 @@ def test_local_translator(msg): assert msg(excinfo.value) == "this mod" -@pytest.mark.parametrize("use_move, expected", ((False, "copied."), (True, "moved."))) -def test_error_already_set_copy_move(use_move, expected): - assert m.move_error_already_set(use_move) == "RuntimeError: To be " + expected - - class FlakyException(Exception): def __init__(self, failure_point): if failure_point == "failure_point_init": @@ -348,3 +343,13 @@ def test_cross_module_interleaved_error_already_set(): "2nd error.", # Almost all platforms. "RuntimeError: 2nd error.", # Some PyPy builds (seen under macOS). ) + + +def test_error_already_set_double_restore(): + m.test_error_already_set_double_restore(True) # dry_run + with pytest.raises(RuntimeError) as excinfo: + m.test_error_already_set_double_restore(False) + assert str(excinfo.value) == ( + "Internal error: pybind11::detail::error_fetch_and_normalize::restore()" + " called a second time. ORIGINAL ERROR: ValueError: Random error." + ) From a7ba693a73cfac7e950cbb1cacb5141b25c5cb75 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 12:57:02 -0700 Subject: [PATCH 115/121] Add comments. --- include/pybind11/pytypes.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 7106c1fc0b..6d8f75a801 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -394,6 +394,12 @@ inline const char *obj_class_name(PyObject *obj) { std::string error_string(); struct error_fetch_and_normalize { + // Immediate normalization is long-established behavior (starting with + // https://github.com/pybind/pybind11/commit/135ba8deafb8bf64a15b24d1513899eb600e2011 + // from Sep 2016) and safest. Normalization could be deferred, but this could mask + // errors elsewhere, the performance gain is very minor in typical situations + // (usually the dominant bottleneck is EH unwinding), and the implementation here + // would be more complex. explicit error_fetch_and_normalize(const char *called) { PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr()); if (!m_type) { @@ -539,7 +545,11 @@ struct error_fetch_and_normalize { m_trace.release().dec_ref(); } + // Not protecting these for simplicity. object m_type, m_value, m_trace; + +private: + // Only protecting invariants. mutable std::string m_lazy_error_string; mutable bool m_lazy_error_string_completed = false; mutable bool m_restore_called = false; From 9854589e607c85741bd1e95101bc69a087b39a15 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 13:02:24 -0700 Subject: [PATCH 116/121] Trivial renaming of a newly introduced member function. --- include/pybind11/pytypes.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 6d8f75a801..c36c0f4368 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -436,7 +436,7 @@ struct error_fetch_and_normalize { msg += m_lazy_error_string; msg += " REPLACED BY "; msg += exc_type_name_norm; - msg += ": " + complete_lazy_error_string(); + msg += ": " + format_value_and_trace(); pybind11_fail(msg); } } @@ -444,7 +444,7 @@ struct error_fetch_and_normalize { error_fetch_and_normalize(const error_fetch_and_normalize &) = delete; error_fetch_and_normalize(error_fetch_and_normalize &&) = delete; - std::string complete_lazy_error_string() const { + std::string format_value_and_trace() const { std::string result; std::string message_error_string; if (m_value) { @@ -517,7 +517,7 @@ struct error_fetch_and_normalize { std::string const &error_string() const { if (!m_lazy_error_string_completed) { - m_lazy_error_string += ": " + complete_lazy_error_string(); + m_lazy_error_string += ": " + format_value_and_trace(); m_lazy_error_string_completed = true; } return m_lazy_error_string; From 0e50c459b2b58131bc9470e1fb2e95459dfb2040 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 13:30:12 -0700 Subject: [PATCH 117/121] Workaround for PyPy --- tests/test_exceptions.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 639fc05fca..a5984a142f 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -327,13 +327,20 @@ def test_flaky_exception_failure_point_str(): ) assert not py_err_set_after_what lines = what.splitlines() - assert lines[:5] == [ - "FlakyException: ", - "", - "MESSAGE UNAVAILABLE DUE TO EXCEPTION: ValueError: triggered_failure_point_str", - "", - "At:", - ] + if env.PYPY and len(lines) == 3: + n = 3 # Traceback is missing. + else: + n = 5 + assert ( + lines[:n] + == [ + "FlakyException: ", + "", + "MESSAGE UNAVAILABLE DUE TO EXCEPTION: ValueError: triggered_failure_point_str", + "", + "At:", + ][:n] + ) def test_cross_module_interleaved_error_already_set(): From 4e06fb1cd48a24943598a786fc86d375fcdc7f2b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Wed, 1 Jun 2022 15:20:41 -0700 Subject: [PATCH 118/121] Bug fix (oversight). Only valgrind got this one. --- include/pybind11/pytypes.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 2a8d1d859a..78c4dd3b79 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -452,7 +452,7 @@ struct error_fetch_and_normalize { std::string result; std::string message_error_string; if (m_value) { - auto value_str = reinterpret_borrow(PyObject_Str(m_value.ptr())); + auto value_str = reinterpret_steal(PyObject_Str(m_value.ptr())); if (!value_str) { message_error_string = detail::error_string(); result = ""; From f892cceb3604fb70b9d1968d5e7dae380ba3e77c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 2 Jun 2022 00:46:15 -0700 Subject: [PATCH 119/121] Use shared_ptr custom deleter for m_fetched_error in error_already_set. This enables removing the dtor, copy ctor, move ctor completely. --- include/pybind11/pybind11.h | 9 ++++----- include/pybind11/pytypes.h | 22 ++++++---------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index de6138c816..83d84bd81e 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2625,13 +2625,12 @@ void print(Args &&...args) { detail::print(c.args(), c.kwargs()); } -inline error_already_set::~error_already_set() { - if (m_fetched_error.use_count() != 1 || !m_fetched_error->has_py_object_references()) { - return; // Avoid gil and scope overhead if there is nothing to release. - } +inline void +error_already_set::m_fetched_error_deleter(detail::error_fetch_and_normalize *raw_ptr) { gil_scoped_acquire gil; error_scope scope; - m_fetched_error->release_py_object_references(); + raw_ptr->release_py_object_references(); + delete raw_ptr; } inline const char *error_already_set::what() const noexcept { diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 78c4dd3b79..077050c96b 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -541,8 +541,6 @@ struct error_fetch_and_normalize { return (PyErr_GivenExceptionMatches(m_type.ptr(), exc.ptr()) != 0); } - bool has_py_object_references() const { return m_type || m_value || m_trace; } - void release_py_object_references() { m_type.release().dec_ref(); m_value.release().dec_ref(); @@ -580,20 +578,8 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { /// Fetches the current Python exception (using PyErr_Fetch()), which will clear the /// current Python error indicator. error_already_set() - : m_fetched_error{ - std::make_shared("pybind11::error_already_set")} {} - - /// WARNING: This destructor needs to acquire the Python GIL. This can lead to - /// crashes (undefined behavior) if the Python interpreter is finalizing. - ~error_already_set() override; - - // This copy ctor does not need the GIL because it simply increments a shared_ptr use_count. - error_already_set(const error_already_set &) noexcept = default; - - // This move ctor cannot easily be deleted (some compilers need it). - // It is the responsibility of the caller to not use the moved-from object. - // For simplicity, guarding ifs are omitted. - error_already_set(error_already_set &&) noexcept = default; + : m_fetched_error{new detail::error_fetch_and_normalize("pybind11::error_already_set"), + m_fetched_error_deleter} {} /// The what() result is built lazily on demand. /// WARNING: This member function needs to acquire the Python GIL. This can lead to @@ -637,6 +623,10 @@ class PYBIND11_EXPORT_EXCEPTION error_already_set : public std::exception { private: std::shared_ptr m_fetched_error; + + /// WARNING: This custom deleter needs to acquire the Python GIL. This can lead to + /// crashes (undefined behavior) if the Python interpreter is finalizing. + static void m_fetched_error_deleter(detail::error_fetch_and_normalize *raw_ptr); }; #if defined(_MSC_VER) # pragma warning(pop) From 53e29f419efd66ac4676e4c82b46219c47c46d5c Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 2 Jun 2022 01:01:44 -0700 Subject: [PATCH 120/121] Further small simplification. With the GIL held, simply deleting the raw_ptr takes care of everything. --- include/pybind11/pybind11.h | 1 - include/pybind11/pytypes.h | 6 ------ 2 files changed, 7 deletions(-) diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h index 83d84bd81e..d61dcd5c7e 100644 --- a/include/pybind11/pybind11.h +++ b/include/pybind11/pybind11.h @@ -2629,7 +2629,6 @@ inline void error_already_set::m_fetched_error_deleter(detail::error_fetch_and_normalize *raw_ptr) { gil_scoped_acquire gil; error_scope scope; - raw_ptr->release_py_object_references(); delete raw_ptr; } diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 077050c96b..87d789ad2b 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -541,12 +541,6 @@ struct error_fetch_and_normalize { return (PyErr_GivenExceptionMatches(m_type.ptr(), exc.ptr()) != 0); } - void release_py_object_references() { - m_type.release().dec_ref(); - m_value.release().dec_ref(); - m_trace.release().dec_ref(); - } - // Not protecting these for simplicity. object m_type, m_value, m_trace; From 8e145c09f76da8d037e0fd9a25088ce2ca9854aa Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 2 Jun 2022 14:01:09 -0700 Subject: [PATCH 121/121] IWYU cleanup ``` iwyu version: include-what-you-use 0.17 based on Debian clang version 13.0.1-3+build2 ``` Command used: ``` iwyu -c -std=c++17 -DPYBIND11_TEST_BOOST -Iinclude/pybind11 -I/usr/include/python3.9 -I/usr/include/eigen3 include/pybind11/pytypes.cpp ``` pytypes.cpp is a temporary file: `#include "pytypes.h"` The raw output is very long and noisy. I decided to use `#include ` instead of `#include ` for `std::size_t` (iwyu sticks to the manual choice). I ignored all iwyu suggestions that are indirectly covered by `#include `. I manually verified that all added includes are actually needed. --- include/pybind11/pytypes.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/include/pybind11/pytypes.h b/include/pybind11/pytypes.h index 87d789ad2b..f9625e77ea 100644 --- a/include/pybind11/pytypes.h +++ b/include/pybind11/pytypes.h @@ -12,10 +12,15 @@ #include "detail/common.h" #include "buffer_info.h" -#include +#include +#include #include +#include +#include +#include #include #include +#include #include #if defined(PYBIND11_HAS_OPTIONAL)