boost::python Export Custom Exception

后端 未结 4 1205
天涯浪人
天涯浪人 2020-12-13 03:24

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

相关标签:
4条回答
  • 2020-12-13 03:36

    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.

    0 讨论(0)
  • 2020-12-13 03:45

    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.

    0 讨论(0)
  • 2020-12-13 03:47

    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)
    
    0 讨论(0)
  • 2020-12-13 03:51

    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
    
    0 讨论(0)
提交回复
热议问题