foolyc

cpp python bingdings

刚好趁ubuntu系统崩溃重装系统的闲暇之际,对之前用python3封装KDL库过程中的一些经验进行一番总结。

常见python封装方式

工作中经常会有在python层调用c/cpp实现的功能的需求,常用的封装方式及工具如下:

SWIG

  • 支持 Python 2 and 3
  • 配置正确的话,可以全自动完成封装(*.i文件需要自己写)
  • 当不是全自动的时候,它大多会重复你的.h文件并给出提示
  • 除了Python外,还支持其他语言(Java, Ruby, Lua, 等)
  • 输出一个本地文件(这个文件会被编译成.pyd)和一个封装(这个封装是python脚本,调用对应生成的.pyd)
  • 绑定(Bindings)的性能不是太好,不支持内部类(inner classes)的封装
  • 不支持属性(通过getter/setters访问values)
  • 文档很全,很容易学习
  • google使用了
  • C++支持不太好

Boost::Python

  • 支持 Python 2, 3和C++的绑定
  • 对于新手来说,学习它的语法有一定难度。但是这些语法很直观
  • 大量使用了 C++ templates (可能是好事,也可能是坏事),会很明显地提高编译时间
  • 随boost库一起发布,且boost库有用,但很大
  • 刚开始编译使用boost.python封装好的C++代码时,出现的各种错误
  • 一些语法不易学习,像函数返回值时的规则
  • 非常可靠、稳定、经过充分测试的库(boost库里的部件都具有这特性)
  • 不支持属性
  • 支持文档写得比较差,有些功能文档里甚至都没有写。
    编译后的pyd文件有些大,这不利于在手机或者嵌入平台使用
  • 配合py++使用,几乎可以全自动封装项目

Py++

  • 支持 Python 2, 3 和C++的绑定
  • 它调用boost.python自动完成项目绑定,相当于boost.python的高级工具

SIP

  • 支持 Python 2, 3 和C++的绑定
  • 在PyQt中使用过,其他地方很少见它(原始的orocos KDL也使用SIP封装)

ctypes

  • python自带库
  • 对纯C代码几乎无缝调用
  • 仅支持C语音
  • 使用起来繁琐

PyCXX

  • 支持 Python 2, 3 和C++的绑定
  • 轻量级的封装库,用的人挺多
  • 只支持C++
  • 目前没有自动绑定工具

参考:http://blog.csdn.net/lainegates/article/details/19565823

ctypes库调用纯C库

前面提到对于纯C写成的库,直接用ctypes调用即可,比如我们之前在python里使用can驱动,以及deepmind最新发布的dm_control也是采用ctypes调用mujoco150。

简单示例:

1
2
3
from ctypes import *
mjlib = cdll.LoadLibrary("libmujoco150.so")
mjlib.mj_activate("mjkey.txt")

该示例加载mujoco动态链接库,并调用mj_activate激活。

boost python封装

鉴于个人认为boost python达到了工业级封装的要求,因此当时封装KDL时采用了boost python的方案,下面主要介绍boost python中对cpp中常见代码的处理。

Boost Python封装

函数封装

对于简单函数封装,直接定义即可

1
bp::def("JntToCart",JntToCart);

简单类封装

简单的类及结构体封装示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct World
{
void set(std::string msg) { this->msg = msg; }
std::string greet() { return msg; }
std::string msg;
};
BOOST_PYTHON_MODULE(hello)
{
class_<World>("World")
.def("greet", &World::greet)
.def("set", &World::set)
;
}

有多个构造函数的,对应多个init即可

1
2
3
4
5
class_<World>("World", init<std::string>())
.def(init<double, double>())
.def("greet", &World::greet)
.def("set", &World::set)
;

如果不希望暴露任何构造函数

1
class_<Abstract>("Abstract", no_init)

Operator处理

目前Boost Python支持常见符号重载,以KDL中Vector对象为例,以下代码实现+、-重载操作

1
2
Vector_exposer.def( bp::self += bp::self );
Vector_exposer.def( bp::self -= bp::self );

成员变量

以Frame类中M和p为例

1
2
Frame_exposer.def_readwrite( "M", &KDL::Frame::M );
Frame_exposer.def_readwrite( "p", &KDL::Frame::p );

类继承

以KDL::ChainIkSolverPos为例,我们知道其继承自KDL::SolverI,封装该类时

1
bp::class_< ChainIkSolverPos_wrapper, bp::bases< KDL::SolverI >, boost::noncopyable >( "ChainIkSolverPos" )

虚函数

以KDL::ChainFkSolverPos为例,JntToCart为其纯虚函数

