Python 进阶

我们在Python进阶教程(二),介绍了一些Python进阶用法。今天给大家介绍的是和c/c++混合编程的用法。我们都知道特别是Python2.x版本GIL的影响造成多线程计算方面是鸡肋,并且常常在高性能计算方面依赖很多包。如果我们想切合我们自己的业务处理,比如高性能部分我们用C/C++生成动态链接库,Python调用该动态链接库。我们今天主要介绍Python在和c/c++混合编程。

Python和C/C++混合编程

在和C/C++代码进行混合编程时,我们通常需要采用以下几个技术点: * Ctypes:C++编译成动态链接库(DLL,即运行时动态调用),然后通过ctypes来解析和加载动态链接库,python调用该动态链接库。从ctypes的文档中可以推断,在各个平台上均使用了对应平台动态加载动态链接库的方法,并通过一套类型映射的方式将Python与二进制动态链接库相连接。ctypes 实现了一系列的类型转换方法,Python的数据类型会包装或直接推算为C类型,作为函数的调用参数;函数的返回值也经过一系列的包装成为Python类型。 优点: 1.Python内建,不需要单独安装 2.可以直接调用二进制的动态链接库 3.在Python一侧,不需要了解Python内部的工作方式 4.在C/C++一侧,也不需要了解Python内部的工作方式 5.对基本类型的相互映射有良好的支持 缺点: 1.平台兼容性差 2.不能够直接调用动态链接库中未经导出的函数或变量 3.对C++的支持差 * SWIG:通过提供的接口文件来调用。 * Python/C在C中直接扩展Python的代码。

Using C from Python(Ctypes)

我们通过一个C的源代码函数,我们来看一下,我们新建一个C函数文件叫做test.c。比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>
int add_int(int, int);
float add_float(float, float);

int add_int(int x, int y)
{
  return x + y;
}

float add_float(float x, float y)
{
  return x + y;
}

然后将该test.c文件生成动态链接库add.so,通过如下命令:

1
gcc -shared -Wl,-install_name,add.so -o add.so -fPIC test.c

-shared代表这是动态库,-fPIC使得位置独立,-o指定了输出文件,改成dll后缀一样可以用。可以在当前目录看到有一个文件add.so

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from ctypes import *

#load the shared object file
adder = CDLL('./add.so')

#Find sum of integers
res_int = adder.add_int(4,5)
print "Sum of 4 and 5 = " + str(res_int)

#Find sum of floats
a = c_float(5.5)
b = c_float(4.1)

add_float = adder.add_float
#这个非常重要,如果你没有给返回值指定输出Python ctypes的数据类型则会出现异常值。
add_float.restype = c_float
print "Sum of 5.5 and 4.1 = ", str(add_float(a, b))
#输出为
Sum of 4 and 5 = 9
Sum of 5.5 and 4.1 =  10.6000003815

在指定ctypes的数据格式时一定要注意数据类型的匹配[^test1]。我们来看一下数据格式的匹配[^test]: Ctypes 数据格式 更详细的使用ctypes教程[ctypes tutorial][1]

Using C++ from Python(Ctypes)

ctypes不仅可以和C在一混合编程,我们还可以和C++混合编程,但是在ctypes的官方文档里面只定义了支持C Type。那么c++如何和Python进行混合编程呢?所以需要在C++里面通过extend c来支持C++混合编程。 我们来看两个例子,第一个例子由两个文件构成一个是test.h,test.cpp,其中test.h是c++自定义的头文件,test.cpp是对外接口。依次来看看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#test.h
#ifndef _Foo_h
#define _Foo_h
#include <iostream>

class Foo
{
public:
  Foo(int);
  void bar();
  int foobar(int);

private:
  int val;
};

Foo::Foo(int n)
{
  val = n;
}

void Foo::bar()
{
  std::cout << "Value is " << val << std::endl;
}

int Foo::foobar(int n)
{
  return val + n;
}

#endif // !_Foo_h

我们来看一下它的对外接口test.cpp是什么样的?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include "test.h"
//值得注意的是extern "C"
extern "C" {
Foo *Foo_new(int n)
{
  return new Foo(n);
}
void Foo_bar(Foo *foo)
{
  foo->bar();
}
int Foo_foobar(Foo *foo, int n)
{
  return foo->foobar(n);
}
}

这个例子非常不错不仅有对象,指针和普通参数。这个我是通过cmake和make来进行编译和处理的,我们看一下生成动态链接库。

