25

I am currently writing a C++ extension for Python using Boost.Python. A function in this extension may generate an exception containing information about the error (beyond just a human-readable string describing what happened). I was hoping I could export this exception to Python so I could catch it and do something with the extra information.

For example:

import my_cpp_module
try:
    my_cpp_module.my_cpp_function()
except my_cpp_module.MyCPPException, e:
    print e.my_extra_data

Unfortunately Boost.Python seems to translate all C++ exceptions (that are subclasses of std::exception) into RuntimeError. I realize that Boost.Python allows one to implement custom exception translation however, one needs to use PyErr_SetObject which takes a PyObject* (for the exception's type) and a PyObject* (for the exception's value)--neither of which I know how to get from my Boost.Python classes. Perhaps there is a way (which would be great) that I simply have not found yet. Otherwise does anyone know how to export a custom C++ exception so that I may catch it in Python?

Barry
  • 286,269
  • 29
  • 621
  • 977
Jack Edmonds
  • 31,931
  • 18
  • 65
  • 77

6 Answers6

29

The solution is to create your exception class like any normal C++ class

class MyCPPException : public std::exception {...}

The trick is that all boost::python::class_ instances hold a reference to the object's type which is accessible through their ptr() function. You can get this as you register the class with boost::python like so:

class_<MyCPPException> myCPPExceptionClass("MyCPPException"...);
PyObject *myCPPExceptionType=myCPPExceptionClass.ptr();
register_exception_translator<MyCPPException>(&translateFunc);

Finally, when you are translating the C++ exception to a Python exception, you do so as follows:

void translate(MyCPPException const &e)
{
    PyErr_SetObject(myCPPExceptionType, boost::python::object(e).ptr());
}

Here is a full working example:

#include <boost/python.hpp>
#include <assert.h>
#include <iostream>

class MyCPPException : public std::exception
{
private:
  std::string message;
  std::string extraData;
public:
  MyCPPException(std::string message, std::string extraData)
  {
    this->message = message;
    this->extraData = extraData;
  }
  const char *what() const throw()
  {
    return this->message.c_str();
  }
  ~MyCPPException() throw()
  {
  }
  std::string getMessage()
  {
    return this->message;
  }
  std::string getExtraData()
  {
    return this->extraData;
  }
};

void my_cpp_function(bool throwException)
{
  std::cout << "Called a C++ function." << std::endl;
  if (throwException)
    {
      throw MyCPPException("Throwing an exception as requested.",
               "This is the extra data.");
    }
}

PyObject *myCPPExceptionType = NULL;

void translateMyCPPException(MyCPPException const &e)
{
  assert(myCPPExceptionType != NULL);
  boost::python::object pythonExceptionInstance(e);
  PyErr_SetObject(myCPPExceptionType, pythonExceptionInstance.ptr());
}

BOOST_PYTHON_MODULE(my_cpp_extension)
{
  boost::python::class_<MyCPPException>
    myCPPExceptionClass("MyCPPException",
            boost::python::init<std::string, std::string>());
  myCPPExceptionClass.add_property("message", &MyCPPException::getMessage)
    .add_property("extra_data", &MyCPPException::getExtraData);
  myCPPExceptionType = myCPPExceptionClass.ptr();
  boost::python::register_exception_translator<MyCPPException>
    (&translateMyCPPException);
  boost::python::def("my_cpp_function", &my_cpp_function);
}

Here is the Python code that calls the extension:

import my_cpp_extension
try:
    my_cpp_extension.my_cpp_function(False)
    print 'This line should be reached as no exception should be thrown.'
except my_cpp_extension.MyCPPException, e:
    print 'Message:', e.message
    print 'Extra data:',e.extra_data

try:
    my_cpp_extension.my_cpp_function(True)
    print ('This line should not be reached as an exception should have been' +
       'thrown by now.')
except my_cpp_extension.MyCPPException, e:
    print 'Message:', e.message
    print 'Extra data:',e.extra_data
Jack Edmonds
  • 31,931
  • 18
  • 65
  • 77
4

The answer given by Jack Edmonds defines a Python "exception" class that does not inherit Exception (or any other built-in Python exception class). So although it can be caught with

except my_cpp_extension.MyCPPException as e:
    ...

