Skip to content

Adding boost::python exports to your ROS package

Paul Furgale edited this page Nov 6, 2013 · 4 revisions

Introduction

This page will guide you getting started exporting your C++ code to python using the rosbuild build system and some of the ROS tools to find your python code. For an introduction to boost::python, see the following:

The main differences between the stuff below and other tutorials are:

  1. The stuff below uses the ros build system and some handy macros to make building boost::python code easy.

  2. The stuff below strongly encourages you to use the overloaded numpy_eigen headers that allow proper handling of Eigen matrices within boost python, including the aligned storage of fixed-sized types.

Preparation

Always replace MYPACKAGE with name of your package.

  1. Go to your package directory:
> roscd MYPACKAGE
  1. Create a directory with the name of your package in the "src"-folder:
> mkdir src/MYPACKAGE
  1. Add a file to the src/MYPACKAGE directory called "init.py" and add the following contents:
# Import the numpy to Eigen type conversion.
import roslib; roslib.load_manifest('numpy_eigen'); import numpy_eigen
# Import the the C++ exports from your package library.
from libMYPACKAGE_python import *
# Import other files in the directory
# from mypyfile import *
  1. Create a directory with the name "python" in the "src"-folder:
> mkdir src/python
  1. For every source file whose methods have to be exported to python, one has to add a file of the same name to the src/python directory. The content of a MyClass.cpp would start like that:
// This is MyClass.cpp
// Note, it is very important that this file is included first, before any boost::python headers
#include <numpy_eigen/boost_python_headers.hpp>
// This is the header of the class being exported.
#include <MyClass.hpp>

using namespace boost::python;

// Here is the function that exports some specific stuff
// this is just a very simple example. There are more details
// below of how to export more complicated things.
void exportMyClass() {
   class_<MyClass, boost::shared_ptr<MyClass> >("MyClass", init<>())
     .def("method1", &MyClass::method1)
     .def("method2", &MyClass::method2)
   ;
}
  1. Add a boost::python project to your CMakeList.txt file: This example assumes that you already have a target called ${PROJECT_NAME} that includes all of your regular C++ code. Best practice is to have a separate ROS package with your python exports. This avoids having your C++ code pull in all the python dependencies.
# Use this handy cmake function
rosbuild_find_ros_package(numpy_eigen)

include(${numpy_eigen_PACKAGE_PATH}/cmake/add_python_export_library.cmake)

# This functions take TARGET_NAME PYTHON_MODULE_DIRECTORY sourceFile1 [sourceFile2 ...]
add_python_export_library(${PROJECT_NAME}_python src/${PROJECT_NAME}
  src/module.cpp
  src/python/EXAMPLEFILE.cpp
)

# This is only necessary if there is a previously-defined target
# called ${PROJECT_NAME}. If your pure C++ code is defined in a
# different ros package, use the regular manifest.xml dependency
# scheme to make sure this stuff links correctly.
target_link_libraries(${PROJECT_NAME}_python ${PROJECT_NAME})
  1. Export the python path in the package manifest (manifest.xml):
<depend package="numpy_eigen"/>
<export>
  <python path="${prefix}/src"/>
</export>
  1. Create the python module file "module.cpp" in the "MYPACKAGE/src"-folder:
// It is extremely important to use this header
// if you are using the numpy_eigen interface
#include <numpy_eigen/boost_python_headers.hpp>

// Here is where I usually put the function definitions
// for the exports of specific classes. This just allows
// one to avoid writing a bunch of header files that
// are one line
void exportMyClass();

// Each boost::python export library can have 
// only one call to BOOST_PYTHON_MODULE and the
// the title in the brackets must match the library
// name exactly. Within this function, you must call
// all of the export functions that were defined in
// other cpp export files.
BOOST_PYTHON_MODULE(libMYPACKAGE_python)
{
  // fill this in with boost::python export code

  // For example, call each of the export functions
  // listed above.
  exportMyClass();

}
  1. Access your code in Python To find your stuff in python, we use the ros utilities to pull it in to they python path.

