>>> from ctypes import *
>>> dll = CDLL(“test.dll”) # 调用 test.dll
>>> dll.add(10, 30) # 调用 add 函数
可以看见返回了 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)
>>> dll.addf(8.3, 3.1)
11.399999618530273
这就是我们想要的结果。
五、更多地关于 ctypes 类型的创建和使用
我们也可以创建一个 ctypes 的类型(c_int、c_float、c_char……)并给他赋值,例子如下:
>>> i = c_int(45) # 定义一个 int 型变量,值为 45
>>> i.value # 打印变量的值
>>> i.value = 56 # 改变该变量的值为 56
>>> i.value # 打印变量的新值
没错,你要通过 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) # 打印变量
而对于共用体只要类继承自 ctypes.Union 就成,其他与上面相同。
这就是最后一节了,虽然是指针,不过别紧张,且听我娓娓道来。
如何创建一个 ctypes 的指针呢?这里有三个跟指针有个的 ctypes 里的函数,掌握了他们你自然就会了(可能 pointer POINTER 会有点绕,仔细看看就好)。
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) # 创建指针
<__main__.LP_c_long object at 0x00E12AD0>
<__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>>>