it can not be caught with the usual catch all

except Exception as e:
    ...

Here is how to create a custom Python exception class that does inherit Exception.

Community
  • 1
  • 1
Johan Råde
  • 20,480
  • 21
  • 73
  • 110
  • But this doesn't wrap an existing c++ class derived from std::exception... or am I missing something? If I'm not, your solution is not really answering the question in this thread – Dan Niero Sep 06 '12 at 13:14
  • @Dan Niero: The normal way to "export" an exception from C++ to Python is not to wrap it, but to translate it to a Python exception derived from `Exception`. – Johan Råde Sep 06 '12 at 14:19
  • I see your point. But if it is the c++ side that raise/throw an exception, which is the best solution to catch that exception in Python? In the example here I can catch an exception thrown from the c++ code. I can't , however, raise that exception from within python. I can only catch it. If I'm not wrong, in your solution, you give a way to raise a c++ exception from python, but it doesn't make python "aware" of the exception raised from the c++ code. Actually it is, but it thinks they are all RuntimeError. Excuse me if I'm missing something, I'm just trying to understand – Dan Niero Sep 06 '12 at 14:30
  • @Dan Niero: You can not really throw an exception on the C++ side and catch it on the Python side. You must catch the exception on the C++ side and then call `PyErr_SetString` or `PyErr_SetObject` to raise a Python exception. If you are using Boost.Python than Boost.Python does that for you automatically. If Boost.Python does not recognize the C++ exception, then it will by default raise a `RuntimeError`. However, you can override that default by installing your own exception translator. There you can translate your own C++ exceptions to your own Python exceptions. – Johan Råde Sep 06 '12 at 18:11
1

Thanks to variadic templates and generalized lambda capture, we can collapse Jack Edmond's answer into something much more manageable and hide all of the cruft from the user:

template <class E, class... Policies, class... Args>
py::class_<E, Policies...> exception_(Args&&... args) {
    py::class_<E, Policies...> cls(std::forward<Args>(args)...);
    py::register_exception_translator<E>([ptr=cls.ptr()](E const& e){
        PyErr_SetObject(ptr, py::object(e).ptr());
    });
    return cls;
}

To expose MyCPPException as an exception, you just need to change py::class_ in the bindings to exception_:

exception_<MyCPPException>("MyCPPException", py::init<std::string, std::string>())
    .add_property("message", &MyCPPException::getMessage)
    .add_property("extra_data", &MyCPPException::getExtraData)
;

And now we're back to the niceties of Boost.Python: don't need to name the class_ instance, don't need this extra PyObject*, and don't need an extra function somewhere.