1
2
3
4
cmake_minimum_required(VERSION 3.0)
project (Demo)
set (SOURCE test.cpp)
add_library(${PROJECT_NAME} SHARED ${SOURCE})

然后执行下面两条命令

1
2
3
4
cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug ..
make -j 8
#在当前目录就会出现下面两个命令
libDemo.dylib

我们来看一下Python是如何调用该接口的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import ctypes
lib = ctypes.cdll.LoadLibrary("./libDemo.dylib")
class Foo(object):
    def __init__(self, val):
        lib.Foo_new.argtypes = [ctypes.c_int]
        lib.Foo_new.restype = ctypes.c_void_p
        lib.Foo_bar.argtypes = [ctypes.c_void_p]
        lib.Foo_bar.restype = ctypes.c_void_p
        lib.Foo_foobar.argtypes = [ctypes.c_void_p, ctypes.c_int]
        lib.Foo_foobar.restype = ctypes.c_int
        self.obj = lib.Foo_new(val)

    def bar(self):
        lib.Foo_bar(self.obj)
    def foobar(self, val):
        return lib.Foo_foobar(self.obj, val)

f=Foo(5)
f.bar()
print (f.foobar(7))
x = f.foobar(2)
print (type(x))
#输出
12
<type 'int'>

我们来看另外一个例子,也是通过动态链接库来处理的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <iostream>
#include <string>
using namespace std;
typedef struct StructTest
{
  char *name;
  int age;
  int score[3];
} StructTest, *StructPtr;

class TestLib
{
public:
  int passInt(int a);
  double passDouble(double d);
  char passChar(char c);
  char *passString(char *s);
  StructTest passStruct(StructTest st);
  StructPtr passStructPtr(StructPtr p);
  StructPtr passStructArray(StructTest vst[], int size);
};

int TestLib::passInt(int a)
{
  cout << a << " in c++" << endl;
  return a;
}
double TestLib::passDouble(double d)
{
  cout << d << " in c++" << endl;
  return d;
}
char TestLib::passChar(char c)
{
  cout << c << " in c++" << endl;
  return c;
}
char *TestLib::passString(char *s)
{
  cout << s << " in c++" << endl;
  return s;
}
StructTest TestLib::passStruct(StructTest st)
{
  cout << st.name << " " << st.age << " " << st.score[2] << " in c++" << endl;
  return st;
}
StructPtr TestLib::passStructPtr(StructPtr p)
{
  cout << p->name << " " << p->age << " " << p->score[2] << " in c++" << endl;
  return p;
}
StructPtr TestLib::passStructArray(StructTest vst[], int size)
{
  cout << vst[0].name << " in c++" << endl;
  cout << vst[1].name << " in c++" << endl;
  return &vst[0];
}
extern "C" {
TestLib obj;
int passInt(int a)
{
  return obj.passInt(a);
}
double passDouble(double d)
{
  return obj.passDouble(d);
}
char passChar(char c)
{
  return obj.passChar(c);
}
char *passString(char *s)
{
  return obj.passString(s);
}
StructTest passStruct(StructTest st)
{
  return obj.passStruct(st);
}
StructPtr passStructPtr(StructPtr p)
{
  return obj.passStructPtr(p);
}
StructPtr passStructArray(StructTest vst[], int size)
{
  return obj.passStructArray(vst, size);
}
}

我们来看一下python相应的代码,改代码实例来自[参考2],我们看看Python是如何调用的?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import ctypes
lib = ctypes.cdll.LoadLibrary('./libTest.so')
# passInt
print('passInt')
print(lib.passInt(100))
print('--------------------')
# passDouble
print('passDouble')
lib.passDouble.restype = ctypes.c_double
print(str(lib.passDouble(ctypes.c_double(1.23))) + ' in python')
print('--------------------')
# passChar
print('passChar')
lib.passChar.restype = ctypes.c_char
print(str(lib.passChar(ctypes.c_char(65))) + ' in python') # 'A'
print(str(lib.passChar(ctypes.c_char(b'A'))) + ' in python') # 'A'
print('--------------------')
# passString
print('passString')
lib.passString.restype = ctypes.c_char_p
print(str(lib.passString(ctypes.c_char_p(b'abcde'))) + ' in python') # 这里一定要加b
print('--------------------')
# passStruct
print('passStruct')
class Struct(ctypes.Structure):
    _fields_ = [('name', ctypes.c_char_p),
                ('age', ctypes.c_int),
                ('score', ctypes.c_int * 3)]
