boost::python Export Custom Exception

2019-01-21 11:18发布

问题:

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?

回答1:

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


回答2:

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.



回答3:

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.