Skip to content

Commit f3ecd95

Browse files
committed
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 pybind#1853 for more context and an alternative take on the issue.
1 parent 12e8774 commit f3ecd95

3 files changed

Lines changed: 40 additions & 22 deletions

File tree

include/pybind11/cast.h

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -403,38 +403,36 @@ PYBIND11_NOINLINE inline bool isinstance_generic(handle obj, const std::type_inf
403403
return isinstance(obj, type);
404404
}
405405

406-
PYBIND11_NOINLINE inline std::string error_string() {
407-
if (!PyErr_Occurred()) {
406+
PYBIND11_NOINLINE inline std::string error_string(PyObject* type, PyObject* value, PyObject *trace) {
407+
if (!type && !value && !trace) {
408408
PyErr_SetString(PyExc_RuntimeError, "Unknown internal error occurred");
409409
return "Unknown internal error occurred";
410410
}
411411

412-
error_scope scope; // Preserve error state
413-
412+
// TODO(superbobry): is it safe to assume that exception has been
413+
// normalized by the caller?
414414
std::string errorString;
415-
if (scope.type) {
416-
errorString += handle(scope.type).attr("__name__").cast<std::string>();
415+
if (type) {
416+
errorString += handle(type).attr("__name__").cast<std::string>();
417417
errorString += ": ";
418418
}
419-
if (scope.value)
420-
errorString += (std::string) str(scope.value);
421-
422-
PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace);
419+
if (value)
420+
errorString += str(value).cast<std::string>();
423421

424422
#if PY_MAJOR_VERSION >= 3
425-
if (scope.trace != nullptr)
426-
PyException_SetTraceback(scope.value, scope.trace);
423+
if (trace)
424+
PyException_SetTraceback(value, trace);
427425
#endif
428426

429427
#if !defined(PYPY_VERSION)
430-
if (scope.trace) {
431-
PyTracebackObject *trace = (PyTracebackObject *) scope.trace;
428+
if (trace) {
429+
PyTracebackObject *tb = (PyTracebackObject *) trace;
432430

433431
/* Get the deepest trace possible */
434-
while (trace->tb_next)
435-
trace = trace->tb_next;
432+
while (tb->tb_next)
433+
tb = tb->tb_next;
436434

437-
PyFrameObject *frame = trace->tb_frame;
435+
PyFrameObject *frame = tb->tb_frame;
438436
errorString += "\n\nAt:\n";
439437
while (frame) {
440438
int lineno = PyFrame_GetLineNumber(frame);
@@ -450,6 +448,12 @@ PYBIND11_NOINLINE inline std::string error_string() {
450448
return errorString;
451449
}
452450

451+
PYBIND11_NOINLINE inline std::string error_string() {
452+
error_scope scope; // Preserve error state
453+
PyErr_NormalizeException(&scope.type, &scope.value, &scope.trace);
454+
return error_string(scope.type, scope.value, scope.trace);
455+
}
456+
453457
PYBIND11_NOINLINE inline handle get_object_handle(const void *ptr, const detail::type_info *type ) {
454458
auto &instances = get_internals().registered_instances;
455459
auto range = instances.equal_range(ptr);

include/pybind11/pytypes.h

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,17 +313,18 @@ template <typename T> T reinterpret_steal(handle h) { return {h, object::stolen_
313313

314314
NAMESPACE_BEGIN(detail)
315315
inline std::string error_string();
316+
inline std::string error_string(PyObject*, PyObject*, PyObject*);
316317
NAMESPACE_END(detail)
317318

318319
/// Fetch and hold an error which was already set in Python. An instance of this is typically
319320
/// thrown to propagate python-side errors back through C++ which can either be caught manually or
320321
/// else falls back to the function dispatcher (which then raises the captured error back to
321322
/// python).
322-
class error_already_set : public std::runtime_error {
323+
class error_already_set : public std::exception {
323324
public:
324325
/// Constructs a new exception from the current Python error indicator, if any. The current
325326
/// Python error indicator will be cleared.
326-
error_already_set() : std::runtime_error(detail::error_string()) {
327+
error_already_set() : std::exception() {
327328
PyErr_Fetch(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr());
328329
}
329330

@@ -332,10 +333,22 @@ class error_already_set : public std::runtime_error {
332333

333334
inline ~error_already_set();
334335

336+
virtual const char* what() const noexcept {
337+
if (m_lazy_what.empty()) {
338+
PyErr_NormalizeException(&m_type.ptr(), &m_value.ptr(), &m_trace.ptr());
339+
m_lazy_what = detail::error_string(m_type.ptr(), m_value.ptr(), m_trace.ptr());
340+
}
341+
return m_lazy_what.c_str();
342+
}
343+
335344
/// Give the currently-held error back to Python, if any. If there is currently a Python error
336345
/// already set it is cleared first. After this call, the current object no longer stores the
337346
/// error variables (but the `.what()` string is still available).
338-
void restore() { PyErr_Restore(m_type.release().ptr(), m_value.release().ptr(), m_trace.release().ptr()); }
347+
void restore() {
348+
what(); // Force-build `.what()`.
349+
if (m_type || m_value || m_trace)
350+
PyErr_Restore(m_type.release().ptr(), m_value.release().ptr(), m_trace.release().ptr());
351+
}
339352

340353
// Does nothing; provided for backwards compatibility.
341354
PYBIND11_DEPRECATED("Use of error_already_set.clear() is deprecated")
@@ -351,7 +364,8 @@ class error_already_set : public std::runtime_error {
351364
const object& trace() const { return m_trace; }
352365

353366
private:
354-
object m_type, m_value, m_trace;
367+
mutable std::string m_lazy_what;
368+
mutable object m_type, m_value, m_trace;
355369
};
356370

357371
/** \defgroup python_builtins _

tests/test_exceptions.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ TEST_SUBMODULE(exceptions, m) {
157157
PyErr_SetString(PyExc_ValueError, "foo");
158158
try {
159159
throw py::error_already_set();
160-
} catch (const std::runtime_error& e) {
160+
} catch (const py::error_already_set& e) {
161161
if ((err && e.what() != std::string("ValueError: foo")) ||
162162
(!err && e.what() != std::string("Unknown internal error occurred")))
163163
{

0 commit comments

Comments
 (0)