boost::python Export Custom Exception
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)-开发者_StackOverflow社区-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?
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
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
.
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.
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)
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
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())
精彩评论