近几天使用 python 与 c/c++ 程序交互,网上有推荐swig但效果都不理想,所以琢磨琢磨了 python 的 ctypes 模块。同时,虽然网上有这方面的内容,但是感觉还是没说清楚。这里记录下来做备用,同时也给广大 python with c/c++ 派留给方便。如果你觉得我写的不好,可以参考官方文档里对 ctypes 的介绍,那里说不一定有你想要的。
如有错误,请指正:)。
测试环境: win 8.1, Visual Studio 2010, Python 3.5
一、介绍
python 与 c/c++ 交互的主要目的一是为了速度,二大概就是用做脚本了。
说是 python 与 c/c++ 交互,但实际上是 python 与 c 交互, 因为 python 本身只支持 C API。但是我们可以通过调整达到 python 与 c++ 工程协作的目的。下面主要说明 python 使用 ctypes 模块与 c 交互的要点和疑难点。
二、使用 ctypes 可以做到什么?
python 可以通过使用 ctypes 模块调用 c 函数,这其中必定包括可以定义 c 的变量类型(包括结构体类型、指针类型)。
官方给的定义是 “ctypes
is a foreign function library for Python. It provides C compatible data types, and allows calling functions in DLLs or shared libraries. It can be used to wrap these libraries in pure Python.” —— 引自 Python 3.5 chm 文档。其大意就是——ctypes 是一个为 Python 准备的外部函数库。它提供兼容C的数据类型,并允许调用DLL或共享库中的函数。通过它,可以使用纯粹的 Python 包装这些函数库(这样你就可以直接 import xxx 来使用这些函数库了)。
口说无凭,我们需要一个具体的例子,下面我们引入一个 cpp 文件来说明以下所有问题:
现有 test.cpp 文件如下:
#if 1 #define DLL_API __declspec(dllexport) #else #define DLL_API __declspec(dllimport) #endif #include <stdio.h> #include <stdlib.h> // Point 结构体 struct Point { float x, y; }; static Point* position = NULL; extern "C" { DLL_API int add(int a, int b) { return a + b; } DLL_API float addf(float a, float b) { return a + b; } DLL_API void print_point(Point* p) { if (p) printf("position x %f y %f", p->x, p->y); } }
可以看见这里有三个函数,包括一个形参带指针的函数。学会用 Python 成功调用上面的三个函数就是我的本文的目标了。对于windows平台把他生成为 dll 文件就行(其他平台为 .so)。下面我们在解释器中写出出测试用的 Python 代码。
如果你不理解上面的 cpp 文件,那还是先看看其他关于 dll 的文章吧:
1. Dll的分析与编写(一) http://www.cnblogs.com/hicjiajia/archive/2010/08/27/1809997.html
2. extern "C"的用法解析 http://www.cnblogs.com/rollenholt/archive/2012/03/20/2409046.html
三、ctypes 怎么样调用 c 的函数库?
首先,需要 ctypes 加载需要被调用的函数库(废话)。
使用 ctypes.CDLL ,其定义如下(引自 Python 3.5 chm 文档 )
ctypes.
CDLL
(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
另外,在 windows 平台上会忽略 modes 参数。对于 windows 平台来说还可以调用 ctypes.WinDLL,与上面的 CDLL 几乎一样,唯一不同点是它假定库中函数遵循 Windows stdcall 调用约定,其他参数的意义见官方文档。
如果要调用 test.dll 中的 add 函数可以写作 :
>>> from ctypes import * >>> dll = CDLL(“test.dll”) # 调用 test.dll >>> dll.add(10, 30) # 调用 add 函数 40
可以看见返回了 40,是不是很简单?。这是就是我们预期的结果。下面我们再调用 addf 这是 add 的 float 版本,有些人可能会问为什么不直接写 DLL_API float add(float a, float b) ? 用函数的重载就好了,为什么不这么做?注意,我们使用了 extern“C”声明函数,所以不支持函数的重载。
接下来我们调用 addf , 猜猜会发生什么?
>>> dll.addf(10, 30)
9108284
哦,这是不是有点出乎你的意料?为什么会这样?
四、c 类型与 Python 类型, 参数类型、返回类型
之所以会调用 addf 函数“失败”倒不是 Python 出了问题。原因是你没有“告诉” Python 这个函数的“容貌”(更正式的说法是“描述”)——函数的形参类型和返回类型。那么为什么我们调用 add 成功了呢?因为 Python 默认函数的参数类型和返回类型为 int 型。理所当然地 Python 以为 addf 返回了一个 int 类型的值。
也就是说,在 ctypes 读取 dll 时只知道存在这个函数,但是并不知到函数的形参类型和返回值的类型。你可能会疑惑为什么 Python 这么麻烦,还要告诉它共享库中函数的“容貌”。这就不能怪它了,事实上,就是 Microsoft 自己开发的 C# 语言在调用 dll 的时候都需要告诉 C# 这个函数是什么样子的。这解释起来有点烦,还是来专注于我们对 ctypes 用法的研究吧。
那么,对于 Python 来说 c 的类型都有哪些呢?下面就是一张 Python 中的类型对应 c 类型的表(截图自 Python 3.5 chm 文档)
然后,怎么告诉 Python 一个外来函数的形参类型和返回的值的类型呢?
这就要需要给函数的两个属性 restype 和 argtypes 赋值了。它们分别对应返回类型和参数类型。对于 addf 它的返回值类型是 float, 对应到 Python 里就是 c_float。下面我们进行赋值:
>>> dll.addf.restype = c_float # addf 返回值的类型是 flaot
如果函数的返回值是 void 那么你可以赋值为 None。另外,在不是太低的版本中,可以使用 Python 内置类型(上表中最右边的一列)“描述”库函数的返回类型,但是,不可以用 Python 内置类型来描述库函数的参数。
由于函数的参数不是固定的数量,所以需要使用列表或者是元组来说明:
>>> dll.addf.argtypes = (c_float, c_float) # addf 有两个形参,都是 float 类型 或者是下面这样,但是,你知道的,查找元组的效率略高:) >>> dll.addf.argtypes = [c_float, c_float] # addf 有两个形参,都是 float 类型
该做的都做完了,现在再来调用 addf:
>>> dll.addf(8, 3) 11.0 >>> dll.addf(8.3, 3.1) 11.399999618530273
这就是我们想要的结果。
五、更多地关于 ctypes 类型的创建和使用
我们也可以创建一个 ctypes 的类型(c_int、c_float、c_char……)并给他赋值,例子如下:
>>> i = c_int(45) # 定义一个 int 型变量,值为 45 >>> i.value # 打印变量的值 45 >>> i.value = 56 # 改变该变量的值为 56 >>> i.value # 打印变量的新值 56
没错,你要通过 ctypes 的 value 属性给一个 ctypes 类型赋值——赋一个 Python 内置类型的值。
其他的 ctypes 的函数,如 sizeof(i)(是不是感觉很贴心就像 c 一样),就不一一介绍了。自行参见文献第三条和官方文档吧。
六、结构体、共用体
这是调用 print_point 库函数的必要成分之一。
如果要在 Python 中定义一个 c 类型的结构体,需要定义一个类,例如 Structu Point 就这么做:
>>> class Point(Structure): ... _fields_ = [("x", c_float), ("y", c_float)] ... >>>
这就定义好了。其中有两个要点:
1. 类必须继承自 ctypes.Structure
2. 描述这个结构体的“容貌”
第一点很简单, class XXX(Structure) 就 OK。
要做到第二点,则必须在自定义的 c 结构体类中定义一个名为 _fields_ 的属性,并赋值给如上的一个列表。
然后就可以这样使用了:
>>> p = Point(2,5) # 定义一个 Point 类型的变量,初始值为 x=2, y=5 也可以直接写 p = Point() >>> p.y = 3 # 修改值 >>> print (p.x, p.y) # 打印变量 2 3
而对于共用体只要类继承自 ctypes.Union 就成,其他与上面相同。
七、指针
这就是最后一节了,虽然是指针,不过别紧张,且听我娓娓道来。
如何创建一个 ctypes 的指针呢?这里有三个跟指针有个的 ctypes 里的函数,掌握了他们你自然就会了(可能 pointer POINTER 会有点绕,仔细看看就好)。
函数 | 说明 |
byref(x [, offset]) | 返回 x 的地址,x 必须为 ctypes 类型的一个实例。相当于 c 的 &x 。 offset 表示偏移量。 |
pointer(x) | 创建并返回一个指向 x 的指针实例, x 是一个实例对象。 |
POINTER(type) | 返回一个类型,这个类型是指向 type 类型的指针类型, type 是 ctypes 的一个类型。 |
byref 很好理解,传递参数的时候就用这个,用 pointer 创建一个指针变量也行,不过 byref 更快。
而 pointer 和 POINTER 的区别是,pointer 返回一个实例,POINTER 返回一个类型。甚至你可以用 POINTER 来做 pointer 的工作:
>>> a = c_int(66) # 创建一个 c_int 实例 >>> b = pointer(a) # 创建指针 >>> c = POINTER(c_int)(a) # 创建指针 >>> b <__main__.LP_c_long object at 0x00E12AD0> >>> c <__main__.LP_c_long object at 0x00E12B20> >>> b.contents # 输出 a 的值 c_long(66) >>> c.contents # 输出 a 的值 c_long(66)
pointer 创建的指针貌似没方法修改指向的 ctypes 类型值。
该说的都说了,接下来就要调用 print_point 函数了:
>>> dll.print_point.argtypes = (POINTER(Point),) # 指明函数的参数类型 >>> dll.print_point.restype = None # 指明函数的返回类型 >>> >>> p = Point(32.4, -92.1) # 实例化一个 Point >>> dll.print_point(byref(p)) # 调用函数 position x 32.400002 y -92.099998>>>
当然你非要用慢一点的 pointer 也行:
>>> dll.print_point(pointer(p)) # 调用函数 position x 32.400002 y -92.099998>>>
至于为什么输出的后面出现了畸形 “y -92.099998>>>” ,去翻一翻上面的 c 代码你就知道了。
参考文献
更多关于 ctypes 类型的用法可以参加下面的书籍、文档和网页:
1. 《Python参考手册》
2. Python 3.5 官方文档 “python350.chm”
3. http://www.ibm.com/developerworks/cn/linux/l-cn-pythonandc/