Remember that you added your __init__.py file to src/MYPACKAGE? The manifest export tells roslib where to find this so that you can get it in python (and remember that MYPACKAGE is something you should replace with your package name.

Now you can import your package into python

# roslib knows how to find the python exports defined in the
# manifest.xml files
import roslib
# Here you tell roslib to process the manifest file from the package
# named "MYPACKAGE"
roslib.load_manifest('MYPACKAGE')
# now you can actually import your package
import MYPACKAGE

The easiest way to get started is to use ipython. Copy/edit the above into a file called prototype.py. Then type the following at a command prompt

sudo apt-get install ipython
ipython --pylab
# Now you are in the ipython shell
run prototype.py

Now you can use tab-completion to see what was imported by your package. Type MYPACKAGE. and hit tab a few times to see the completion list.

Syntax

Document a non-default constructor

class_<Gps>("Gps",init<>())
  .def(init<double,double,double,int>("Gps(latitudeDegrees, longitudeDegrees, heightMeters, rtkCode)"))
  .def_readwrite("latitudeDegrees",&Gps::latitudeDegrees)
  .def_readwrite("longitudeDegrees",&Gps::longitudeDegrees)
  .def_readwrite("heightMeters",&Gps::heightMeters)
  .def_readwrite("rtkCode",&Gps::rtkCode)
  .def("toUtm",&Gps::toUtm)
  ;

Disambiguate an overloaded function

Eigen3::Matrix4d(*toTEuler1)(double,double,double,double, double, double) = &toTEuler;
def("toTEuler",toTEuler1,"Create a 4x4 transformation matrix from 6 parameters");

Eigen3::Matrix4d(*toTEuler2)(Eigen3::Matrix<double,6,1>const &) = &toTEuler;
def("toTEuler",toTEuler2,"Create a 4x4 transformation matrix from a 6x1 column of parameters");

Disambiguate an overloaded member function

void(PoseGraph::*addVertex2)(VertexId)= &PoseGraph::addVertex;
VertexId(PoseGraph::*addVertex1)()=&PoseGraph::addVertex;
class_<PoseGraph>("PoseGraph",init<>())
  .def("addVertex",addVertex1)
  .def("addVertex",addVertex2)
  ;

Deal with a const_reference return type

def("T_to_from", getTFromEdge, return_value_policy<copy_const_reference>())

Create a tuple

using namespace boost::python;

tuple inverseTransformationAndJacobianTuple(Eigen3::Matrix4d const & T_ba, Eigen3::Vector4d const & v_b)
{
  Eigen3::Vector4d out_v_a;
  Eigen3::Matrix<double,4,6> out_B;
  inverseTransformationAndJacobian(T_ba, v_b, out_v_a, out_B);
  return make_tuple(out_v_a, out_B);
}

// ...

def("inverseTransformationAndJacobian",inverseTransformationAndJacobi anTuple);

Create a list

list allVertices(PoseGraph*pg)
{
  list t;
  VertexSet vs = pg->allVertices();
  for(VertexSet::const_iterator v = vs.begin(); v != vs.end();
  {
    t.append(*v);
  }
  return t;
}

//...
class_<PoseGraph>("PoseGraph",init<>())
  .def("allVertices",&allVertices)
  ;

Add a property

class_<ImuCameraCalibratorOptions> ("ImuCameraCalibrationOptions",init<>())
  .add_property("doGravity",&ImuCameraCalibratorOptions::get_doGravity, &ImuCameraCalibratorOptions::set_doGravity)
  .add_property("doCalibration",&ImuCameraCalibratorOptions::get_doCali bration,&ImuCameraCalibratorOptions::set_doCalibration)
  .add_property("doBias",&ImuCameraCalibratorOptions::get_doBias,&ImuCameraCalibratorOptions::set_doBias)
  ;

Using the Indexing Suite

#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
//...
class_<std::vector<DenseBlockJacobian>> ("DenseBlockJacobianVec")
  def(vector_indexing_suite<std::vector<DenseBlockJacobian> > ())
  ;

Adding a read/write object member

This is useful when the member is an exposed python object

classA{
  public:
  //...
}

classB{
  public:
  //...
  A a;
  int c;
};

//...
class_<A>("A");

class_<B>("B")
  .def_readwrite("a",&B::a)
  .def_readonly("c",&B::c)
  ;

Return an internal reference

class_<ScRelaxationEngine,bases<asrl::sc_solver::SolverV2>> ("ScRelaxationEngine",init<>())
  .def("relax",&ScRelaxationEngine::relax)
  .def("poseGraph",&ScRelaxationEngine::poseGraph, return_internal_reference<>())
  ;

Define a subclass to a class that already has a boost::python interface

[http://wiki.python.org/moin/boost.python/OverridableV irtualFunction](http://wiki.python.org/moin/boost.python/OverridableV irtualFunction)

// SolverV2 is pure virual... I'm not totally sure how this all works.
// Now subclasses should be declared as
//class_<XXX,bases<asrl::sc_solver::Setup>>("XXX")
class_<SolverV2,boost::noncopyable>("SolverV2",no_init)
  .def("numSparseBlocks",&SolverV2::numSparseBlocks)
  .def("getDenseBlockDimension",&SolverV2::getDenseBlockDimension)
  .def("getSparseBlockDimension",&SolverV2::getSparseBlockDimension)
  ;

class_<ScRelaxationEngine,bases<asrl::sc_solver::SolverV2>> ("ScRelaxationEngine",init<>())
  .def("relax",&ScRelaxationEngine::relax)
  .def("poseGraph",&ScRelaxationEngine::poseGraph, return_internal_reference<>())
  ;

For some unknown reason, when I extended a non-abstract base class, I got an error like:

ArgumentError: Python argument types in
    BaseClass.functionName(DerivedClass)
did not match C++ signature:
    functionName(aslam::BaseClass*)

To fix it, I had to add

    boost::python::implicitly_convertible<DerivedClass,BaseClass>();

to the wrapper

Wrap an enum

using namespace boost::python;

enum color { red = 1, green = 2, blue = 4 };

color identity_(color x) { return x; }

BOOST_PYTHON_MODULE(enum_ext)
{
  enum_<color>("color")
    .value("red", red)
    .value("green", green)
    .value("blue", blue)
    ;

  def("identity", identity_);
}

Start Using it

In a python shell type (e.g. use ipython):

> import roslib
> roslib.load_manifest("MYPACKAGE")
> import MYPACKAGE

Helpful Links