[C++-sig] Inheriting from a python base class/exception translation

Nick Rasmussen nick at ilm.com
Tue Nov 8 03:19:56 CET 2011


I'm cleaning up some of our python bindings for public release as
part of the openexr distribution, and wanted to update the python
bindings of our exception library, Iex, to use boost::python.

The first thing I did was the trivial binding of the class hierarchy
out to python, and performed register_exception_translator<> for each
type with a translator like:

void
translateExc(const BaseExc &exc)
{
    PyErr_SetObject(pytype,boost::python::object(exc).ptr());
}

where pytype is the pointer to the type object for BaseExc's
python binding.  This allows instances of the exception types
to be raised in c++, translated into python, and caught.

However, the problem I'm having is this: to allow instances of
the exception types to be raised in python (or in fact to have
a try/except/else block finalize properly), the python objects
need to be derived from the python Exception type or one of
its subclasses.

I saw this thread from several years ago:

http://mail.python.org/pipermail/cplusplus-sig/2006-April/010320.html

but, at least with python 2.6, objects raised from within
a script must derive from Exception or else a TypeError will
be raised:

testing raising existing BaseExc exception object:
Traceback (most recent call last):
  File "testexc.py", line 24, in <module>
    raise e1
TypeError: exceptions must be classes or instances, not BaseExc

So my question is this: how do I bind a c++ type out to
boost::python such that the python bindings for the type
inherits from a type defined in python?

What I've tried so far was to reflect RuntimeError out to c++
in a manner similar to how the other python builtin object
types (e.g. list/dict) are handled in boost::python.  The
reflection was successful, and I was getting correct translation
of RuntimeErrors across the python/C boundary (both for raising
and passing instances of RuntimeError objects).  I didn't
particularly expect this to work because of the object layouts, but
at least by using the c++ RuntimeError type as the parameter to
bases<>, isinstance identified the instances as being derived from
RuntimeError, and the instances were raiseable.  However, whenever
the objects were deallocated, the deallocation would crash:

#0  0x0000000000479e9f in _PyWeakref_GetWeakrefCount (head=0x30) at Objects/weakrefobject.c:16
#1  0x000000000047ca50 in PyObject_ClearWeakRefs (object=0x892960) at Objects/weakrefobject.c:898
#2  0x000000000046a2bd in subtype_dealloc (self=0x892960) at Objects/typeobject.c:968

Looking in valgrind, there were unsurprisingly plenty of other
errors before the crash as well. :)

One other approach that seemed like it may work would be to define the
entire exception hierarchy in python, mirroring the c++ hierarchy,
and reflect each class individually to c++.  That doesn't seem
particularly boost-pythonic, so I'm hoping that there's a better way
such that I can just add a small fix-up in the base of the bindings
for the exception hierarchy (perhaps a custom PyTypeObject?).

Any ideas?  I've included a condensed example and testcase below:

-nick

exctestmodule.cpp:

#include <Python.h>
#include <boost/python.hpp>
#include <boost/format.hpp>

using namespace boost::python;

namespace ExcTest {

template <class Exc>
struct ExcTranslator
{
    static PyObject *pytype;
    static const char *module;
    static const char *name;

    static void translateExc(const Exc &exc)
    {
        PyErr_SetObject(pytype,object(exc).ptr());
    }

