Python 和 C 的混合编程工具有很多,这里介绍 Python 标准库自带的 ctypes 模块的使用方法。
- 初识
Python 的 ctypes 要使用 C 函数,需要先将 C 编译成动态链接库的形式,即 Windows 下的 .dll 文件,或者 Linux 下的 .so 文件。先来看一下 ctypes 怎么使用 C 标准库。
Windows 系统下的 C 标准库动态链接文件为 msvcrt.dll (一般在目录 C:WindowsSystem32 和 C:WindowsSysWOW64 下分别对应 32-bit 和 64-bit,使用时不用刻意区分,Python 会选择合适的)
Linux 系统下的 C 标准库动态链接文件为 libc.so.6 (以 64-bit Ubuntu 系统为例, 在目录 /lib/x86_64-linux-gnu 下)
例如,以下代码片段导入 C 标准库,并使用 printf 函数打印一条消息,
import platform from ctypes import * if platform.system() == 'Windows': libc = cdll.LoadLibrary('msvcrt.dll') elif platform.system() =='Linux': libc = cdll.LoadLibrary('libc.so.6') libc.printf('Hello ctypes! ')
另外导入dll文件,还有其它方式如下,详细解释请参阅 ctypes module 相关文档,
import platform from ctypes import * if platform.system() == 'Windows': libc = cdll.LoadLibrary('msvcrt.dll') #libc = windll.LoadLibrary('msvcrt.dll') # Windows only #libc = oledll.LoadLibrary('msvcrt.dll') # Windows only #libc = pydll.LoadLibrary('msvcrt.dll') #libc = CDLL('msvcrt.dll') #libc = WinDLL('msvcrt.dll') # Windows only #libc = OleDLL('msvcrt.dll') # Windows only #libc = PyDLL('msvcrt.dll') elif platform.system() =='Linux': libc = cdll.LoadLibrary('libc.so.6') #libc = pydll.LoadLibrary('libc.so.6') #libc = CDLL('libc.so.6') #libc = PyDLL('libc.so.6') libc.printf('Hello ctypes! ')
- ctypes 数据类型
ctypes 作为 Python 和 C 联系的桥梁,它定义了专有的数据类型来衔接这两种编程语言。如下表,
注:Python 中的类型,除了 None,int, long, Byte String,Unicode String 作为 C 函数的参数默认提供转换外,其它类型都必须显式提供转换。
None:对应 C 中的 NULL
int, long: 对应 C 中的 int,具体实现时会根据机器字长自动适配。
Byte String:对应 C 中的一个字符串指针 char * ,指向一块内存区域。
Unicode String :对应 C 中一个宽字符串指针 wchar_t *,指向一块内存区域。
例如,
import platform from ctypes import * if platform.system() == 'Windows': libc = cdll.LoadLibrary('msvcrt.dll') elif platform.system() == 'Linux': libc = cdll.LoadLibrary('libc.so.6') libc.printf('%s ', 'here!') # here! libc.printf('%S ', u'there!') # there! libc.printf('%d ', 42) # 42 libc.printf('%ld ', 60000000) # 60000000 #libc.printf('%f ', 3.14) #>>> ctypes.ArgumentError #libc.printf('%f ', c_float(3.14)) #>>> dont know why 0.000000 libc.printf('%f ', c_double(3.14)) # 3.140000
- 创建可变的 string buffer
Python 默认的 string 是不可变的,所以不能传递 string 到一个 C 函数去改变它的内容,所以需要使用 create_string_buffer,对应 Unicode 字符串,要使用 create_unicode_buffer,
定义和用法如下,
>>> help(create_string_buffer) Help on function create_string_buffer in module ctypes: create_string_buffer(init, size=None) create_string_buffer(aString) -> character array create_string_buffer(anInteger) -> character array create_string_buffer(aString, anInteger) -> character array
from ctypes import * p = create_string_buffer(5) print sizeof(p) # 5 print repr(p.raw) # 'x00x00x00x00x00' p.raw = 'Hi' print repr(p.raw) # 'Hix00x00x00' print repr(p.value) # 'Hi'
- 传递自定义参数类型到 C 函数
ctypes 允许你创建自定义参数类型,它会自动去搜索自定义数据的 _as_parameter 属性,将其作为 C 函数的参数,例如,
import platform from ctypes import * if platform.system() == 'Windows': libc = cdll.LoadLibrary('msvcrt.dll') elif platform.system() == 'Linux': libc = cdll.LoadLibrary('libc.so.6') class Bottles(object): def __init__(self, number): self._as_parameter_ = number # here only accept integer, string, unicode string bottles = Bottles(42) libc.printf('%d bottles of beer ', bottles)
输出,
42 bottles of beer
也可以为你的数据定义 _as_parameter 属性,如下,
import platform from ctypes import * if platform.system() == 'Windows': libc = cdll.LoadLibrary('msvcrt.dll') elif platform.system() == 'Linux': libc = cdll.LoadLibrary('libc.so.6') class Bottles(object): def __init__(self): self._as_parameter_ = None # only accept integer, string, unicode string @property def aspram(self): return self._as_parameter_ @aspram.setter def aspram(self, number): self._as_parameter_ = number bottles = Bottles() bottles.aspram = 63 libc.printf('%d bottles of beer ', bottles)
输出,
63 bottles of beer
- 指定 C 函数的参数类型
可以指定要调用 C 函数的参数类型,如果传入参数不符合指定的类型,则 ctypes 会尝试转换,如果转换不成功,则抛 ArgumentError,例如,
import platform from ctypes import * if platform.system() == 'Windows': libc = cdll.LoadLibrary('msvcrt.dll') elif platform.system() == 'Linux': libc = cdll.LoadLibrary('libc.so.6') libc.printf.argtypes = [c_char_p, c_char_p, c_int, c_double] libc.printf('String is "%s", Int is %d, Double is %f ', 'Hi', 10, 2.2) libc.printf('%s, %d, %f ', 'X', 2, 3) try: libc.printf("%d %d %d", 1, 2, 3) except ArgumentError, e: print "*** ERROR: %s" % str(e)
输出,
String is "Hi", Int is 10, Double is 2.200000 X, 2, 3.000000 *** ERROR: argument 2: <type 'exceptions.TypeError'>: wrong type
- 指定 C 函数的返回值类型
如果不指定 C 函数的返回值, ctypes 默认返回 int 类型,如果要返回特定类型,需要指定返回类型 restype,
例如,
import platform from ctypes import * if platform.system() == 'Windows': libc = cdll.LoadLibrary('msvcrt.dll') elif platform.system() == 'Linux': libc = cdll.LoadLibrary('libc.so.6') print '1->', libc.strchr('abcdefghij', c_char('d')) libc.strchr.restype = c_char_p print '2->', libc.strchr('abcdefghij', c_char('d')) print '3->', libc.strchr('abcdefghij', 'd') # Note, here C function strchr not know what 'd' mean, so rerurn None libc.strchr.argtypes = [c_char_p, c_char] print '4->', libc.strchr('abcdefghij', 'd') # Note, here not use c_char('w')
输出:
1-> 40291315 2-> defghij 3-> None 4-> defghij
- 按引用传递参数
有些情况下,需要 C 函数修改传入的参数,或者参数过大不适合传值,需要按引用传递,ctypes 提供关键字 byref() 处理这种情况,
例如,
import platform from ctypes import * if platform.system() == 'Windows': libc = cdll.LoadLibrary('msvcrt.dll') elif platform.system() == 'Linux': libc = cdll.LoadLibrary('libc.so.6') i = c_int() f = c_float() s = create_string_buffer('