From 5a7d17ff16a01436f7228a688c62511ab8c3efde Mon Sep 17 00:00:00 2001
From: Pim Schellart
Date: Fri, 17 Jun 2016 17:35:59 -0400
Subject: [PATCH] Add support for user defined exception translators
---
docs/advanced.rst | 61 ++++++++++++++++++++
example/CMakeLists.txt | 1 +
example/example.cpp | 2 +
example/example19.cpp | 108 ++++++++++++++++++++++++++++++++++++
example/example19.py | 42 ++++++++++++++
example/example19.ref | 15 +++++
include/pybind11/cast.h | 21 +++++++
include/pybind11/common.h | 2 +
include/pybind11/pybind11.h | 62 +++++++++++++++++----
9 files changed, 302 insertions(+), 12 deletions(-)
create mode 100644 example/example19.cpp
create mode 100644 example/example19.py
create mode 100644 example/example19.ref
diff --git a/docs/advanced.rst b/docs/advanced.rst
index 2a0e26870..9ae48b8c5 100644
--- a/docs/advanced.rst
+++ b/docs/advanced.rst
@@ -817,6 +817,8 @@ In other words, :func:`init` creates an anonymous function that invokes an
in-place constructor. Memory allocation etc. is already take care of beforehand
within pybind11.
+.. _catching_and_throwing_exceptions:
+
Catching and throwing exceptions
================================
@@ -869,6 +871,65 @@ There is also a special exception :class:`cast_error` that is thrown by
:func:`handle::call` when the input arguments cannot be converted to Python
objects.
+Registering custom exception translators
+========================================
+
+If the default exception conversion policy described
+:ref:`above `
+is insufficient, pybind11 also provides support for registering custom
+exception translators.
+
+The function ``register_exception_translator(translator)`` takes a stateless
+callable (e.g. a function pointer or a lambda function without captured
+variables) with the following call signature: ``void(std::exception_ptr)``.
+
+When a C++ exception is thrown, registered exception translators are tried
+in reverse order of registration (i.e. the last registered translator gets
+a first shot at handling the exception).
+
+Inside the translator, ``std::rethrow_exception`` should be used within
+a try block to re-throw the exception. A catch clause can then use
+``PyErr_SetString`` to set a Python exception as demonstrated
+in :file:`example19.cpp``.
+
+This example also demonstrates how to create custom exception types
+with ``py::exception``.
+
+The following example demonstrates this for a hypothetical exception class
+``MyCustomException``:
+
+.. code-block:: cpp
+
+ py::register_exception_translator([](std::exception_ptr p) {
+ try {
+ if (p) std::rethrow_exception(p);
+ } catch (const MyCustomException &e) {
+ PyErr_SetString(PyExc_RuntimeError, e.what());
+ }
+ });
+
+Multiple exceptions can be handled by a single translator. If the exception is
+not caught by the current translator, the previously registered one gets a
+chance.
+
+If none of the registered exception translators is able to handle the
+exception, it is handled by the default converter as described in the previous
+section.
+
+.. note::
+
+ You must either call ``PyErr_SetString`` for every exception caught in a
+ custom exception translator. Failure to do so will cause Python to crash
+ with ``SystemError: error return without exception set``.
+
+ Exceptions that you do not plan to handle should simply not be caught.
+
+ You may also choose to explicity (re-)throw the exception to delegate it to
+ the other existing exception translators.
+
+ The ``py::exception`` wrapper for creating custom exceptions cannot (yet)
+ be used as a ``py::base``.
+
.. _opaque:
Treating STL data structures as opaque objects
diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt
index 4ac1cb14f..2d30f0811 100644
--- a/example/CMakeLists.txt
+++ b/example/CMakeLists.txt
@@ -25,6 +25,7 @@ set(PYBIND11_EXAMPLES
example16.cpp
example17.cpp
example18.cpp
+ example19.cpp
issues.cpp
)
diff --git a/example/example.cpp b/example/example.cpp
index 1f3c18d5e..ad37273da 100644
--- a/example/example.cpp
+++ b/example/example.cpp
@@ -27,6 +27,7 @@ void init_ex15(py::module &);
void init_ex16(py::module &);
void init_ex17(py::module &);
void init_ex18(py::module &);
+void init_ex19(py::module &);
void init_issues(py::module &);
#if defined(PYBIND11_TEST_EIGEN)
@@ -54,6 +55,7 @@ PYBIND11_PLUGIN(example) {
init_ex16(m);
init_ex17(m);
init_ex18(m);
+ init_ex19(m);
init_issues(m);
#if defined(PYBIND11_TEST_EIGEN)
diff --git a/example/example19.cpp b/example/example19.cpp
new file mode 100644
index 000000000..e382225e2
--- /dev/null
+++ b/example/example19.cpp
@@ -0,0 +1,108 @@
+/*
+ example/example19.cpp -- exception translation
+
+ Copyright (c) 2016 Pim Schellart
+
+ All rights reserved. Use of this source code is governed by a
+ BSD-style license that can be found in the LICENSE file.
+*/
+
+#include "example.h"
+
+// A type that should be raised as an exeption in Python
+class MyException : public std::exception {
+public:
+ explicit MyException(const char * m) : message{m} {}
+ virtual const char * what() const noexcept override {return message.c_str();}
+private:
+ std::string message = "";
+};
+
+// A type that should be translated to a standard Python exception
+class MyException2 : public std::exception {
+public:
+ explicit MyException2(const char * m) : message{m} {}
+ virtual const char * what() const noexcept override {return message.c_str();}
+private:
+ std::string message = "";
+};
+
+// A type that is not derived from std::exception (and is thus unknown)
+class MyException3 {
+public:
+ explicit MyException3(const char * m) : message{m} {}
+ virtual const char * what() const noexcept {return message.c_str();}
+private:
+ std::string message = "";
+};
+
+// A type that should be translated to MyException
+// and delegated to its exception translator
+class MyException4 : public std::exception {
+public:
+ explicit MyException4(const char * m) : message{m} {}
+ virtual const char * what() const noexcept override {return message.c_str();}
+private:
+ std::string message = "";
+};
+
+void throws1() {
+ throw MyException("this error should go to a custom type");
+}
+
+void throws2() {
+ throw MyException2("this error should go to a standard Python exception");
+}
+
+void throws3() {
+ throw MyException3("this error cannot be translated");
+}
+
+void throws4() {
+ throw MyException4("this error is rethrown");
+}
+
+void throws_logic_error() {
+ throw std::logic_error("this error should fall through to the standard handler");
+}
+
+void init_ex19(py::module &m) {
+ // make a new custom exception and use it as a translation target
+ static py::exception ex(m, "MyException");
+ py::register_exception_translator([](std::exception_ptr p) {
+ try {
+ if (p) std::rethrow_exception(p);
+ } catch (const MyException &e) {
+ PyErr_SetString(ex.ptr(), e.what());
+ }
+ });
+
+ // register new translator for MyException2
+ // no need to store anything here because this type will
+ // never by visible from Python
+ py::register_exception_translator([](std::exception_ptr p) {
+ try {
+ if (p) std::rethrow_exception(p);
+ } catch (const MyException2 &e) {
+ PyErr_SetString(PyExc_RuntimeError, e.what());
+ }
+ });
+
+ // register new translator for MyException4
+ // which will catch it and delegate to the previously registered
+ // translator for MyException by throwing a new exception
+ py::register_exception_translator([](std::exception_ptr p) {
+ try {
+ if (p) std::rethrow_exception(p);
+ } catch (const MyException4 &e) {
+ throw MyException(e.what());
+ }
+ });
+
+ m.def("throws1", &throws1);
+ m.def("throws2", &throws2);
+ m.def("throws3", &throws3);
+ m.def("throws4", &throws4);
+ m.def("throws_logic_error", &throws_logic_error);
+}
+
diff --git a/example/example19.py b/example/example19.py
new file mode 100644
index 000000000..d4ee5f853
--- /dev/null
+++ b/example/example19.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+from __future__ import print_function
+import sys
+sys.path.append('.')
+
+import example
+
+print("Can we catch a MyException?")
+try:
+ example.throws1()
+except example.MyException as e:
+ print(e.__class__.__name__, ":", e)
+print("")
+
+print("Can we translate to standard Python exceptions?")
+try:
+ example.throws2()
+except Exception as e:
+ print(e.__class__.__name__, ":", e)
+print("")
+
+print("Can we handle unknown exceptions?")
+try:
+ example.throws3()
+except Exception as e:
+ print(e.__class__.__name__, ":", e)
+print("")
+
+print("Can we delegate to another handler by rethrowing?")
+try:
+ example.throws4()
+except example.MyException as e:
+ print(e.__class__.__name__, ":", e)
+print("")
+
+print("Can we fall-through to the default handler?")
+try:
+ example.throws_logic_error()
+except Exception as e:
+ print(e.__class__.__name__, ":", e)
+print("")
+
diff --git a/example/example19.ref b/example/example19.ref
new file mode 100644
index 000000000..a0489234a
--- /dev/null
+++ b/example/example19.ref
@@ -0,0 +1,15 @@
+Can we catch a MyException?
+MyException : this error should go to a custom type
+
+Can we translate to standard Python exceptions?
+RuntimeError : this error should go to a standard Python exception
+
+Can we handle unknown exceptions?
+RuntimeError : Caught an unknown exception!
+
+Can we delegate to another handler by rethrowing?
+MyException : this error is rethrown
+
+Can we fall-through to the default handler?
+RuntimeError : this error should fall through to the standard handler
+
diff --git a/include/pybind11/cast.h b/include/pybind11/cast.h
index a2a135e8e..543f67c68 100644
--- a/include/pybind11/cast.h
+++ b/include/pybind11/cast.h
@@ -49,6 +49,27 @@ PYBIND11_NOINLINE inline internals &get_internals() {
internals_ptr->istate = tstate->interp;
#endif
builtins[id] = capsule(internals_ptr);
+ internals_ptr->registered_exception_translators.push_front(
+ [](std::exception_ptr p) -> void {
+ try {
+ if (p) std::rethrow_exception(p);
+ } catch (const error_already_set &) { return;
+ } catch (const index_error &e) { PyErr_SetString(PyExc_IndexError, e.what()); return;
+ } catch (const value_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
+ } catch (const stop_iteration &e) { PyErr_SetString(PyExc_StopIteration, e.what()); return;
+ } catch (const std::bad_alloc &e) { PyErr_SetString(PyExc_MemoryError, e.what()); return;
+ } catch (const std::domain_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
+ } catch (const std::invalid_argument &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
+ } catch (const std::length_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
+ } catch (const std::out_of_range &e) { PyErr_SetString(PyExc_IndexError, e.what()); return;
+ } catch (const std::range_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return;
+ } catch (const std::exception &e) { PyErr_SetString(PyExc_RuntimeError, e.what()); return;
+ } catch (...) {
+ PyErr_SetString(PyExc_RuntimeError, "Caught an unknown exception!");
+ return;
+ }
+ }
+ );
}
return *internals_ptr;
}
diff --git a/include/pybind11/common.h b/include/pybind11/common.h
index e06eac002..7bbb31f1d 100644
--- a/include/pybind11/common.h
+++ b/include/pybind11/common.h
@@ -66,6 +66,7 @@
# pragma warning(pop)
#endif
+#include
#include
#include
#include
@@ -264,6 +265,7 @@ struct internals {
std::unordered_map registered_types_py; // PyTypeObject* -> type_info
std::unordered_map registered_instances; // void * -> PyObject*
std::unordered_set, overload_hash> inactive_overload_cache;
+ std::forward_list registered_exception_translators;
#if defined(WITH_THREAD)
decltype(PyThread_create_key()) tstate = 0; // Usually an int but a long on Cygwin64 with Python 3.x
PyInterpreterState *istate = nullptr;
diff --git a/include/pybind11/pybind11.h b/include/pybind11/pybind11.h
index 3c9e6910d..3fe177809 100644
--- a/include/pybind11/pybind11.h
+++ b/include/pybind11/pybind11.h
@@ -401,19 +401,32 @@ protected:
if (result.ptr() != PYBIND11_TRY_NEXT_OVERLOAD)
break;
}
- } catch (const error_already_set &) { return nullptr;
- } catch (const index_error &e) { PyErr_SetString(PyExc_IndexError, e.what()); return nullptr;
- } catch (const value_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return nullptr;
- } catch (const stop_iteration &e) { PyErr_SetString(PyExc_StopIteration, e.what()); return nullptr;
- } catch (const std::bad_alloc &e) { PyErr_SetString(PyExc_MemoryError, e.what()); return nullptr;
- } catch (const std::domain_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return nullptr;
- } catch (const std::invalid_argument &e) { PyErr_SetString(PyExc_ValueError, e.what()); return nullptr;
- } catch (const std::length_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return nullptr;
- } catch (const std::out_of_range &e) { PyErr_SetString(PyExc_IndexError, e.what()); return nullptr;
- } catch (const std::range_error &e) { PyErr_SetString(PyExc_ValueError, e.what()); return nullptr;
- } catch (const std::exception &e) { PyErr_SetString(PyExc_RuntimeError, e.what()); return nullptr;
+ } catch (const error_already_set &) {
+ return nullptr;
} catch (...) {
- PyErr_SetString(PyExc_RuntimeError, "Caught an unknown exception!");
+ /* When an exception is caught, give each registered exception
+ translator a chance to translate it to a Python exception
+ in reverse order of registration.
+
+ A translator may choose to do one of the following:
+
+ - catch the exception and call PyErr_SetString or PyErr_SetObject
+ to set a standard (or custom) Python exception, or
+ - do nothing and let the exception fall through to the next translator, or
+ - delegate translation to the next translator by throwing a new type of exception. */
+
+ auto last_exception = std::current_exception();
+ auto ®istered_exception_translators = pybind11::detail::get_internals().registered_exception_translators;
+ for (auto& translator : registered_exception_translators) {
+ try {
+ translator(last_exception);
+ } catch (...) {
+ last_exception = std::current_exception();
+ continue;
+ }
+ return nullptr;
+ }
+ PyErr_SetString(PyExc_SystemError, "Exception escaped from default exception translator!");
return nullptr;
}
@@ -1089,6 +1102,31 @@ template void implicitly_convertible()
((detail::type_info *) it->second)->implicit_conversions.push_back(implicit_caster);
}
+template
+void register_exception_translator(ExceptionTranslator&& translator) {
+ detail::get_internals().registered_exception_translators.push_front(
+ std::forward(translator));
+}
+
+/* Wrapper to generate a new Python exception type.
+ *
+ * This should only be used with PyErr_SetString for now.
+ * It is not (yet) possible to use as a py::base.
+ * Template type argument is reserved for future use.
+ */
+template
+class exception : public object {
+public:
+ exception(module &m, const std::string name, PyObject* base=PyExc_Exception) {
+ std::string full_name = std::string(PyModule_GetName(m.ptr()))
+ + std::string(".") + name;
+ char* exception_name = const_cast(full_name.c_str());
+ m_ptr = PyErr_NewException(exception_name, base, NULL);
+ inc_ref(); // PyModule_AddObject() steals a reference
+ PyModule_AddObject(m.ptr(), name.c_str(), m_ptr);
+ }
+};
+
#if defined(WITH_THREAD)
/* The functions below essentially reproduce the PyGILState_* API using a RAII