最近做项目一直使用Python来调用C中的动态链接库,有的地方不是很清楚,系统整理一下。
模块ctypes是Python内建的用于调用动态链接库函数的功能模块,一定程度上可以用于Python与其他语言的混合编程。由于编写动态链接库,使用C/C++是最常见的方式,故ctypes最常用于Python与C/C++混合编程之中。
ctypes 实现了一系列的类型转换方法,Python的数据类型会包装或直接推算为C类型,作为函数的调用参数;函数的返回值也经过一系列的包装成为Python类型。也就是说,PyObject* <-> C types的转换是由ctypes内部完成的。
ctypes 有以下优点:
- Python内建,不需要单独安装
- 可以直接调用二进制的动态链接库
- 在Python一侧,不需要了解Python内部的工作方式
- 在C/C++一侧,也不需要了解Python内部的工作方式
- 对基本类型的相互映射有良好的支持
ctypes 有以下缺点:
- 平台兼容性差
- 不能够直接调用动态链接库中未经导出的函数或变量(这个现在不明白什么意思,但是不影响后面的使用)
- 对C++的支持差
简单的例子
先写一个简单的C语言程序func.c,然后编译成动态链接库:
1 2 3 4 5 6 |
#include <stdio.h> int hello(int a) { printf("Hello, input is: %d\n", a); return a + 1; } |
接下来把它编译为动态链接库。Windows下动态链接库的扩展名是dll,Linux下是so,Mac OS X下是dylib。其实扩展名是给人看的,对于机器来说无所谓,在linux下扩展名命名为dll也没有问题。GCC编译命令如下:
1 2 |
gcc -fPIC -shared func.c -o func.so |
-shared
与 –fPIC
参数用于指示编译器进行动态链接库的编译
然后写一个python程序func.py来测试一下:
1 2 3 4 |
from ctypes import * lib = cdll.LoadLibrary('./func.so') print (lib.hello(10)) |
输出:
1 2 3 |
Hello, input is: 10 11 |
所以一个简单的python调用C语言完成了,但是真正的代码怎么会这么简单。感觉Python和C之间的调用主要的是解决两个语言之间的数据类型映射关系,也就是用Python调用C时主要是解决怎么把python中的数据转换成C函数参数中对应的数据类型以及把C函数返回值转换成python中的数据类型,而这个工作就是ctypes模块要做的。
类型映射:基本类型
对于数字和字符串等基本类型。ctype作为Python和C之间的桥梁,在python调用C时,将python中的值类型通过ctypes中间类型转换成C中的类型;在调用后返回值时,将C中值类型通过ctypes中间类型转换成python中的类型。ctypes中间类型的值可以通过构造函数中传递对应python类型的值来初始化ctypes中间变量。还可以通过修改中间类型的成员变量value的值,(中间变量).value。
ctypes中间类型,C类型和python类型对应关系如下表:
ctypes 类型 | C 类型 | Python 数据类型 |
---|---|---|
c_bool |
_Bool |
bool (1) |
c_char |
char |
单字符字节对象 |
c_wchar |
wchar_t |
单字符字符串 |
c_byte |
char |
int |
c_ubyte |
unsigned char |
int |
c_short |
short |
int |
c_ushort |
unsigned short |
int |
c_int |
int |
int |
c_uint |
unsigned int |
int |
c_long |
long |
int |
c_ulong |
unsigned long |
int |
c_longlong |
__int64 或 long long |
int |
c_ulonglong |
unsigned __int64 或 unsigned long long |
int |
c_size_t |
size_t |
int |
c_ssize_t |
ssize_t 或 Py_ssize_t |
int |
c_float |
float |
float |
c_double |
double |
float |
c_longdouble |
long double |
float |
c_char_p |
char * (NUL terminated) |
字节串对象或 None |
c_wchar_p |
wchar_t * (NUL terminated) |
字符串或 None |
c_void_p |
void * |
int 或 None |
举个例子,用python来调用C语言中的printf函数,当然只是在这里举这个例子而已,其实正常情况下没有必要这么调用,因为python中就有print函数。
在Windows中,printf 函数位于%SystemRoot%\System32\msvcrt.dll,在Mac OS X中,它位于 /usr/lib/libc.dylib,在Linux中,一般位于 /usr/lib/libc.so.6。所以写一个python代码:
1 2 3 4 5 6 7 8 9 10 11 12 |
from ctypes import * from platform import * cdll_names = { 'Darwin' : 'libc.dylib', 'Linux' : 'libc.so.6', 'Windows': 'msvcrt.dll' } clib = cdll.LoadLibrary(cdll_names[system()]) clib.printf(c_char_p("Hello %d %f"),c_int(15),c_double(2.3)) |
从最后一行中可以看到C中printf函数原型为:
1 2 |
int printf (const char * format,...) |
第一个参数用c_char_p创建一个C字符串,并以构造函数的方式用一个Python字符串初始化它;后两个参数是一个int型和一个double型的变量,相应地用c_int和c_double创建对应的C类型变量,并以构造函数的方式初始化它们。
如果不用构造函数,还可以用value成员:
1 2 3 4 5 6 7 8 9 |
str_format = c_char_p() int_val = c_int() double_val = c_double() str_format.value = "Hello %d %f" int_val.value = 15 double_val.value = 2.3 clib.printf(str_format,int_val,double_val) |
上面这一块代码与下面这一句代码等价:
1 2 |
clib.printf(c_char_p("Hello %d %f"),c_int(15),c_double(2.3)) |
这里面可以看成是先用构造函数创建几个PyObject,比如上面的str_format,int_val和double_val,然后在设置对象中成员变量value的值。感觉作用机理是,python将这些PyObject传递给ctypes,然后ctypes将相应PyObject中的成员变量value值取出传递给C中的函数。
另外,C语言中经常有一些函数接受指针变量并修改指针变量所指向的值。这种情况下相当于数据从C函数中流回到python。可以使用value成员来获取该值。
同样以C中的strcat函数为例,函数原型如下:
1 2 |
char *strcat(char *dest, const char *src); |
函数功能是将后面一个字符串src接到前面一个字符串dest后面,返回新的字符串(要保证dest字符数组够长)。当然这里面也是用来举例子,正常情况下没有必要这么调用,python中加号’+’重载直接可以字符串连接。
在Windows中,strcat函数位于%SystemRoot%\System32\msvcrt.dll,在Mac OS X中,它位于 /usr/lib/libc.dylib,在Linux中,一般位于 /usr/lib/libc.so.6。所以写一个python代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from ctypes import * from platform import * cdll_names = { 'Darwin' : 'libc.dylib', 'Linux' : 'libc.so.6', 'Windows': 'msvcrt.dll' } clib = cdll.LoadLibrary(cdll_names[system()]) s1 = c_char_p('a') s2 = c_char_p('b') s3 = clib.strcat(s1,s2) print s1.value #输出ab |
另外,当 ctypes 可以判断类型对应关系的时候,可以直接将Python类型赋予C函数,而不需要使用ctypes构造函数来传递。ctypes 会进行隐式类型转换。例如:
1 2 3 4 |
s1 = c_char_p('a') s3 = clib.strcat(s1,'b') # 等价于 s3 = clib.strcat(s1,c_char_p('b')) print s1.value #输出ab |
但是,当 ctypes 无法确定类型对应的时候,会触发异常。
1 2 |
clib.printf(c_char_p("Hello %d %f"),15,2.3) |
这里面会报错,第三个浮点数类型不能进行隐式类型转换,会报错。所以,对于这种情况,不管能不能进行隐式类型转换,都先用ctypes构造函数显示转换一下。
高级类型映射:数组
在C语言中,char代表简单的字符类型,而char [ ]代表字符数组类型。ctypes中也是一样,使用数组是需要预先生成所需要使用的数组类型。
写一个C语言代码func2.c如下:
1 2 3 4 |
int array_get(int a[], int index) { return a[index]; } |
然后执行下面命令编译成动态链接库:
1 2 |
gcc -fPIC -shared func2.c -o func2.so |
然后在写一个python代码func2.py来调用上面的动态链接库:
1 2 3 4 5 6 7 8 9 10 11 |
from ctypes import * lib = cdll.LoadLibrary('./func2.so') type_int_array_10 = c_int * 10 my_array = type_int_array_10() my_array[2] = c_int(5) print lib.array_get(my_array, 2) |
ctypes类型重载了操作符”*”,这里的”*”不是乘号。type_int_array_10为创建的数组类型,这里是数组类型而不是数组变量,也就是相当C语言中int [10],想要得到数组变量就需要实例化,即my_array = type_int_array_10(),就是相当于C语言中my_array = int [10]。my_array的每一个成员的类型应该是 c_int,这里将它索引为2的成员赋予值 c_int(5),由于隐式转换的存在,这里写 my_array[2] = 5也可以,但是个人觉得还是显式指定类型就行。
返回值类型映射
ctypes 规定,总是假设返回值的类型为int。对于上面的array_get函数而言,碰巧函数返回值也是int,所以返回的数值能被正确取到,但是如果C语言中函数返回值不是int,则需要在函数调用之前显式告诉ctypes返回值类型。
上面的strcat.py中作如下修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
from ctypes import * from platform import * cdll_names = { 'Darwin' : 'libc.dylib', 'Linux' : 'libc.so.6', 'Windows': 'msvcrt.dll' } clib = cdll.LoadLibrary(cdll_names[system()]) s3 = clib.strcat('a','b') print s3 # an int value clib.strcat.restype = c_char_p s4 = clib.strcat('c','d') print s4 # cd |
上面的代码在没有指定strcat的返回值类型的时候,输出一长串数字,猜测可能是把字符串的首地址当成十进制数打印出来了。另外,我还发现一个问题,在python中的动态链接库中可以直接调用clib.strcat(‘a’, ‘b’)(隐式类型转换),但是在C语言中不能直接strcat(“a”, “b”)这样调用,必须给前面的那个字符数组先分配足够大的空间,不然会报错,而在python中根本不需要考虑这一点,可能是ctypes自动给前面的字符串分配空间,不得不说这对python程序源很友好。
定义一个“高维数组”的方法类似,之所以加了引号,是因为C语言里并没有真正的高维数组,同样ctype也是利用数组的数组实现的:
1 2 3 4 5 6 |
from ctypes import * type_int_array_10 = c_int * 10 type_int_array_10_10 = type_int_array_10 * 10 my_array = type_int_array_10_10() my_array[1][2] = 3 |
高级类型映射:简单指针类型
ctypes 和C一样区分指针类型和指针变量。语言里,int *是指针类型,用它声明的变量就叫指针变量,指针变量可以被赋予某个变量的地址。
在ctypes中,指针类型用 POINTER(ctypes_type) 创建,例如创建一个类似于C语言的int *:
1 2 3 4 5 6 |
type_p_int = POINTER(c_int) v = c_int(4) p_int = type_p_int(v) print p_int[0] print p_int.contents |
上面的type_p_int是一个类型,这个类型是指向int的指针类型,只有将指针类型例化之后才能得到指针变量。指针类型的成员变量contents代表指针所指向的内容。这段代码在C语言里相当于:
1 2 3 4 5 6 |
typedef int * type_p_int; int v = 4; type_p_int p = &v; printf("%d",p[0]); printf("%d",*p); |
另外,还可以直接使用 ctypes 提供的pointer()得到一个变量的指针变量 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from ctypes import * type_p_int = POINTER(c_int) v = c_int(4) p_int = type_p_int(v) print type(p_int) print p_int[0] print p_int.contents #------- p_int = pointer(v) print type(p_int) print p_int[0] print p_int.contents |
上面的代码#——-前后输出一样,pointer()就是取变量的地址,相当于C语言中&操作符。
注意:POINTER()是用来定义指针类型,是指针类型而不是指针变量,同时与注意定义指针类型和定义数组的区别,上面的p_int[0]方式注意和数组的调用方式区分开;pointer()是用来取地址的,相当于C语言中的&操作符,与指针中的[指针变量].contents互为逆运算。
高级类型映射:结构体
C语言中经常会用到结构体,ctypes提供了很多类型的PyObject到C的映射,同样也提供了结构体类型的映射。python中将结构体定义为一个class(继承ctypes中Structure的class);结构体成员变量的定义在python中是在_fields_中定义,在python中调用结构体的成员变量值的方式:[结构体变量名].[成员变量名]。
现在分别从结构体作为函数参数和函数返回值来进行说明ctypes中的结构体,先写一个C语言程序func3.c如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# include <malloc.h> typedef struct structTest* structTestPointer; struct structTest { int a; int b; }; // 结构体指针作为函数参数 void set(structTestPointer sp) { sp->a = 10; sp->b = 20; } // 结构体作为函数参数,结构体指针作为函数返回值 structTestPointer get(struct structTest s) { structTestPointer sp = malloc(sizeof(structTestPointer)); sp->a = s.a; sp->b = s.b; return sp; } |
先写一个python代码func3.py,验证结构体指针作为函数参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from ctypes import * class StructTest(Structure): _fields_ = [('a', c_int), ('b', c_int)] lib = cdll.LoadLibrary('./func3.so') stp = pointer(StructTest(1, 2)) print stp print stp.contents.a print stp.contents.b lib.set(stp) print stp print stp.contents.a print stp.contents.b |
输出:
1 2 3 4 5 6 7 |
<__main__.LP_StructTest object at 0x7f75b753c710> 1 2 <__main__.LP_StructTest object at 0x7f75b753c710> 10 20 |
再写一个python代码func4.py,验证结构体作为函数参数,结构体指针作为函数返回值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from ctypes import * class structTest(Structure): pass structTest._fields_ = [('a', c_int), ('b', c_int)] lib = cdll.LoadLibrary('./func3.so') lib.get.argtype = structTest lib.get.restype = POINTER(structTest) st = structTest(5, 10) print st.a print st.b p = lib.get(st) print p.contents.a print p.contents.b |
输出:
1 2 3 4 5 |
5 10 5 10 |
Reference:
python中还可以使用cffi来集成C语言程序,例如:
https://www.cnblogs.com/ccxikka/p/9637545.html
https://zhuanlan.zhihu.com/p/52485004