Community
  • 1
  • 1
Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    I tried your solution and got the following error on the Python side: `SystemError: exception not a BaseException subclass` , and `TypeError: catching classes that do not inherit from BaseException is not allowed` . Boost.Python V1.61, Python 3.4. – András Aszódi Oct 06 '16 at 10:05
  • same tried this solution and get `TypeError: exceptions must derive from BaseException` :( – gst Nov 29 '19 at 17:28
1

I combined the answers from Barry and David Faure and created a working python exception. It extracts the S parameter for exception name (so it must be passed explicitly to class_ object).

template <class E, class... Policies, class S, class... Args>
boost::python::class_<E, Policies...> exception_(S name, Args&&... args) {
    boost::python::class_<E, Policies...> cls(name, std::forward<Args>(args)...);
        
    pythonExceptionType = createExceptionClass(name);
    
    boost::python::register_exception_translator<E>([ptr=pythonExceptionType](E const& e){
        boost::python::object exc_t(boost::python::handle<>(boost::python::borrowed(ptr)));
        exc_t.attr("cause") = boost::python::object(e); 
        exc_t.attr("what") = boost::python::object(e.what());
        PyErr_SetString(ptr, e.what());
        PyErr_SetObject(ptr, boost::python::object(e).ptr());
    });
    return cls;
}

static PyObject* createExceptionClass(const char* name, PyObject* baseTypeObj = PyExc_Exception)
{
    using std::string;
    namespace bp = boost::python;

    const string scopeName = bp::extract<string>(bp::scope().attr("__name__"));
    const string qualifiedName0 = scopeName + "." + name;
    PyObject* typeObj = PyErr_NewException(qualifiedName0.c_str(), baseTypeObj, 0);
    bp::scope().attr(name) = bp::handle<>(bp::borrowed(typeObj));
    return typeObj;
}

BOOST_PYTHON_MODULE(MyModule)

exception_<MyException, bases<SomeBaseException>>("MyException", no_init)
            .def("get_message", &MyException::get_message)
            .def("get_reason", &MyException::get_reason)
            ;

And in python

try:
    do_sth()
except MyModule.MyException as e: 
    print(e.cause.get_message())

    print(e.cause.get_reason())
0

Here's the solution from Jack Edmonds, ported to Python 3, using advice from here which itself uses code from here. Assembling it all together (and modernizing the C++ code a little bit) gives:

#include <boost/python.hpp>
#include <assert.h>
#include <iostream>

class MyCPPException : public std::exception
{
public:
    MyCPPException(const std::string &message, const std::string &extraData)
        : message(message), extraData(extraData)
    {
    }
    const char *what() const noexcept override
    {
        return message.c_str();
    }
    std::string getMessage() const
    {
        return message;
    }
    std::string getExtraData() const
    {
        return extraData;
    }
private:
    std::string message;
    std::string extraData;
};

void my_cpp_function(bool throwException)
{
    std::cout << "Called a C++ function." << std::endl;
    if (throwException) {
        throw MyCPPException("Throwing an exception as requested.",
                             "This is the extra data.");
    }
}

static PyObject* createExceptionClass(const char* name, PyObject* baseTypeObj = PyExc_Exception)
{
    using std::string;
    namespace bp = boost::python;

    const string scopeName = bp::extract<string>(bp::scope().attr("__name__"));
    const string qualifiedName0 = scopeName + "." + name;
    PyObject* typeObj = PyErr_NewException(qualifiedName0.c_str(), baseTypeObj, 0);
    if (!typeObj) bp::throw_error_already_set();
    bp::scope().attr(name) = bp::handle<>(bp::borrowed(typeObj));
    return typeObj;
}

static PyObject *pythonExceptionType = NULL;

static void translateMyCPPException(MyCPPException const &e)
{
    using namespace boost;
    python::object exc_t(python::handle<>(python::borrowed(pythonExceptionType)));
    exc_t.attr("cause") = python::object(e); // add the wrapped exception to the Python exception
    exc_t.attr("what") = python::object(e.what()); // for convenience
    PyErr_SetString(pythonExceptionType, e.what()); // the string is used by print(exception) in python
}

BOOST_PYTHON_MODULE(my_cpp_extension)
{
    using namespace boost;
    python::class_<MyCPPException>
            myCPPExceptionClass("MyCPPException",
                                python::init<std::string, std::string>());
    myCPPExceptionClass.add_property("message", &MyCPPException::getMessage)
            .add_property("extra_data", &MyCPPException::getExtraData);

    pythonExceptionType = createExceptionClass("MyPythonException");
    python::register_exception_translator<MyCPPException>(&translateMyCPPException);
    python::def("my_cpp_function", &my_cpp_function);
}

and the python file to test it:

#!/usr/bin/env python3

import my_cpp_extension
try:
    my_cpp_extension.my_cpp_function(False)
    print('This line should be reached as no exception should be thrown.')
except my_cpp_extension.MyPythonException as e:
    print('Message:', e.what)
    print('Extra data:',e.cause.extra_data)

try:
    my_cpp_extension.my_cpp_function(True)
    print ('This line should not be reached as an exception should have been' +
       'thrown by now.')
except my_cpp_extension.MyPythonException as e:
    print('Message:', e.what)
    print('Extra data:',e.cause.extra_data)

And catching it as a standard python Exception works too:

except Exception as e:
    print('Exception: ',e)
David Faure
  • 1,836
  • 15
  • 19
0

Have you tested it in macOS? The solution works perfectly in Linux (gcc) and Windows (VS), but when I tested it in macOS Big Sur (Xcode Clang) and I get the following error instead of the exception:

Called a C++ function.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
SystemError: <Boost.Python.function object at 0x7fdcf5c30700> returned NULL without setting an error
Camilo
  • 35
  • 4
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Sep 28 '21 at 19:00