Search code examples
pythonc++boosttuplesboost-python

boost python returning tuple containing custom types


I have a C++ myObject class that I expose via boost python using a wrapper structure:

struct myObjectWrapper{
   static tuple compute(myObject& o,const Container& x0, const double& t0, Container& x){
      double t;
      int stat = o.evaluate(x0,t0,x,t);
      return make_tuple(stat,t);
   }
}

BOOST_PYTHON_MODULE(myModule)
{
   // not shown here is code to expose Container class

   class_<myObject>("MyObject")
      .def("compute",&myObjectWrapper::compute)
   ;
}

Container is currently defined as:

typedef std::valarray<double> Container

and is exposed to python.

Now in python I can do.

x = Container()
(status,t) = obj.compute(Container([0.,0.,0.]),0.0,x)
print status, t, x[0]

This is not very pythonic. I would prefer to do:

(status,t,x) = obj.compute(Container([0.,0.,0.]),0.0)
print status, t, x[0]    

I could write an additional wrapper in python, but I would prefer to avoid adding more wrappers.

The following code does't compile:

struct myObjectWrapper{
   static tuple compute(myObject& o,const Container& x0, const double& t0){
      double t;
      Container x;
      int stat = o.evaluate(x0,t0,x,t);
      return make_tuple(stat,t,x);
   }
}

Also I would prefer to steal the content of the local variable x and have python manage it rather than copy it:

return make_tuple(stat,t,std::move(x));

How do I achieve this?


Solution

  • In short, allocate the wrapper on the free store and use the manage_new_object result convert to transfer ownership to a Python object. This will cause Boost.Python to copy the pointer when constructing the Python object, rather than copying the pointee. For more details, see this answer.

    Here is an auxiliary function that will transfer ownership to a Python object:

    /// @brief Transfer ownership to a Python object.  If the transfer fails,
    ///        then object will be destroyed and an exception is thrown.
    template <typename T>
    boost::python::object transfer_to_python(T* t)
    {
      // Transfer ownership to a smart pointer, allowing for proper cleanup
      // incase Boost.Python throws.
      std::unique_ptr<T> ptr(t);
    
      // Use the manage_new_object generator to transfer ownership to Python.
      namespace python = boost::python;
      typename python::manage_new_object::apply<T*>::type converter;
    
      // Transfer ownership to the Python handler and release ownership
      // from C++.
      python::handle<> handle(converter(*ptr));
      ptr.release();
    
      return python::object(handle);
    }
    

    And one could use it as follows:

    boost::python::tuple myObjectWrapper::compute(
      myObject& o, const Container& x0, const double& t0)
    {
      auto x1 = std::make_unique<container>();
      double t1 = 0;
      int stat = self.evaluate(x0, t0, *x1, t1);
      return boost::python::make_tuple(stat, t1, transfer_to_python(x1.release()));
    }
    

    Here is a complete example based on the original question that demonstrates using the transfer_to_python auxiliary function.

    #include <boost/python.hpp>
    #include <cassert>
    #include <memory> // std::unique_ptr
    
    // Mock legacy API.
    struct container
    {
      container() {}
      container(boost::python::object) {}
    
      container(const container&)
      {
        // For this example, guarantee copy is not made.
        assert(false);
       }
    };
    
    struct my_object
    {
      int evaluate(container&, double&, container&, double&) { return 42; }
    };
    
    /// @brief Transfer ownership to a Python object.  If the transfer fails,
    ///        then object will be destroyed and an exception is thrown.
    template <typename T>
    boost::python::object transfer_to_python(T* t)
    {
      // Transfer ownership to a smart pointer, allowing for proper cleanup
      // incase Boost.Python throws.
      std::unique_ptr<T> ptr(t);
    
      // Use the manage_new_object generator to transfer ownership to Python.
      namespace python = boost::python;
      typename python::manage_new_object::apply<T*>::type converter;
    
      // Transfer ownership to the Python handler and release ownership
      // from C++.
      python::handle<> handle(converter(*ptr));
      ptr.release();
    
      return python::object(handle);
    }
    
    // API wrapper.
    boost::python::tuple my_object_compute(
      my_object& self, container& x0, double t0)
    {
      auto x1 = std::make_unique<container>();
      double t1 = 21;
      int stat = self.evaluate(x0, t0, *x1, t1);
      return boost::python::make_tuple(stat, t1, transfer_to_python(x1.release()));
    }
    
    BOOST_PYTHON_MODULE(example)
    {
      namespace python = boost::python;
    
      python::class_<container>("Container")
        .def(python::init<python::object>())
        ;
    
      python::class_<my_object>("MyObject")
        .def("compute", &my_object_compute)
        ;
    }
    

    Interactive usage:

    >>> import example
    >>> my_object = example.MyObject()
    >>> status, t, x = my_object.compute(example.Container([1, 2, 3]), 4)
    >>> assert(status == 42)
    >>> assert(t == 21)
    >>> assert(isinstance(x, example.Container))