Skip to content

[cpyrt] Raise C++ exceptions from destructors in Python - #170

Open
conrade-ctc wants to merge 1 commit into
compiler-research:mainfrom
chicagotrading:upstreaming/throwing-destructor
Open

conrade-ctc wants to merge 1 commit into
compiler-research:mainfrom
chicagotrading:upstreaming/throwing-destructor

Conversation

@conrade-ctc

@conrade-ctc conrade-ctc commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

A throwing noexcept(false) destructor ended the process with std::terminate. The destructor path (op_dealloc_nofree → Cpp::Destruct) had no handler, so the exception unwound into CPython's C frames.

  • __destruct__() now raises the C++ exception in Python. The proxy gives up ownership, so a second call does not run the destructor again.
  • Deallocation cannot raise. It reports the exception through sys.unraisablehook, as CPython does for __del__. On Python 3.13 and later, PyErr_FormatUnraisable names the C++ type and address of the object in the message.
  • For a by-value object, the storage is freed also when the destructor throws ([expr.delete]).
  • The C++-to-Python exception conversion moves from CPPMethod to ProxyWrappers (SetPyErrFromCppException), so that both paths use it.

test33_throwing_destructor covers these cases. It is xfail on Apple Silicon, like the other tests that throw C++ exceptions.

As far as I can tell, InitializerListConverter::Clear has the same gap. This PR does not change it.

🤖 Done with the help of Claude Code (Opus 5.5, human in the loop)

@aaronj0 aaronj0 left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Apart from the comment below this patch looks good to me; as discussed today I'll get this tested with ROOT before merging.

Comment thread src/cpyrt/CPPInstance.cxx
PyObject *pytype, *pyvalue, *pytrace;
PyErr_Fetch(&pytype, &pyvalue, &pytrace);
if (!op_dealloc_nofree(pyobj))
PyErr_WriteUnraisable((PyObject*)Py_TYPE(pyobj));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Perhaps we could use PyErr_FormatUnraisable and propagate some info about the object being deallocated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea, thanks. PyErr_FormatUnraisable is public only from Python 3.13, and cppjit still supports 3.12. I will use it on 3.13 and later, with a message that names the C++ type and address of the object. On 3.12 I will keep PyErr_WriteUnraisable.

A noexcept(false) destructor that throws aborted the process, because
the destructor path did not catch C++ exceptions. __destruct__() now
raises the exception as a Python exception. Deallocation reports it
through sys.unraisablehook, as CPython does for __del__. On Python 3.13
and later, the message names the C++ type and address of the object.

Move the exception conversion from CPPMethod into ProxyWrappers, so
that both paths use it.

Co-developed-with-the-help-of: Claude Code (Opus 5.5, human in the loop)
@conrade-ctc
conrade-ctc force-pushed the upstreaming/throwing-destructor branch from ee4f5d3 to 13fe239 Compare October 28, 2026 16:33

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants