2014年10月03日

Python C++绑定


这篇博客用来记录工作使用到的Python C++绑定,不涉及深入的分析。众所周知,人生苦短,何不用python。这儿提到的绑定(banding)均是指使用python来调用C++相关的函数或方法。

Python可以方便的集成其他语言,对C/C++的集成,存在很多工具,均列在了此wiki中。

工作中需要使用的场景是: 以存在基于C++开发的系统,需要对其中部分的类,函数进行处理,然后使得Python能够像使用普通python modules一样,使用这些类和方法。经过Google的简单调研,发现Boost.python工具简单易用,学习成本低,并且存在的资料较多,若遇到问题,能够找到大量参考。

参考资料


由于下面的内容是基于自己的简单实践,涵盖的面略窄,为方便大家更好的理解Python与C/C++的集成,所以先贴出参考的文档,快速扫一下这些文档,基本上就可以迅速动手热火朝天的干起来了。

1 Python与C集成: 基于Python的C objects来完成的集成,包括Python调用C,C调用Python

2 浅谈Python程序和C程序的整合: 基于python module ctypes完成的集成,没错,很浅

3 Python与C++的交互编程 : 侧重介绍基本概念和Boost.Python的基本使用

4 基于Boost.Python库的混合语言编程及其应用: Boost.Python的入门教程

5 Boost.Python: Boost官方文档,还是强烈推荐,介绍的内容更加基础,全面

6 Boost.Python: Python Wiki上的官方文档,介绍了较多的实用问题

7 Boost.Python教程: 这是篇Boost.Python的中文简明教程,文章内容和排版都很舒服,貌似需要翻墙看

8 Python与C++联合编程的简介: 简单介绍比较了几种解决方法

对上面的文档做个简单的总结(若有误,欢迎指正): Python中调用C

实际中,可以将C语言的project编译成动态库,使用ctypes加载动态库,使用ctypes提供的函数等完成动态库函数的调用,可以参考文档2中关于散列代码的实现,例如调用动态库的某个函数,参数类型是char*类型,则需要使用c_char_p来完成python类型到c类型的转换。在虚拟化技术中,使用的libvirt接口提供了python绑定,可以参考该banding的实现,直接使用的是python c objects,编写接口,和libvirt源码结合完美。

python中调用C++,使用boost.python封装一下类,函数,然后编译成动态库,在python中,直接import。

问题汇总


上面一节提到的文档可以完成Boost.Python使用的入门,特别是文档7,可读性极强。下面则记录在用的过程中,遇到的一些问题。

在C++代码中,存在vector<T>这样的模板类,那么如何wrap让python使用了?

python中对应的类型是list,因此需要进行封装转换下,可以使用如下的方式:

class Test;
typedef std::vector<Test> TestVec;
BOOST_PYTHON_MODULE(test){
    class_<TestVec>("TestVec")
        .def(vector_indexing_suite<TestVec>());
}

可以参考StackOverflow上的问题std::vector to boost::python::list。对于vector,可以这么做,对于map, set 等其他的容器,是否存在同样的解决方法,需要Google下,可能需要自己再wrap一下吧。在使用vector_indexing_suite的时候,注意其中的TestVec可能需要重载operator==函数,编译时若遇到问题,直接Google。

shared_ptr<T>的转换,C++中函数的参数是shared_ptr<T>,那么python中该如何处理了?

这中间还有一个问题,可能C++使用的std::tr1::shared_ptr,而boost中是boost::shared_ptr,还有此二者之间的转换。可以参考论坛中的帖

std::tr1::shared_ptr as boost::shared_ptr

现有的系统中基本上都是使用tr1::shared_ptr,例如我们需要暴露TestClient类,系统中都是使用的std::tr1::shared_ptr智能指针,目前是尝试这样处理的。注意其中的命名空间。

namespace choudan{
    namespace test{
        template<class T>
        inline T* get_pointer(std::tr1::shared_ptr<T> const& p){
            return p.get();
        }

        template<class T>
        inline T* get_pointer(std::tr1::shared_ptr<const T> const& p){
            return const_cast<T*>(p.get());
        }
    };
};
namespace boost{
    namespace python{
        template<class T>        
        struct pointee<std::tr1::shared_ptr<T> >{
            typedef T type;
        };

        template<class T>
        struct pointee<std::tr1::shared_ptr<const T> >{
            typedef T type;
        };
    }
};

BOOST_PYTHON_MODULE(spamcaller){
    class_<TestClient, std::tr1::shared_ptr<TestClient> >("TestClient")
        .def_readwrite("debug", &TestClient::debug_);
}

引用传参,C++使用引用方式传入参数,希望函数的调用修改参数的值,在Python中怎么处理?

如果对于这样的函数调用没有wrap一下,Python中并不是简单的统一按值传参或者按引用传参,所以直接调用这样的函数,是不行的,需要做一个简单的wrap,现在尝试采用的方法很简单,提供新的函数,封装需要调用的函数,即存在一个中间层(新的函数),中间层处理传引用返回值等问题,python直接调用新函数。

多个构造方法,C++的类存在多个构造方法,需要怎么处理?

C++的构造函数可以通过Boost.Python中提供的init方法来实现,代码如下:

class_<World>("World", init<std::string>())
    .def(init<double, double>())
    .def("greet", &World::greet)
    .def("set", &World::set);

类的静态成员函数

例如存在如下的类:

class Foo{     
    public:
       static void show(){
            std::cout << "Static Method" << std::endl;
       }
       static Foo* example();
 }

Foo类中存在两个静态函数,一个无返回值,一个返回值是指针,所以需要进行转换,让Python能够接受这两个函数,知道是静态的。主要参考于python mail list中的Static functions in Boost.Python

BOOST_PYTHON_MODULE(foo){
     class_<Foo>("Foo")
           .def("show", &Foo::show)
           .staticmethod("show");
           .def("example", &Foo::example, return_value_policy<manage_new_object>())
           .staticmethod("example")
 } 

注意example函数的处理,由于返回的是指针(引用同样适用),所以使用call policy 方式。

动态库中的输出


直接将已有的C++实现通过boost.python处理后编译成动态库,若C++代码中存在std::cout这样的语句,那么最后在python中使用时,C++部分的输出和Python部分的输出都会打到屏幕上。对于C++代码中的输出,可能存在这样的预期:

因此,需要在Python调用该动态库时,做个简单的处理。处理的思路是输出重定向。

1 在python中存在sys.stdout对象,我们不能简单的修改该对象的值而达到目的。在python官方文档中描述到,sys.stdout只是简单的封装了标准输出,改变sys.stdout的值会影响python进程,而不会印象潜在的文件描述符,任何其他的非python代码,包括exec和动态库中的代码,依然使用正常的文件描述符。

2 上面的问题就涉及到了进程间的通信,思路是采用pipe和dup来完成对标准输出的截取。请参考StackOverflow上的问题how to capture the stdout from a c++ shared library to a variable。这个问题的接受答案中给出了详细的解释和实现。

基于StackOverflow上给出的思路,我们可以利用Python的decorator方法,来灵活的实现动态库的输出捕捉。

44 def capture_stdout(func):
45     def wrap(*args, **kwargs):
46         stdout_fo = sys.stdout.fileno()
47         oldstdout = os.dup(stdout_fo)
48         r,w = os.pipe()
49         os.dup2(w, stdout_fo)
50         os.close(w)
51         capture = ['']
52         def drain_pipe():
53             #global capture
54             while True:
55                 data = os.read(r, 1024)
56                 if not data:
57                     break
58                 capture[0] += data
59         t = threading.Thread(target=drain_pipe)
60         t.start()
61         func(*args, **kwargs)
62         os.close(stdout_fo)
63         t.join()
64         os.close(r)
65         os.dup2(oldstdout, stdout_fo)
66         os.close(oldstdout)
67     return wrap

在这份代码中,需要注意两点:

1 def drain_pipe函数存在的意义: 防止死锁。

2 51行capture的定义,定义成了一个列表,在内部函数drain_pipe中访问了该函数,这涉及到了变量的namespace问题和mutable or imutable问题。请参考此链接python overwriting varaibles in nested functions

总结


以上的内容可能描述不准确,提到的问题都是实践过程中遇到的问题,亲自验证,但是方法不一定是最好的,若有错误或者更好的方法,欢迎交流。

前一篇: 小结(一) 后一篇: 网络学习点滴(一)