mercredi 22 juillet 2015

Create Python child class and add to std::vector in boost::python wrapped class

Problem

I have an abstract base class in c++, IFunctor, that I wish to write concrete classes for in python.

These python classes then need to be added back into a std::vector<std::shared_ptr<IFunctor> > that is held in the c++ FunctorContainer class, and can then be used from within c++.

All wrapping is provided for by boost::python.

Challenges

  • We use std::shared_ptr extensively in the c++, so simply switching to boost::shared_ptr doesn't make sense.
  • I don't think you can write a python function to create a std::shared_ptr of a python class -- maybe someone knows how to do this?

Solution

I have tried to bring in many sources from the web to come up with a solution. It works (at least on the surface), but I don't fully understand why. I'm hoping there are people here that might be able to explain:

  • Is this a reasonable solution? How might it break?
  • Is there a better way?
  • How does it actually work? Is it just to do with boost::python magic under the hood with boost::shared_ptr?

Working example

// pyfunctor.cpp
#include <memory>
#include <exception>
#include <boost/python.hpp>

/** Abstract base class */
class IFunctor
{
public:
    virtual void apply() const = 0;
};

/** Concrete c++ class */
class CPPFunctor :
    public IFunctor
{
public:
    void apply() const override
    {
        std::cout << "CPPFunctor::apply()" << std::endl;
    }

    static
    std::shared_ptr<IFunctor> create()
    {
        return std::make_shared<CPPFunctor>();
    }
};

/** Container for the functors, store functors in vector */
class FunctorContainer
{
public:
    void addFunctor(
        const std::shared_ptr<IFunctor>& functor
        )
    {
        mFunctors.push_back(functor);
    }

    void test() const
    {
        for (const auto& functor : mFunctors) {
            functor->apply();
        }
    }
private:
    std::vector<std::shared_ptr<IFunctor> > mFunctors;
};

/** Wrapper class to enable inheritance of abstract base
 * class from within python */
class IFunctorWrapper :
    public IFunctor,
    public boost::python::wrapper<IFunctor>
{
public:
    PyObject* self;

    IFunctorWrapper(PyObject* tmpSelf) :
        self(tmpSelf) {}

    void apply() const
    {
        try {
            return boost::python::call_method<void>(self, "apply");
        } catch (const boost::python::error_already_set& e) {
            // handle python exception
            throw "Python failed";
        }
    }
};

/** Helps in the switching of boost::shared_ptr to std::shared_ptr */
template<typename T>
void do_release(typename boost::shared_ptr<T> const&, T*) {}

/**
 * Boost can handle python objects as boost::shared_ptr
 * http://ift.tt/1LsY2lk
 */
void
addPythonFunctor(
    FunctorContainer& container,
    const boost::shared_ptr<IFunctor>& functor
    )
{
    // convert from boost::shared_ptr to std::shared_ptr
    // http://ift.tt/1LsY2lm
    std::shared_ptr<IFunctor> stdPtr(
            functor.get(),
            boost::bind(&do_release<IFunctor>, functor, _1));
    assert(stdPtr);
    container.addFunctor(stdPtr);
}

/** helper for wrapping both pointer types */
template<typename T>
void
registerPtrToPython()
{
    boost::python::register_ptr_to_python<std::shared_ptr<T> >();
    boost::python::register_ptr_to_python<boost::shared_ptr<T> >();
}

/** primary boost::python code */
BOOST_PYTHON_MODULE(pyfunctor)
{
    using namespace boost::python;

    // wrap the abstract base class
    class_<IFunctor, IFunctorWrapper, boost::noncopyable>("IFunctor")
        .def("apply", &IFunctorWrapper::apply)
        ;
    registerPtrToPython<IFunctor>();

    // wrap the C++ child class
    class_<CPPFunctor, bases<IFunctor>, boost::noncopyable>("CPPFunctor", no_init)
        .def("create", &CPPFunctor::create)
        .staticmethod("create")
        ;
    registerPtrToPython<CPPFunctor>();

    // wrap the container
    class_<FunctorContainer, boost::noncopyable>("FunctorContainer")
        .def("addFunctor", &FunctorContainer::addFunctor) // add c++ created functors
        .def("addPythonFunctor", &addPythonFunctor) // add python created functors
        .def("test", &FunctorContainer::test)
        ;
}

A Makefile for compiling

# Makefile
PYTHON_VERSION = 2.7
PYTHON_INCLUDE = /usr/include/python$(PYTHON_VERSION)
BOOST_INC = /usr/include
BOOST_LIB = /usr/lib

TARGET = pyfunctor

$(TARGET).so: $(TARGET).o
    g++ -shared -Wl,--export-dynamic $(TARGET).o -L$(BOOST_LIB) -lboost_python -L/usr/lib/python$(PYTHON_VERSION)/config -lpython$(PYTHON_VERSION) -o $(TARGET).so

$(TARGET).o: $(TARGET).cpp
    g++ -Wall -std=c++11 -I$(PYTHON_INCLUDE) -I$(BOOST_INC) -fPIC -c $(TARGET).cpp

A test script that shows the example working

# test.py
from pyfunctor import IFunctor, CPPFunctor, FunctorContainer

class PythonFunctor(IFunctor):
    def __init__(self):
        super(PythonFunctor, self).__init__()

    def apply(self):
        print "PythonFunctor::apply()"

container = FunctorContainer()

#functor = CPPFunctor()  # this would fail due to no_init
functor = CPPFunctor.create()  # creates a std::shared_ptr
container.addFunctor(functor)

functor = PythonFunctor()  # can be used as a boost::shared_ptr (?)
container.addPythonFunctor(functor)

container.test()

Aucun commentaire:

Enregistrer un commentaire