1
2
3
4
.def(
"JntToCart"
, bp::pure_virtual( (int ( ::KDL::ChainFkSolverPos::* )( ::KDL::JntArray const &,::KDL::Frame &,int ))(&::KDL::ChainFkSolverPos::JntToCart) )
, ( bp::arg("q_in"), bp::arg("p_out"), bp::arg("segmentNr")=(int)(-1) ) )

对于带实现的虚函数,以KDL::ChainFkSolverPos_recursive中的JntToCart为例:

1
2
3
4
5
6
7
8
9
10
11
12
{ //::KDL::ChainFkSolverPos_recursive::JntToCart
typedef int ( ::KDL::ChainFkSolverPos_recursive::*JntToCart_function_type)( ::KDL::JntArray const &,::KDL::Frame &,int ) ;
typedef int ( ChainFkSolverPos_recursive_wrapper::*default_JntToCart_function_type)( ::KDL::JntArray const &,::KDL::Frame &,int ) ;
ChainFkSolverPos_recursive_exposer.def(
"JntToCart"
, JntToCart_function_type(&::KDL::ChainFkSolverPos_recursive::JntToCart)
, default_JntToCart_function_type(&ChainFkSolverPos_recursive_wrapper::default_JntToCart)
, ( bp::arg("q_in"), bp::arg("p_out"), bp::arg("segmentNr")=(int)(-1) ) );
}

默认参数处理

如果C++中函数有默认参数,必须特殊处理,以下简单示例:

1
int f(int, double = 3.14, char const* = "hello");

封装过程,必须对应不同参数省略形式:

1
2
3
4
5
6
7
8
int(*g)(int,double,char const*) = f; // defaults lost!
int f1(int x) { return f(x); }
int f2(int x, double y) { return f(x,y); }
// in module init
def("f", f); // all arguments
def("f", f2); // two arguments
def("f", f1); // one argument

枚举类型处理

对于枚举类型,以JointType为例:

1
2
3
4
5
6
7
8
9
10
11
12
bp::enum_< KDL::Joint::JointType>("JointType")
.value("RotAxis", KDL::Joint::RotAxis)
.value("RotX", KDL::Joint::RotX)
.value("RotY", KDL::Joint::RotY)
.value("RotZ", KDL::Joint::RotZ)
.value("TransAxis", KDL::Joint::TransAxis)
.value("TransX", KDL::Joint::TransX)
.value("TransY", KDL::Joint::TransY)
.value("TransZ", KDL::Joint::TransZ)
.value("NoJoint", KDL::Joint::None)
.export_values()
;

注:之前将KDL::Joint::None直接封装为None使用过程会报错,不太清楚其机制和原理,因此改为NoJoint。

数据切片处理

对于KDL::Vector中三个变量,我们希望封装之后在python层可以像List或者numpy数组一样方便的使用切片[]操作来访问和操作,以操作为例,构造操作函数:

1
2
3
4
5
6
7
8
9
10
11
void Vector_setitem(KDL::Vector& v, int index, double value)
{
if (index >= 0 && index < 3)
{
v[index] = value;
}
else {
PyErr_SetString(PyExc_IndexError, "index out of range");
bp::throw_error_already_set();
}
}

封装时将操作函数封装为setitem即可

1
Vector_exposer.def("__setitem__", &Vector_setitem);

Py++实现自动化封装

使用中需要安装gccxml和pygccxml,使用pygccxml自动生成封装cpp文件

1
2
3
4
5
6
7
8
9
10
11
12
13
import os
import sys
from pyplusplus import module_builder
mb = module_builder.module_builder_t(
files=['kdl/joint.hpp'] #要需要封装的头文件
,gccxml_path='/usr/local/bin/gccxml') # 根据实际安装位置确定
mb.build_code_creator( module_name='PyKDL' ) #要生成的python模块的名称
mb.code_creator.user_defined_directories.append( os.path.abspath('.') )
mb.write_module( os.path.join( os.path.abspath('.'), 'PyKDL.cpp' ) ) #要生成的boost.python封装好的代码文件的名称

注:我当时的代码已经丢失,该脚本仅供理解(实际使用需要修改)。

一般来说Py++生成的封装cpp手动代替了很多工作,对于简单封装也可以直接使用,部分情况会需要人手动继续修改生成的cpp文件。

总结

前面简单总结了在使用Boost Python封装KDL过程中的一些经验,仅供参考。

封装的PyKDL位于 https://github.com/foolyc/PyKDL ,如果在python3环境中有需要,欢迎下载使用。

本文由foolyc创作和发表,采用BY-NC-SA国际许可协议进行许可
转载请注明作者及出处,本文作者为foolyc
本文标题为cpp python bingdings
本文链接为http://foolyc.com//2018/01/14/cpp-python-bingdings/.