https://zhuanlan.zhihu.com/p/20152309?columnSlug=python-devhtml
摘要:模块ctypes是Python内建的用于调用动态连接库函数的功能模块,必定程度上能够用于Python与其余语言的混合编程。因为编写动态连接库,使用C/C++是最多见的方式,故ctypes最经常使用于Python与C/C++混合编程之中。python
=================================================================编程
1. ctypes 的原理以及优缺点小程序
从ctypes的文档中能够推断,在各个平台上均使用了对应平台动态加载动态连接库的方法,并经过一套类型映射的方式将Python与二进制动态连接库相链接。经过阅读ctypes自己的代码也能够印证这个推断(/Modules/_ctypes/_ctypes.c和/Modules/_ctypes/callproc.c)。在Windows平台下,最终调用的是Windows API中LoadLibrary函数和GetProcAddress函数,在Linux和Mac OS X平台下,最终调用的是Posix标准中的dlopen和dlsym函数。ctypes 实现了一系列的类型转换方法,Python的数据类型会包装或直接推算为C类型,做为函数的调用参数;函数的返回值也通过一系列的包装成为Python类型。也就是说,PyObject* <-> C types的转换是由ctypes内部完成的,这和SWIG是同一个原理。windows
从ctypes的实现原理不难看出:数组
ctypes 有如下优势:函数
ctypes 有如下缺点:测试
就我的的经验来看,ctypes 适合于“中轻量级”的Python C/C++混合编程。特别是遇到第三方库提供动态连接库和调用文档,且没有编译器或编译器并不互相兼容的场合下,使用ctypes特别方便。值得注意的是,对于某种需求,在Python自己就能够实现的状况下(例如获取系统时间、读写文件等),应该优先使用Python自身的功能而不要使用操做系统提供的API接口,不然你的程序会丧失跨平台的特性。spa
2. 一个简单的例子操作系统
做为Python文档的一部分,ctypes 提供了完善的文档。但没有Windows API编程经验的初学者读ctypes文档仍然会晕头转向。这里举一个小例子,尽力避开Windows API以及POSIX自己的复杂性,读者只须要了解C语言便可。
在尝试本节例子以前,依然要搭建Python扩展编程环境。见 搭建Python扩展开发环境 - 蛇之魅惑 - 知乎专栏
首先咱们写一个C语言的小程序,而后把它编译成动态连接库。
//great_module.c #include <nmmintrin.h> #ifdef _MSC_VER #define DLL_EXPORT __declspec( dllexport ) #else #define DLL_EXPORT #endif DLL_EXPORT int great_function(unsigned int n) { return _mm_popcnt_u32(n); }
这个源文件中只有一个函数 great_function,它会调用Intel SSE4.2指令集的POPCNT指令(封装在_mm_popcnt_u32中),即计算一个无符号整数的二进制表示中“1”的个数。若是你的电脑是2010年前购买的,那么极可能不支持SSE4.2指令集,你只须要把return这一行改成 return n+1;便可,一样可以说明问题。
调用_mm_popcnt_u32须要包含Intel 指令集头文件nmmintrin.h,它虽然不是标准库的一部分,可是全部主流编译器都支持。
中间还有一坨#ifdef...#else...#endif,这个是给MSVC准备的。由于在MSVC下,动态连接库导出的函数必须加 __declspec( dllexport ) 进行修饰。而gcc(Linux和Mac OS X的默认编译器)下,全部函数默认均导出。
接下来把它编译为动态连接库。Windows下动态连接库的扩展名是dll,Linux下是so,Mac OS X下是dylib。这里为了方便起见,一概将扩展名设定为dll。
Windows MSVC 下编译命令:(启动Visual Studio命令提示)
cl /LD great_module.c /o great_module.dll
Windows GCC、Linux、Mac OS X下编译命令相同:
gcc -fPIC -shared -msse4.2 great_module.c -o great_module.dll
写一个Python程序测试它,这个Python程序是跨平台的:
from ctypes import * great_module = cdll.LoadLibrary('./great_module.dll') print great_module.great_function(13)
整数13是二进制的1101,因此应该输出3
3. 类型映射:基本类型
对于数字和字符串等基本类型。ctypes 采用”中间类型“的方式在Python和C之间搭建桥梁。对于C类型Tc,均有ctypes类型Tm,将其转换为Python类型Tp。具体地说,例如某动态连接库中的函数要求参数具备C类型Tc,那么在Python ctypes 调用它的时候,就给予对应的ctypes类型Tm。Tm的值能够经过构造函数的方式传递对应的Python类型Tp。或者,使用它的可修改为员 Tm.value。
Tm(ctypes type)、Tc(C type)、Tp (Python type) 之对应关系见下表。
上面一段话比较绕。下面举个例子。
你们熟知的printf函数位于C标准库中。在C代码中调用printf是标准化的,可是,C标准库的实现不是标准化的。在Windows中,printf 函数位于%SystemRoot%\System32\msvcrt.dll,在Mac OS X中,它位于 /usr/lib/libc.dylib,在Linux中,通常位于 /usr/lib/libc.so.6。
下面一段代码能够在三大平台上运行:
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))
咱们只关注最后一行。printf的原型是
int printf (const char * format,...)
因此,第一个参数咱们用c_char_p建立一个C字符串,并以构造函数的方式用一个Python字符串初始化它。其后,咱们给予printf一个int型和一个double型的变量,相应的,咱们用c_int和c_double建立对应的C类型变量,并以构造函数的方式初始化它们。
若是不用构造函数,还能够用value成员。如下代码与 clib.printf(c_char_p("Hello %d %f"),c_int(15),c_double(2.3)) 等价:
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)
一些C库函数接受指针并修改指针所指向的值。这种状况下至关于数据从C函数流回Python。仍然使用value成员获取值。
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
s1 = c_char_p('a') s3 = clib.strcat(s1,'b') # 等价于 s3 = clib.strcat(s1,c_char_p('b')) print s1.value #ab
可是,当 ctypes 没法肯定类型对应的时候,会触发异常。
clib.printf(c_char_p("Hello %d %f"),15,2.3)
异常:
Traceback (most recent call last):
File "test_printf.py", line 12, in <module>
clib.printf(c_char_p("Hello %d %f"),15,2.3)
ctypes.ArgumentError: argument 3: <type 'exceptions.TypeError'>: Don't know how to convert parameter 3
4. 高级类型映射:数组
在C语言中,char 是一种类型,char [100]是另一种类型。ctypes 也是同样。使用数组须要预先生成须要的数组类型。
为了方便咱们用great_module,增长一个函数 array_get
//great_module.c #ifdef _MSC_VER #define DLL_EXPORT __declspec( dllexport ) #else #define DLL_EXPORT #endif DLL_EXPORT int array_get(int a[], int index) { return a[index]; }
下面咱们在Python里产生数组类型。ctypes 类型重载了操做符 *,所以产生数组类型很容易:
from ctypes import *
great_module = cdll.LoadLibrary('./great_module.dll')
type_int_array_10 = c_int * 10
my_array = type_int_array_10()
my_array[2] = c_int(5)
print great_module.array_get(my_array,2)
type_int_array_10 即为建立的数组类型,若是想获得数组变量,则须要例化这个类型,即my_array。my_array的每个成员的类型应该是 c_int,这里将它索引为2的成员赋予值 c_int(5)。固然因为隐式转换的存在,这里写 my_array[2] = 5也彻底没有问题。
至于函数返回值的类型,ctypes 规定,老是假设返回值为int。对于array_get而言,碰巧函数返回值也是int,因此具体的数值能被正确的取到。
若是动态连接库中的C函数返回值不是int,须要在调用函数以前显式的告诉ctypes返回值的类型。例如:
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 like 5444948 clib.strcat.restype = c_char_p s4 = clib.strcat('c','d') print s4 # cd
定义一个“高维数组”的方法相似。之因此加了引号,是由于C语言里并无真正的高维数组,ctype也同样——都是利用数组的数组实现的。
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
5. 高级类型映射:简单类型指针
ctypes 和C同样区分指针类型和指针变量。复习这两个概念:C语言里,int *是指针类型。用它声明的变量就叫指针变量。指针变量能够被赋予某个变量的地址。
在ctypes中,指针类型用 POINTER(ctypes_type) 建立。例如建立一个相似于C语言的int *:
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的指针类型。只有将指针类型例化以后才能获得指针变量。在例化为指针变量的同时将其指向变量v。这段代码在C语言里至关于
typedef c_int * type_p_int; int v = 4; type_p_int p = &v; printf("%d",p[0]); printf("%d",*p);
固然,因为Python是依靠绑定传递类型的语言,能够直接使用 ctypes 提供的pointer()获得一个变量的指针变量
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
"#-------" 以前和以后输出的内容是同样的。
6. 高级类型映射:函数指针
函数指针并无什么特别之处。若是一个动态连接库里的某个C函数须要函数指针,那么能够遵循如下的步骤将一个Python函数包装成函数指针:
咱们这里举两个例子。
第一个例子来源于ctypes官方文档。它调用的是C标准库中的qsort函数。
咱们先观察qsort的文档:
它的函数原型是
void qsort (void* base, size_t num, size_t size,
int (*compar)(const void*,const void*));
第三个参数即为函数指针做为回调函数,用于给出元素之间大小的判断方法。咱们这里使用整数做为判断类型。那么qsort的函数原型能够理解为:
void qsort (int* base, size_t num, size_t size,
int (*compar)(const int*,const int*));
其中,回调函数的原型为:
int compar(const int*,const int*)
使用CFUNCTYPE建立ctypes的函数指针类型:
CMPFUNC = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
CFUNCTYPE的第一个参数是函数的返回值,函数的其余参数紧随其后。
接下来用Python写回调函数的实现:
def py_cmp_func(a, b): print type(a) print "py_cmp_func", a[0], b[0] return a[0] - b[0]
最后,用刚才获得的函数指针类型CMPFUNC,以Python回调函数的函数名做为构造函数的参数,就获得了能够用于C函数的函数指针变量:
p_c_cmp_func = CMPFUNC(py_cmp_func)
完整的代码以下:
from ctypes import * from platform import * cdll_names = { 'Darwin' : 'libc.dylib', 'Linux' : 'libc.so.6', 'Windows': 'msvcrt.dll' } clib = cdll.LoadLibrary(cdll_names[system()]) CMPFUNC = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int)) def py_cmp_func(a, b): print type(a) print "py_cmp_func", a[0], b[0] return a[0] - b[0] type_array_5 = c_int * 5 ia = type_array_5(5, 1, 7, 33, 99) clib.qsort(ia, len(ia), sizeof(c_int), CMPFUNC(py_cmp_func))
注意到,Python函数获得的参数a和b的类型都是 POINTER(c_int) (显示为<class '__main__.LP_c_int'>),对指针变量解引用的方法是以前提到的[0]或者.contents。咱们这里应用了ctypes的隐式类型转换,因此a[0]和b[0]能够当成Python的int类型使用。
有趣的是,这段代码在*nix(Linux、Mac OS X)下调用和在Windows下调用,比较的次数是不同的。Windows彷佛更费事。
第二个例子比较实用,但只能在Windows下运行。
咱们要利用的是Windows API EnumWindows枚举系统全部窗口的句柄,再根据窗口的句柄列出各个窗口的标题。
EnumWindows的文档见EnumWindows function (Windows)
调用Windows API有特殊之处。因为Windows API函数不使用标准C的调用约定(微软一向的尿性)。故在LoadLibrary时不可以使用cdll.LoadLibrary而使用windll.LoadLibrary。在声明函数指针类型的时候,也不能用CFUNCTYPE而是用WINFUNCTYPE。关于调用约定的问题参见x86 calling conventions
Windows API有不少内建类型,ctypes也对应地提供了支持。代码以下:
from ctypes import * from ctypes import wintypes WNDENUMPROC = WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) user32 = windll.LoadLibrary('user32.dll') def EnumWindowsProc(hwnd, lParam): length = user32.GetWindowTextLengthW(hwnd) + 1 buffer = create_unicode_buffer(length) user32.GetWindowTextW(hwnd, buffer, length) print buffer.value return True user32.EnumWindows(WNDENUMPROC(EnumWindowsProc), 0)
7. 其余
ctypes 还对C语言中的结构体、联合体等提供支持。这部分代码比较繁琐,可参见ctypes的文档 https://docs.python.org/2/library/ctypes.html