6

I have an in-house library written in C++ that I'm currently working on extending into Python. I started this task with Boost.Python in mind, but I'm open to alternatives.

Currently I have a C++ function that needs to accept a Python class instance, and then use the methods of this object to perform certain tasks. The idea is for the Python user to never need to deal with C++. They're expected to create this Python object from a Python template/example class that I will provide, with preset method names which I can assume to be there in my C++ library.

The interface exposed to the Python user looks like:

class Foo(object):

    def __init__(self, args):
        """create an instance of this class with instance-specific attributes"""

    def Bar1(self, a, b, c):
        """do something with the given integers a, b and c"""
        pass

    def Bar2(self, a, b, c):
        """do something else with the given integers a, b and c"""
        pass

import mylib

cheese = mylib.Wine()
cheese.do_something(Foo)

In C++, the corresponding code looks like:

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

class Wine {

public:  

    Wine() {};

    ~Wine() {};

    static void do_something(boost::python::object *Foo) {

        int a = 1;
        int b = 2;
        int c = 3;

        Foo->attr("Bar1")(a, b, c);
        Foo->attr("Bar2")(a, b, c);
    };
};

BOOST_PYTHON_MODULE(mylib)
{
    using namespace boost::python;
    class_<Wine>("Wine")
        .def("do_something", &Wine::do_something);
};

I have successfully compiled this code and verified that the C++ class called Wine is really exposed to Python and I can access its member functions. If I write a member function called "greet()" that only returns "Hello, world!", it works perfectly.

I need to stress here the importance of passing an instance of Foo. I do not have the luxury to simply import the Foo module into C++ code and create an instance of Foo in C++. The object that I want to receive from the Python user has attributes I need to use that is specific to the instance, and not to the class itself.

Problem is that I can't figure out how to pass a Python instance into do_something, such that it will appear in C++ as a callable boost::python::object. The above code returns the following C++ signature mismatch error:

Boost.Python.ArgumentError: Python argument types in
    Wine.do_something(Wine, Foo)
did not match C++ signature:
    do_something(boost::python::api::object*)

Two days of perusing the internet for answers has yielded no progress. There seems to be a wealth of information on how to pass C++ classes into Python, but I was unable to find information on doing this in the opposite direction. Would really appreciate some guidance here.

Thanks!

Alp Dener
  • 65
  • 1
  • 5

2 Answers2

5

There are two errors in the initial code:

  • Boost.Python is attempting to pass two arguments (self and an instance of Foo) to the static Wine::do_something() C++ function that only accepts one argument. To resolve this, when exposing the Wine class, the Python Wine.do_something() member function needs to be set as static via the boost::python::class_::staticmethod() member function. When exposed as a static method, Boost.Python will no longer pass the self instance argument.
  • Unlike the Python/C API, where pointers are often used as handles to objects (PyObject*), Boost.Python provides a higher-level notation boost::python::object class that is often passed around by value or reference. Internally, this class interacts with a boost::python::handle that performs smart pointer management for PyObject.

Here is a complete Python extension based on the original code:

#include <boost/python.hpp>

class Wine
{
public:  

  static void do_something(boost::python::object object)
  {
    int a = 1;
    int b = 2;
    int c = 3;

    object.attr("Bar1")(a, b, c);
    object.attr("Bar2")(a, b, c);
  };
};

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;
  python::class_<Wine>("Wine")
    .def("do_something", &Wine::do_something)
      .staticmethod("do_something")
    ;
};

Interactive usage:

>>> class Foo(object):
...     def Bar1(self, a, b, c):
...         print "Bar1", locals()
...     def Bar2(self, a, b, c):
...         print "Bar2", locals()
... 
>>> import example
>>> cheese = example.Wine()
>>> cheese.do_something(Foo())
Bar1 {'a': 1, 'c': 3, 'b': 2, 'self': <__main__.Foo object at 0xb6b0f2ac>}
Bar2 {'a': 1, 'c': 3, 'b': 2, 'self': <__main__.Foo object at 0xb6b0f2ac>}
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • This is incredibly helpful. I'll report back on its success when I'm back in front if my computer but it certainly makes a lot of sense. One question though. If I'm understanding it correctly, you're implying that boost object class already handles a smart pointer to the pyobject and therefore accepting an object instead of object* will not create a copy of Foo. If so, that's good news because we really don't want to clone Foo here. It's intended to be a very large object in practice. – Alp Dener May 03 '14 at 22:53
  • @AlpDener Glad it is helpful. A copy of `Foo` will not be made. Boost.Python maintains Python's argument passing semantics, where object references are passed by value. For more information on lifetime and threading (noticed that your other comment mentions parallelism), consider reading [this](http://stackoverflow.com/a/17684703/1053968) answer. – Tanner Sansbury May 04 '14 at 04:22
  • I just had the opportunity to implement this in my code and I'm happy to report that it's working flawlessly. Also bookmarked your response on threading, in case we bump up against that in the future. Much appreciated! – Alp Dener May 04 '14 at 06:50
0

To expose a method which accepts a Python object as an argument you should use boost::python::object not boost::python::object *

void do_something(boost::python::object Foo)

To expose a static method expose it like a regular function: def("do_something", Wine::do_something);

import mylib

# method
cheese = mylib.Wine()
foo = Foo()
cheese.do_something(foo)

#static method
foo = Foo()
mylib.do_something(foo)
Slava Zhuyko
  • 691
  • 4
  • 12
  • Does that make a copy of Foo in C++? This is very undesirable. The Foo object is intended to be pretty big (and heavily parallelized). We want the C++ function to only have a reference to this Python object. – Alp Dener May 03 '14 at 22:51