lib.passStruct.restype = Struct
array = [1, 2, 3]
st = lib.passStruct(Struct(b'xidui', 10, (ctypes.c_int * 3)(*array)))
# p = lib.passStruct(Struct(b'xidui', 10, (ctypes.c_int * 3)(1, 2, 3)))
print(str(st.name) + ' ' + str(st.age) + ' ' + str(st.score[2]) + ' in python')
print('--------------------')
# passStructPointer
print('passStructPointer')
lib.passStructPtr.restype = ctypes.POINTER(Struct)
lib.passStructPtr.argtypes = [ctypes.POINTER(Struct)] # 这行不加,程序会宕
p = lib.passStructPtr(Struct(b'xidui', 10, (ctypes.c_int * 3)(*array)))
print(str(p.contents.name) + ' ' + str(p.contents.age) + ' ' + str(p.contents.score[2]) + ' in python')
print('--------------------')
# passStructArray
print('passStructArray')
lib.passStructArray.restype = ctypes.POINTER(Struct)
lib.passStructArray.argtypes = [ctypes.ARRAY(Struct, 2), ctypes.c_int]
array = [Struct(b'xidui1', 10, (ctypes.c_int * 3)(1, 2, 3)),
    Struct(b'xidui2', 10, (ctypes.c_int * 3)(1, 2, 3))]
p = lib.passStructArray(ctypes.ARRAY(Struct, 2)(*array), 2)
print(str(p.contents.name) + ' ' + str(p.contents.age) + ' ' + str(p.contents.score[2]) + ' in python')
print('--------------------')

SWIG

SWIG (Simplified Wrapper and Interface Generator) 是用来为C/C++语言构造脚本语言接口的软件开发工具。SWIG 实际上是一个编译器,获取C/C++的声明,用一个壳包起来,以便通过其他语言访问这些声明。因此,SWIG 最大的好处就是将脚本语言的开发效率和 C/C++ 的运行效率结合起来。我们来看一个例子: c++源代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//File: JsonTool.h
int EnSerialization(const char *file);
int DeSerialization(const char *jsonFile);


//File: JsonTool.cpp
int EnSerialization(const char *file)
{
    ...
}

int DeSerialization(const char *jsonFile)
{
    ...
}

编写interface文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//第一种方式:
%module JsonTool
%{
#define SWIG_FILE_WITH_INIT
#include "JsonTool.h"
%}

int EnSerialization(const char *file);
int DeSerialization(const char *jsonFile);
//第二种方式(推荐使用)
//File: JsonTool.i
%module JsonTool
%{
#define SWIG_FILE_WITH_INIT
#include "JsonTool.h"
%}

%include "JsonTool.h"

.i接口文件中主要包含了三个部分: 1.%module后面的名字是被封装的模块名称,Python通过这个名称加载程序。 2.%{…%}之间所添加的内容,一般包含此文件需要的一些函数声明和头文件。 3.最后一部分,声明了要封装的函数和变量或者直接引用c++的head文件。

我们下面就是封装代码了。

1
swig -python -c++ JsonTool.i

执行完毕后会生成JsonTool.py和JsonTool_wrap.cxx这两个文件,相当于将原cpp文件进行了封装,wrap了一层。 最后生成动态链接库,我们之前介绍了通过g++。下面介绍通过setup自动化生成动态链接库。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from distutils.core import setup, Extension
pht_json_module = Extension('_JsonTool', #模块名称,必须要有下划线
                        sources=['JsonTool_wrap.cxx', #封装后的接口cxx文件
                                 'base32.cpp',		#以下为原始代码所依赖的文件
                                 'JsonTool.cpp',
                                 'md5.cpp',
                                 'sha1.cpp',
                                 'stdafx.cpp'
                                ],
                      )

setup(name = 'JsonTool',	#打包后的名称
        version = '0.1',
        author = 'SWIG Docs',
        description = 'Simple swig pht from docs',
        ext_modules = [pht_json_module], #与上面的扩展模块名称一致
        py_modules = ['JsonTool'], #需要打包的模块列表
    )

最后执行编译

1
python setup.py build

编译通过后在build/lib.*开头的子目录下即可见到编译好的Python库文件:_JsonTool.so和JsonTool.py。我们来看一下具体如何使用?

1
2
3
import JsonTool
JsonTool.EnSerialization( "data" )
JsonTool.DeSerialization( "data.json")