Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 54 additions & 17 deletions src/cpyrt/CPPInstance.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ using namespace cppjit;
#include "cppjit_interop.h"

#include "cpyrt/DispatchPtr.h"
#include "cpyrt/PyException.h"

// Standard
#include <algorithm>
Expand Down Expand Up @@ -214,8 +215,9 @@ void cpyrt::CPPInstance::SetDispatchPtr(void* ptr) {
}

//----------------------------------------------------------------------------
void cpyrt::op_dealloc_nofree(CPPInstance* pyobj) {
bool cpyrt::op_dealloc_nofree(CPPInstance* pyobj) {
// Destroy the held C++ object, if owned; does not deallocate the proxy.
// Returns false with a Python error set if the destructor throws.

interop::TCppScope_t klass = pyobj->ObjectIsA(false /* check_smart */);
void*& cppobj = pyobj->GetObjectRaw();
Expand All @@ -224,28 +226,46 @@ void cpyrt::op_dealloc_nofree(CPPInstance* pyobj) {
MemoryRegulator::UnregisterPyObject(pyobj,
(PyObject*)Py_TYPE((PyObject*)pyobj));

bool ok = true;
if (cppobj && (pyobj->fFlags & CPPInstance::kIsOwner)) {
if (pyobj->fFlags & CPPInstance::kIsValue) {
interop::CallDestructor(klass, cppobj);
interop::Deallocate(klass, cppobj);
} else if (pyobj->fFlags & CPPInstance::kIsMalloc)
std::free(cppobj);
else if (pyobj->fFlags & CPPInstance::kIsNoConstruct) {
if (pyobj->fFlags & CPPInstance::kIsArrayAlloc)
::operator delete[](cppobj);
try {
if (pyobj->fFlags & CPPInstance::kIsValue) {
// [expr.delete]: the storage is freed even if the destructor throws
try {
interop::CallDestructor(klass, cppobj);
} catch (...) {
interop::Deallocate(klass, cppobj);
throw;
}
interop::Deallocate(klass, cppobj);
} else if (pyobj->fFlags & CPPInstance::kIsMalloc)
std::free(cppobj);
else if (pyobj->fFlags & CPPInstance::kIsNoConstruct) {
if (pyobj->fFlags & CPPInstance::kIsArrayAlloc)
::operator delete[](cppobj);
else
::operator delete(cppobj);
} else if (pyobj->fFlags & CPPInstance::kIsArrayAlloc)
interop::Destruct(klass, cppobj, 1);
// Default case: just kIsOwner set in all of memory-ownership flags
else
::operator delete(cppobj);
} else if (pyobj->fFlags & CPPInstance::kIsArrayAlloc)
interop::Destruct(klass, cppobj, 1);
// Default case: just kIsOwner set in all of memory-ownership flags
else
interop::Destruct(klass, cppobj);
interop::Destruct(klass, cppobj);
} catch (PyException&) {
ok = false; // error already set
} catch (std::exception& e) {
SetPyErrFromCppException(e);
ok = false;
} catch (...) {
PyErr_SetString(PyExc_Exception, "unhandled, unknown C++ exception");
ok = false;
}
}
cppobj = nullptr;

if (pyobj->IsExtended())
delete (ExtendedData*)pyobj->fObject;
pyobj->fFlags = CPPInstance::kNoWrapConv;
return ok;
}

namespace cppjit::cpyrt {
Expand Down Expand Up @@ -287,7 +307,8 @@ static PyObject* op_destruct(CPPInstance* self) {
// garbage collector (like in PyPy), to allow the user control over when
// the C++ destructor is called. This method requires that the C++ object
// is owned (no-op otherwise).
op_dealloc_nofree(self);
if (!op_dealloc_nofree(self))
return nullptr;
Py_RETURN_NONE;
}

Expand Down Expand Up @@ -465,7 +486,23 @@ static CPPInstance* op_new(PyTypeObject* subtype, PyObject*, PyObject*) {
static void op_dealloc(CPPInstance* pyobj) {
// Remove (Python-side) memory held by the object proxy.
PyObject_GC_UnTrack((PyObject*)pyobj);
op_dealloc_nofree(pyobj);
// dealloc cannot raise, so report the way CPython reports __del__ errors
PyObject *pytype, *pyvalue, *pytrace;
PyErr_Fetch(&pytype, &pyvalue, &pytrace);
void* cppobj = pyobj->GetObjectRaw(); // cleared by op_dealloc_nofree
if (!op_dealloc_nofree(pyobj)) {
#if PY_VERSION_HEX >= 0x030D0000
std::string name =
interop::GetScopedFinalName(((CPPScope*)Py_TYPE(pyobj))->fCppType);
PyErr_FormatUnraisable(
"Exception ignored while destroying C++ object of type %s at %p",
name.c_str(), cppobj);
#else
(void)cppobj;
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.

#endif
}
PyErr_Restore(pytype, pyvalue, pytrace);
PyObject_GC_Del((PyObject*)pyobj);
}

Expand Down
2 changes: 1 addition & 1 deletion src/cpyrt/CPPInstance.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ template <typename T> inline bool CPPInstance_CheckExact(T* object) {
}

//- helper for memory regulation (no PyTypeObject equiv. member in p2.2) -----
void op_dealloc_nofree(CPPInstance*);
bool op_dealloc_nofree(CPPInstance*);

} // namespace cppjit::cpyrt

Expand Down
54 changes: 1 addition & 53 deletions src/cpyrt/CPPMethod.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -121,60 +121,8 @@ inline PyObject* cpyrt::CPPMethod::ExecuteFast(void* self, ptrdiff_t offset,
ctxt->fFlags |= CallContext::kPyException;
result = nullptr; // error already set
} catch (std::exception& e) {
// attempt to set the exception to the actual type, to allow catching with
// the Python C++ type
static interop::TCppScope_t exc_type =
interop::GetFullScope("std::exception");

ctxt->fFlags |= CallContext::kCppException;

PyObject* pyexc_type = nullptr;
PyObject* pyexc_obj = nullptr;

// TODO: factor this code with the same in ProxyWrappers (and cache it there
// to be able to look up based on TCppType_t):
interop::TCppScope_t actual = interop::GetActualClass(exc_type, &e);
const std::string& finalname = interop::GetScopedFinalName(actual);
const std::string& parentname = TypeManip::extract_namespace(finalname);
PyObject* parent = CreateScopeProxy(parentname);
if (parent) {
pyexc_type = PyObject_GetAttrString(
parent,
parentname.empty()
? finalname.c_str()
: finalname.substr(parentname.size() + 2, std::string::npos)
.c_str());
Py_DECREF(parent);
}

if (pyexc_type) {
// create a copy of the exception (TODO: factor this code with the same in
// ProxyWrappers)
PyObject* pyclass = cpyrt::GetScopeProxy(actual);
PyObject* source = BindCppObjectNoCast(&e, actual);
PyObject* pyexc_copy =
PyObject_CallFunctionObjArgs(pyclass, source, nullptr);
Py_DECREF(source);
Py_DECREF(pyclass);
if (pyexc_copy) {
pyexc_obj = CPPExcInstance_Type.tp_new((PyTypeObject*)pyexc_type,
nullptr, nullptr);
((CPPExcInstance*)pyexc_obj)->fCppInstance = (PyObject*)pyexc_copy;
} else
PyErr_Clear();
} else
PyErr_Clear();

if (pyexc_type && pyexc_obj) {
PyErr_SetObject(pyexc_type, pyexc_obj);
Py_DECREF(pyexc_obj);
Py_DECREF(pyexc_type);
} else {
PyErr_Format(PyExc_Exception, "%s (C++ exception)", e.what());
Py_XDECREF(pyexc_obj);
Py_XDECREF(pyexc_type);
}

SetPyErrFromCppException(e);
result = nullptr;
} catch (...) {
// don't set the kCppException flag here, as there is basically no useful
Expand Down
52 changes: 52 additions & 0 deletions src/cpyrt/ProxyWrappers.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -1058,3 +1058,55 @@ PyObject* cpyrt::BindCppObjectArray(interop::TCppObject_t address,
// TODO: this function exists for symmetry; need to figure out if it's useful
return TupleOfInstances_New(address, klass, dims);
}

//----------------------------------------------------------------------------
void cpyrt::SetPyErrFromCppException(std::exception& e) {
// attempt to set the exception to the actual type, to allow catching with
// the Python C++ type
static interop::TCppScope_t exc_type =
interop::GetFullScope("std::exception");

PyObject* pyexc_type = nullptr;
PyObject* pyexc_obj = nullptr;

// TODO: cache the lookup based on TCppType_t
interop::TCppScope_t actual = interop::GetActualClass(exc_type, &e);
const std::string& finalname = interop::GetScopedFinalName(actual);
const std::string& parentname = TypeManip::extract_namespace(finalname);
PyObject* parent = CreateScopeProxy(parentname);
if (parent) {
pyexc_type = PyObject_GetAttrString(
parent, parentname.empty()
? finalname.c_str()
: finalname.substr(parentname.size() + 2, std::string::npos)
.c_str());
Py_DECREF(parent);
}

if (pyexc_type) {
// create a copy of the exception
PyObject* pyclass = cpyrt::GetScopeProxy(actual);
PyObject* source = BindCppObjectNoCast(&e, actual);
PyObject* pyexc_copy =
PyObject_CallFunctionObjArgs(pyclass, source, nullptr);
Py_DECREF(source);
Py_DECREF(pyclass);
if (pyexc_copy) {
pyexc_obj = CPPExcInstance_Type.tp_new((PyTypeObject*)pyexc_type, nullptr,
nullptr);
((CPPExcInstance*)pyexc_obj)->fCppInstance = (PyObject*)pyexc_copy;
} else
PyErr_Clear();
} else
PyErr_Clear();

if (pyexc_type && pyexc_obj) {
PyErr_SetObject(pyexc_type, pyexc_obj);
Py_DECREF(pyexc_obj);
Py_DECREF(pyexc_type);
} else {
PyErr_Format(PyExc_Exception, "%s (C++ exception)", e.what());
Py_XDECREF(pyexc_obj);
Py_XDECREF(pyexc_type);
}
}
4 changes: 4 additions & 0 deletions src/cpyrt/ProxyWrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "Dimensions.h"

// Standard
#include <exception>
#include <string>

namespace cppjit::cpyrt {
Expand Down Expand Up @@ -32,6 +33,9 @@ PyObject* BindCppObject(interop::TCppObject_t object,
PyObject* BindCppObjectArray(interop::TCppObject_t address,
interop::TCppScope_t klass, cdims_t dims);

// set the Python error to a proxy of the C++ exception, or a plain Exception
void SetPyErrFromCppException(std::exception& e);

} // namespace cppjit::cpyrt

#endif // !CPYRT_PROXYWRAPPERS_H
54 changes: 54 additions & 0 deletions test/test_advancedcpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1098,3 +1098,57 @@ def test31_virtual_base_downcast(self):
assert isinstance(obj3, ns.Visible3)
assert obj3.value == 23
assert obj3.z == 7

@mark.xfail(
condition=IS_MAC_ARM,
run=False,
reason="Crashes with exception not being caught on Apple Silicon",
)
def test33_throwing_destructor(self):
"""A noexcept(false) destructor that throws raises in Python"""

import sys

import cppjit

cppjit.cppdef("""\
#include <stdexcept>
namespace throwing_dtor {
int g_count = 0;
struct Throws {
~Throws() noexcept(false) {
++g_count;
throw std::runtime_error("from dtor");
}
};
Throws make() { return {}; }
}""")

ns = cppjit.gbl.throwing_dtor

t = ns.Throws()
with raises(cppjit.gbl.std.runtime_error, match="from dtor"):
t.__destruct__()
assert ns.g_count == 1
t.__destruct__() # no longer owned: no second destructor call
assert ns.g_count == 1

v = ns.make()
with raises(cppjit.gbl.std.runtime_error, match="from dtor"):
v.__destruct__()
assert ns.g_count == 2

# dealloc cannot raise: the error goes to sys.unraisablehook
unraisable = []
old_hook = sys.unraisablehook
sys.unraisablehook = unraisable.append
try:
t = ns.Throws()
del t
finally:
sys.unraisablehook = old_hook
assert ns.g_count == 3
assert len(unraisable) == 1
assert "from dtor" in str(unraisable[0].exc_value)
if sys.version_info >= (3, 13):
assert "throwing_dtor::Throws" in unraisable[0].err_msg
Loading