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