Skip to content

[RFC] Add support for binding C++ Exceptions as full-featured Python classes #2333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
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
14 changes: 13 additions & 1 deletion include/pybind11/attr.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ struct arithmetic { };
/// Mark a function for addition at the beginning of the existing overload chain instead of the end
struct prepend { };

// Annotation to mark a class as needing an exception base type.
struct is_except {};

/** \rst
A call policy which places one or more guard variables (``Ts...``) around the function call.

Expand Down Expand Up @@ -222,7 +225,8 @@ struct function_record {
struct type_record {
PYBIND11_NOINLINE type_record()
: multiple_inheritance(false), dynamic_attr(false), buffer_protocol(false),
default_holder(true), module_local(false), is_final(false) { }
default_holder(true), module_local(false), is_final(false),
is_except(false) { }

/// Handle to the parent scope
handle scope;
Expand Down Expand Up @@ -278,6 +282,9 @@ struct type_record {
/// Is the class inheritable from python classes?
bool is_final : 1;

// Does the class need an exception base type?
bool is_except : 1;

PYBIND11_NOINLINE void add_base(const std::type_info &base, void *(*caster)(void *)) {
auto base_info = detail::get_type_info(base, false);
if (!base_info) {
Expand Down Expand Up @@ -469,6 +476,11 @@ struct process_attribute<is_final> : process_attribute_default<is_final> {
static void init(const is_final &, type_record *r) { r->is_final = true; }
};

template <>
struct process_attribute<is_except> : process_attribute_default<is_except> {
static void init(const is_except &, type_record *r) { r->is_except = true; }
};

template <>
struct process_attribute<buffer_protocol> : process_attribute_default<buffer_protocol> {
static void init(const buffer_protocol &, type_record *r) { r->buffer_protocol = true; }
Expand Down
18 changes: 13 additions & 5 deletions include/pybind11/detail/class.h
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ extern "C" inline void pybind11_object_dealloc(PyObject *self) {
/** 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. */
inline PyObject *make_object_base_type(PyTypeObject *metaclass) {
inline PyObject *make_object_base_type(PyTypeObject *metaclass, bool is_except=false) {
constexpr auto *name = "pybind11_object";
auto name_obj = reinterpret_steal<object>(PYBIND11_FROM_STRING(name));

Expand All @@ -457,7 +457,12 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) {

auto type = &heap_type->ht_type;
type->tp_name = name;
type->tp_base = type_incref(&PyBaseObject_Type);
if (is_except) {
type->tp_base = type_incref(reinterpret_cast<PyTypeObject*>(PyExc_Exception));
}
else {
type->tp_base = type_incref(&PyBaseObject_Type);
}
type->tp_basicsize = static_cast<ssize_t>(sizeof(instance));
type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE;

Expand All @@ -474,7 +479,9 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) {
setattr((PyObject *) type, "__module__", str("pybind11_builtins"));
PYBIND11_SET_OLDPY_QUALNAME(type, name_obj);

assert(!PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
if (!is_except) {
assert(!PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
}
return (PyObject *) heap_type;
}

Expand Down Expand Up @@ -630,8 +637,9 @@ inline PyObject* make_new_python_type(const type_record &rec) {

auto &internals = get_internals();
auto bases = tuple(rec.bases);
auto base = (bases.empty()) ? internals.instance_base
: bases[0].ptr();
auto base = (bases.empty()) ? (rec.is_except ? internals.exception_base
: internals.instance_base)
: bases[0].ptr();

/* Danger zone: from now (and until PyType_Ready), make sure to
issue no Python C API calls which could potentially invoke the
Expand Down
9 changes: 9 additions & 0 deletions include/pybind11/detail/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,16 @@ struct nonsimple_values_and_holders {

/// The 'instance' type which needs to be standard layout (need to be able to use 'offsetof')
struct instance {
#if (PY_MAJOR_VERSION == 3)
PyException_HEAD
#else
PyObject_HEAD
// Necessary to support exceptions.
PyObject *dict;
PyObject *args;
PyObject *message;
#endif

/// Storage for pointers and holder; see simple_layout, below, for a description
union {
void *simple_value_holder[1 + instance_simple_holder_in_ptrs()];
Expand Down
6 changes: 4 additions & 2 deletions include/pybind11/detail/internals.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ PYBIND11_NAMESPACE_BEGIN(detail)
// Forward declarations
inline PyTypeObject *make_static_property_type();
inline PyTypeObject *make_default_metaclass();
inline PyObject *make_object_base_type(PyTypeObject *metaclass);
inline PyObject *make_object_base_type(PyTypeObject *metaclass, bool is_except);

// The old Python Thread Local Storage (TLS) API is deprecated in Python 3.7 in favor of the new
// Thread Specific Storage (TSS) API.
Expand Down Expand Up @@ -111,6 +111,7 @@ struct internals {
PyTypeObject *static_property_type;
PyTypeObject *default_metaclass;
PyObject *instance_base;
PyObject *exception_base;
#if defined(WITH_THREAD)
PYBIND11_TLS_KEY_INIT(tstate);
PyInterpreterState *istate = nullptr;
Expand Down Expand Up @@ -312,7 +313,8 @@ PYBIND11_NOINLINE inline internals &get_internals() {
internals_ptr->registered_exception_translators.push_front(&translate_exception);
internals_ptr->static_property_type = make_static_property_type();
internals_ptr->default_metaclass = make_default_metaclass();
internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass);
internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass, false);
internals_ptr->exception_base = make_object_base_type(internals_ptr->default_metaclass, true);
}
return **internals_pp;
}
Expand Down
37 changes: 32 additions & 5 deletions tests/test_exceptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ struct PythonCallInDestructor {
};



struct PythonAlreadySetInDestructor {
PythonAlreadySetInDestructor(const py::str &s) : s(s) {}
~PythonAlreadySetInDestructor() {
Expand All @@ -106,7 +105,30 @@ struct PythonAlreadySetInDestructor {
};


// An Exception that is going to be bound as a py:class_
class BoundException : public std::exception {
public:
BoundException(const std::string& m, int e) : message{m}, m_errorCode(e) {}
virtual const char * what() const noexcept override {return message.c_str();}
int errorCode() const noexcept { return m_errorCode;}
private:
std::string message;
int m_errorCode;
};



TEST_SUBMODULE(exceptions, m) {
// Provide a class binding for BoundException. This is providing the
// dynamic_attr because the parent type (PyExc_Exception) permits dynamic
// attributes. One improvement might be to make is_except force
// dynamic_attr
auto PyBoundException = py::class_< BoundException>(m, "BoundException", py::is_except(), py::dynamic_attr())
.def(py::init< std::string, int>())
.def("getErrorCode", &BoundException::errorCode)
.def("getMessage", &BoundException::what)
.def("__str__", &BoundException::what);

m.def("throw_std_exception", []() {
throw std::runtime_error("This exception was intentionally thrown.");
});
Expand Down Expand Up @@ -150,15 +172,19 @@ TEST_SUBMODULE(exceptions, m) {
// A slightly more complicated one that declares MyException5_1 as a subclass of MyException5
py::register_exception<MyException5_1>(m, "MyException5_1", ex5.ptr());

//py::register_local_exception<LocalSimpleException>(m, "LocalSimpleException")
py::register_local_exception<LocalSimpleException>(m, "LocalSimpleException");

py::register_local_exception_translator([](std::exception_ptr p) {
// An exception translator for the exception that is bound as a class
// This is using PyErr_SetObject instead of PyErr_SetString.
py::register_exception_translator([](std::exception_ptr p) {
try {
if (p) {
std::rethrow_exception(p);
}
} catch (const MyException6 &e) {
PyErr_SetString(PyExc_RuntimeError, e.what());
} catch (const BoundException &e) {
auto err = py::cast(e);
auto errType = err.get_type().ptr();
PyErr_SetObject(errType, err.ptr());
}
});

Expand All @@ -173,6 +199,7 @@ TEST_SUBMODULE(exceptions, m) {
m.def("throws_overflow_error", []() { throw std::overflow_error(""); });
m.def("throws_local_error", []() { throw LocalException("never caught"); });
m.def("throws_local_simple_error", []() { throw LocalSimpleException("this mod"); });
m.def("throws_bound_exception", []() { throw BoundException("this error is a class", 42); });
m.def("exception_matches", []() {
py::dict foo;
try {
Expand Down
15 changes: 15 additions & 0 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,18 @@ def test_local_translator(msg):
m.throws_local_simple_error()
assert not isinstance(excinfo.value, cm.LocalSimpleException)
assert msg(excinfo.value) == "this mod"


def test_bound_exceptions():
"""Tests throwing/catching an exception bound as a class"""
try:
m.throws_bound_exception()
except m.BoundException as ex:
assert str(ex) == "this error is a class"
assert ex.getErrorCode() == 42

try:
raise m.BoundException("raising from python", 14)
except m.BoundException as ex:
assert str(ex) == "raising from python"
assert ex.getErrorCode() == 14