    static std::string
    repr(const Exc &exc)
    {
        return (boost::format("%s.%s('%s')")%module%name%exc.what()).str();
    }
};

template <class Exc, class Base>
class_<Exc,bases<Base> >
registerExc()
{
    class_<Exc,bases<Base> > exc_class(ExcTranslator<Exc>::name,init<const std::string &>());
    exc_class
        .def("__repr__",&ExcTranslator<Exc>::repr)
        ;
    ExcTranslator<Exc>::pytype = exc_class.ptr();
    register_exception_translator <Exc>(&ExcTranslator<Exc>::translateExc);
    return exc_class;
}

struct BaseExc : public std::exception
{
    explicit BaseExc(const std::string &message) : _message(message) {}
    virtual ~BaseExc() throw() {}
    virtual const char *what() const throw() { return _message.c_str(); }
    std::string _message;
};

struct ArgExc : public BaseExc
{
    explicit ArgExc(const std::string &message) : BaseExc(message) {}
    virtual ~ArgExc() throw() {}
};

void
testException (int idx)
{
    if (idx == 1)
        throw ArgExc("ArgExc from c++");
    throw BaseExc("BaseExc from c++");
}

#define PY_DEFINE_EXC(ExcType,ModuleName,ExcName)                     \
template <> PyObject *ExcTranslator<ExcType>::pytype = 0;             \
template <> const char *ExcTranslator<ExcType>::module = #ModuleName; \
template <> const char *ExcTranslator<ExcType>::name = #ExcName;

PY_DEFINE_EXC(BaseExc,exctest,BaseExc)
PY_DEFINE_EXC(ArgExc,exctest,ArgExc)


} // namespace ExcTest

using namespace ExcTest;

BOOST_PYTHON_MODULE(exctest)
{
    def("testException", &testException);

    class_<BaseExc> myExc_class("BaseExc",init<const std::string &>());
    myExc_class
        .def("__str__",&BaseExc::what)
        .def("__repr__",&ExcTranslator<BaseExc>::repr)
        ;
    ExcTranslator<BaseExc>::pytype = myExc_class.ptr();
    register_exception_translator <BaseExc>(&ExcTranslator<BaseExc>::translateExc);

    registerExc<ArgExc,BaseExc>();
}

testexc.py:

#!/usr/bin/env python2.6
import exctest
import traceback

print 'testing BaseExc exception creation:'
e1 = exctest.BaseExc('BaseExc from python')
assert str(e1) == 'BaseExc from python'
assert repr(e1) == "exctest.BaseExc('BaseExc from python')"
#assert isinstance(e1,RuntimeError)
#del e1
print '  pass: %s' % (repr(e1))

print 'testing ArgExc exception creation:'
e2 = exctest.ArgExc('ArgExc from python')
assert str(e2) == 'ArgExc from python'
assert repr(e2) == "exctest.ArgExc('ArgExc from python')"
#assert isinstance(e2,RuntimeError)
assert isinstance(e2,exctest.BaseExc)
#del e2
print '  pass: %s' % (repr(e2))

print 'testing raising existing BaseExc exception object:'
try:
   raise e1
except exctest.ArgExc, e:
   traceback.print_exc()
   assert False
except exctest.BaseExc, e:
   print '  pass: %s' % (repr(e))
except:
   traceback.print_exc()
   assert False
else:
   assert False

print 'testing raising existing ArgExc exception object:'
try:
   raise e2
except exctest.ArgExc, e:
   print '  pass: %s' % (repr(e))
except exctest.BaseExc, e:
   traceback.print_exc()
   assert False
except:
   traceback.print_exc()
   assert False
else:
   assert False

print 'testing BaseExc exception translation:'
try:
   exctest.testException(0)
except exctest.ArgExc, e:
   traceback.print_exc()
   assert False
except exctest.BaseExc, e:
   print '  pass: %s' % (repr(e))
except:
   traceback.print_exc()
   assert False
else:
   assert False

print 'testing ArgExc exception translation:'
try:
   exctest.testException(1)
except exctest.ArgExc, e:
   print '  pass: %s' % (repr(e))
except exctest.BaseExc, e:
   traceback.print_exc()
   assert False
except:
   traceback.print_exc()
   assert False
else:
   assert False


print 'testing BaseExc raise:'
try:
   raise exctest.BaseExc('new BaseExc from python')
except exctest.ArgExc, e:
   traceback.print_exc()
   assert False
except exctest.BaseExc, e:
   print '  pass: %s' % (repr(e))
except:
   traceback.print_exc()
   assert False
else:
   assert False

print 'testing ArgExc raise:'
try:
   raise exctest.ArgExc('new ArgExc from python')
except exctest.ArgExc, e:
   print '  pass: %s' % (repr(e))
except exctest.BaseExc, e:
   traceback.print_exc()
   assert False
except:
   traceback.print_exc()
   assert False
else:
   assert False

print "done"



More information about the Cplusplus-sig mailing list