Please ask about problems and questions regarding this tutorial on answers.ros.org. Don't forget to include in your question the link to this page, the versions of your OS & ROS, and also add appropriate tags. |
Using a C++ class in Python
Description: This tutorial illustrates a way to use a C++ class with ROS messages in Python.Keywords: C++, Python, bindings
Tutorial Level: ADVANCED
Contents
This tutorial illustrates a way to use a C++ class with ROS messages in Python. The Boost Python library is used. The difficulty is to translate Python objects of ROS messages written in pure Python into equivalent C++ instances. This translation will be done through serialization/deserialization.
Another solution is to use classical ROS services, the server written in C++ will be a wrapper around the C++ class and the client, C++ or Python, will call the service. The solution proposed here does not create a ROS node, provided the class to be wrapped does not make use of ros::NodeHandle.
Another solution is to write a wrapper for all needed ROS messages and their dependencies. Some apparently deprecated package proposed some solutions for the automation of this task: genpybindings and boost_ python_ros.
Class without NodeHandle
Because roscpp is not initialized when calling rospy.init_node. ros::NodeHandle instances cannot be used in the C++ class without generating an error. If the C++ does not make use of ros::NodeHandle, this is no issue though.
Creating the package and writing the C++ class
Create a package and create the C++ class for which we will want to make a Python binding. This class uses ROS messages as arguments and return type.
The content of include/python_bindings_tutorial/add_two_ints.h will be:
1 #ifndef PYTHON_BINDINGS_TUTORIAL_ADD_TWO_INTS_H
2 #define PYTHON_BINDINGS_TUTORIAL_ADD_TWO_INTS_H
3
4 #include <std_msgs/Int64.h>
5
6 namespace python_bindings_tutorial {
7
8 class AddTwoInts
9 {
10 public:
11 std_msgs::Int64 add(const std_msgs::Int64& a, const std_msgs::Int64& b);
12 };
13
14 } // namespace python_bindings_tutorial
15
16 #endif // PYTHON_BINDINGS_TUTORIAL_ADD_TWO_INTS_H
17
Write the class implementation into .
roscd python_bindings_tutorial/src touch add_two_ints.cpp rosed python_bindings_tutorial add_two_ints.cpp
The content of src/add_two_ints.cpp will be:
Binding, C++ part
The binding occurs through two wrapper classes, one in C++ and one in Python. The C++ wrapper translates input from serialized content to C++ message instances and output from C++ message instances into serialized content.
1 roscd python_bindings_tutorial/src
2 touch add_two_ints_wrapper.cpp
3 rosed python_bindings_tutorial add_two_ints_wrapper.cpp
The content of src/add_two_ints_wrapper.cpp will be:
1 #include <boost/python.hpp>
2
3 #include <string>
4
5 #include <ros/serialization.h>
6 #include <std_msgs/Int64.h>
7
8 #include <python_bindings_tutorial/add_two_ints.h>
9
10 /* Read a ROS message from a serialized string.
11 */
12 template <typename M>
13 M from_python(const std::string str_msg)
14 {
15 size_t serial_size = str_msg.size();
16 boost::shared_array<uint8_t> buffer(new uint8_t[serial_size]);
17 for (size_t i = 0; i < serial_size; ++i)
18 {
19 buffer[i] = str_msg[i];
20 }
21 ros::serialization::IStream stream(buffer.get(), serial_size);
22 M msg;
23 ros::serialization::Serializer<M>::read(stream, msg);
24 return msg;
25 }
26
27 /* Write a ROS message into a serialized string.
28 */
29 template <typename M>
30 std::string to_python(const M& msg)
31 {
32 size_t serial_size = ros::serialization::serializationLength(msg);
33 boost::shared_array<uint8_t> buffer(new uint8_t[serial_size]);
34 ros::serialization::OStream stream(buffer.get(), serial_size);
35 ros::serialization::serialize(stream, msg);
36 std::string str_msg;
37 str_msg.reserve(serial_size);
38 for (size_t i = 0; i < serial_size; ++i)
39 {
40 str_msg.push_back(buffer[i]);
41 }
42 return str_msg;
43 }
44
45 class AddTwoIntsWrapper : public python_bindings_tutorial::AddTwoInts
46 {
47 public:
48 AddTwoIntsWrapper() : AddTwoInts() {}
49
50 std::string add(const std::string& str_a, const std::string& str_b)
51 {
52 std_msgs::Int64 a = from_python<std_msgs::Int64>(str_a);
53 std_msgs::Int64 b = from_python<std_msgs::Int64>(str_b);
54 std_msgs::Int64 sum = AddTwoInts::add(a, b);
55
56 return to_python(sum);
57 }
58 };
59
60 BOOST_PYTHON_MODULE(_add_two_ints_wrapper_cpp)
61 {
62 boost::python::class_<AddTwoIntsWrapper>("AddTwoIntsWrapper", boost::python::init<>())
63 .def("add", &AddTwoIntsWrapper::add)
64 ;
65 }
The line Error: No code_block found creates a Python module in the form of a dynamic library. The name of the module will have to be the name of the exported library in CMakeLists.txt.
Binding, Python part
The Python wrapper translates input from Python message instances into serialized content and output from serialized content to Python message instances. The translation from Python serialized content (str) into C++ serialized content (std::string) is built in the Boost Python library.
roscd python_bindings_tutorial/src mkdir python_bindings_tutorial roscd python_bindings_tutorial/src/python_bindings_tutorial touch _add_two_ints_wrapper_py.py rosed python_bindings_tutorial _add_two_ints_wrapper_py.py
The content of src/python_bindings_tutorial/_add_two_ints_wrapper_py.py will be
1 from StringIO import StringIO
2
3 import rospy
4 from std_msgs.msg import Int64
5
6 from python_bindings_tutorial._add_two_ints_wrapper_cpp import AddTwoIntsWrapper
7
8
9 class AddTwoInts(object):
10 def __init__(self):
11 self._add_two_ints = AddTwoIntsWrapper()
12
13 def _to_cpp(self, msg):
14 """Return a serialized string from a ROS message
15
16 Parameters
17 ----------
18 - msg: a ROS message instance.
19 """
20 buf = StringIO()
21 msg.serialize(buf)
22 return buf.getvalue()
23
24 def _from_cpp(self, str_msg, cls):
25 """Return a ROS message from a serialized string
26
27 Parameters
28 ----------
29 - str_msg: str, serialized message
30 - cls: ROS message class, e.g. sensor_msgs.msg.LaserScan.
31 """
32 msg = cls()
33 return msg.deserialize(str_msg)
34
35 def add(self, a, b):
36 """Add two std_mgs/Int64 messages
37
38 Return a std_msgs/Int64 instance.
39
40 Parameters
41 ----------
42 - a: a std_msgs/Int64 instance.
43 - b: a std_msgs/Int64 instance.
44 """
45 if not isinstance(a, Int64):
46 rospy.ROSException('Argument 1 is not a std_msgs/Int64')
47 if not isinstance(b, Int64):
48 rospy.ROSException('Argument 2 is not a std_msgs/Int64')
49 str_a = self._to_cpp(a)
50 str_b = self._to_cpp(b)
51 str_sum = self._add_two_ints.add(str_a, str_b)
52 return self._from_cpp(str_sum, Int64)
In order to be able to import the class as python_bindings_tutorial.AddTwoInts, we import the symbols in __init__.py. First, we create the file:
1 roscd python_bindings_tutorial/src/python_bindings_tutorial
2 touch __init__.py
3 rosed python_bindings_tutorial __init__.py
The content of src/python_bindings_tutorial/__init__.py will be:
1 from python_bindings_tutorial._add_two_ints_wrapper_py import AddTwoInts
Glueing everything together
Edit the CMakeLists.txt (rosed python_bindings_tutorial CmakeLists.txt) like this:
Adapt CMakeLists.txt from the catkin version of this page.
cmake_minimum_required(VERSION 2.8.3) project(python_bindings_tutorial) find_package(catkin REQUIRED COMPONENTS roscpp roscpp_serialization std_msgs ) ## Both Boost.python and Python libs are required. find_package(Boost REQUIRED COMPONENTS python) find_package(PythonLibs 2.7 REQUIRED) ## Uncomment this if the package has a setup.py. This macro ensures ## modules and global scripts declared therein get installed ## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html catkin_python_setup() ################################### ## catkin specific configuration ## ################################### catkin_package( INCLUDE_DIRS include LIBRARIES add_two_ints _add_two_ints_wrapper_cpp CATKIN_DEPENDS roscpp # DEPENDS system_lib ) ########### ## Build ## ########### # include Boost and Python. include_directories( include ${catkin_INCLUDE_DIRS} ${Boost_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS} ) ## Declare a cpp library add_library(add_two_ints src/add_two_ints.cpp) add_library(_add_two_ints_wrapper_cpp src/add_two_ints_wrapper.cpp) ## Specify libraries to link a library or executable target against target_link_libraries(add_two_ints ${catkin_LIBRARIES}) target_link_libraries(_add_two_ints_wrapper_cpp add_two_ints ${catkin_LIBRARIES} ${Boost_LIBRARIES}) # Don't prepend wrapper library name with lib and add to Python libs. set_target_properties(_add_two_ints_wrapper_cpp PROPERTIES PREFIX "" LIBRARY_OUTPUT_DIRECTORY ${CATKIN_DEVEL_PREFIX}/${CATKIN_PACKAGE_PYTHON_DESTINATION} )
The c++ wrapper library should be have the same name as the Python module. If the target name needs to be different for a reason, the library name can be specified with set_target_properties(_add_two_ints_wrapper_cpp PROPERTIES OUTPUT_NAME correct_library_name).
The line
catkin_python_setup()
is used to export the Python module and is associated with the file setup.py
The content of setup.py will be:
1 # ! DO NOT MANUALLY INVOKE THIS setup.py, USE CATKIN INSTEAD
2
3 from distutils.core import setup
4 from catkin_pkg.python_setup import generate_distutils_setup
5
6 # fetch values from package.xml
7 setup_args = generate_distutils_setup(
8 packages=['python_bindings_tutorial'],
9 package_dir={'': 'src'})
10
11 setup(**setup_args)
We then build the package with rosmake.
We then build the package with catkin_make.
Testing the binding
You can now use the binding in Python scripts or in a Python shell
Class with NodeHandle
As stated, a Python call to rospy.init_node does not initialize roscpp. If roscpp is not initialized, instanciating ros::NodeHandle will lead to a fatal error. A solution for this is provided by the moveit_ros_planning_interface. In any Python script that uses the wrapped class, two lines need to be added before instanciating AddTwoInts:
This will create a ROS node. The advantage of this method over a classical ROS service server/client implementation is thus not as clear as in the case without the need of ros::NodeHandle.
Class with container of ROS messages
If the class uses containers of ROS messages, an extra conversion step must be added. This step is not specific to ROS but is part of the Boost Python library.
`std::vector<M>` as return type
In the file where the C++ wrapper class is defined, add these lines:
1 // Extracted from https://gist.github.com/avli/b0bf77449b090b768663.
2 template<class T>
3 struct vector_to_python
4 {
5 static PyObject* convert(const std::vector<T>& vec)
6 {
7 boost::python::list* l = new boost::python::list();
8 for(std::size_t i = 0; i < vec.size(); i++)
9 (*l).append(vec[i]);
10
11 return l->ptr();
12 }
13 };
14
15 class Wrapper : public WrappedClass
16 {
17 /*
18 ...
19 */
20 std::vector<std::string> wrapper_fun(const std::string str_msg)
21 {
22 /* ... */
23 }
24
25 };
26
27 BOOST_PYTHON_MODULE(module_wrapper_cpp)
28 {
29 boost::python::class_<Wrapper>("Wrapper", bp::init</* ... */>())
30 .def("fun", &Wrapper::wrapper_fun);
31
32 boost::python::to_python_converter<std::vector<std::string, std::allocator<std::string> >, vector_to_python<std::string> >();
33 }
`std::vector<M>` as argument type
Cf. Boost.Python documentation.