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?

0

6 Answers 6

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
Sign up to request clarification or add additional context in comments.

2 Comments

Great answer, but it's for Python 2. After porting the python code to python 3, this happens at runtime: SystemError: exception <class 'my_cpp_extension.MyCPPException'> not a BaseException subclass
See my answer for a python 3 version of this code.
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.

4 Comments

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: 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.
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: 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.
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.

2 Comments

I tried your solution and got the following error on the Python side: SystemError: exception <class 'MyCPPException'> not a BaseException subclass , and TypeError: catching classes that do not inherit from BaseException is not allowed . Boost.Python V1.61, Python 3.4.
same tried this solution and get TypeError: exceptions must derive from BaseException :(
1

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)

Comments

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())

Comments

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

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.