ctypes-Python外部函数库

时间:2022-05-11 03:46:08

来之:http://gashero.iteye.com/blog/519837

 

译者注:翻译的并不完全,不过对于大多数应用是足够了。总体感觉使用ctypes还是比较麻烦,需要自己重新把头文件给用Python写一遍。再者就是对于指针的指针一类还不知怎么实现。结论是我不打算再用ctypes,还是原生的扩展模块靠谱。发出来给能用的上的人用吧。

 

译者: gashero
日期: 2009-11-02

从Python2.5开始引入。

ctypes是Python的外部函数库。它提供了C兼容的数据类型,并且允许调用动态链接库/共享库中的函数。它可以将这些库包装起来给Python使用。

1   ctypes入门

本入门中的代码使用doctest确保可用。不过一些代码在linux/windows/mac os x中的行为可能略有差异,这在其doctest的注释中有所表示。

少数代码示例引用了ctypes的c_int类型。这个类型是32bit系统中c_long类型的别名。所以你在期待c_int而显示c_long时不必疑惑,他们是一样的。

1.1   载入动态链接库

ctypes导出了 cdll ,在windows上还有 windll 和 oledll 对象用于载入动态链接库。

载入动态链接库可以直接存取其属性。 cdll 载入导出函数符合cdecl调用规范的库,而 windll 载入导出函数符合 stdcall 调用规范的库, oledll 也使用 stdcall 调用规范,并假设函数返回Windows的HRESULT错误码。错误码用于在出错时自动抛出WindowsError这个Python异常。

如下是Windows的例子,主意msvcrt是MS标准C库,包含了大部分标准C函数,并且使用cdecl调用规范:

>>> from ctypes import *
>>> print windll.kernel32
<WinDLL 'kernel32', handle ... at ...>
>>> print cdll.msvcrt
<CDLL 'msvcrt', handle ... at ...>
>>> libc=cdll.msvcrt
>>>

Windows通常使用".dll"作为动态链接库的扩展名。

Linux上需要指定包含扩展名的文件名来载入动态库,所以属性存取方式就失效了。你可以使用 LoadLibrary 方法,或者创建CDLL的实例来载入:

>>> cdll.LoadLibrary("libc.so.6")
<CDLL 'libc.so.6', handle ... at ...>
>>> libc==CDLL("libc.so.6")
>>> libc
<CDLL 'libc.so.6', handle ... at ...>
>>>

1.2   从载入的动态链接库中存取函数

函数是作为dll对象的属性来存取的:

>>> from ctypes import *
>>> libc.printf
<_FuncPtr object at 0x...>
>>> print windll.kernel32.GetModuleHandleA
<_FuncPtr object at 0x...>
>>> print windll.kernel32.MyOwnFunction
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "ctypes.py", line 239, in __getattr__
func = _StdcallFuncPtr(name,self)
AttributeError: function 'MyOwnFunction' not found
>>>

主意win32系统动态链接库,如kernel32和user32经常同时导出ANSI和UNICODE版本的函数。UNICODE版本的会在名字末尾加"W",而ANSI版本的加上"A"。Win32版本的 GetModuleHandle 函数,返回给定模块名的句柄,有如下C原型,还有一个宏用于暴露其中一个作为 GetModuleHandle ,依赖于UNICODE定义与否:

/* ANSI version */
HMODULE GetModuleHandleA(LPCSTR lpModuleName);
/* UNICODE version */
HMODULE GetModuleHandleW(LPCWSTR lpModuleName);

windll 并不会自动选择调用某个版本,所以你必须指定要调用的,传递的时候也要指定正确的字符串参数类型。

有时动态链接库导出函数并不是有效的Python标识符,例如 "??2@YAPAXI@Z" 。这种情况下,你必须使用 getattr获取函数:

>>> getattr(cdll.msvcrt,"??2@YAPAXI@Z")
<_FuncPtr object at 0x...>
>>>

在Windows上,有些动态链接库导出函数不是用名字,而是用序号(ordinal)。这些函数通过索引存取:

>>> cdll.kernel32[1]
<_FuncPtr object at 0x...>
>>> cdll.kernel32[0]
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "ctypes.py", line 310, in __getitem__
func = _StdcallFuncPtr(name,self)
AttributeError: function ordinal 0 not found
>>>

1.3   调用函数

你可以像正常的Python函数一样调用这些函数。这里用 time() 函数示例,返回Unix epoch系统时间,和GetModuleHandleA() 函数,返回win32模块句柄。

这个例子调用函数时附带NULL指针(None作为NULL指针):

>>> print libc.time(None)
1150640792
>>> print hex(windll.kernel32.GetModuleHandleA(None))
0x1d000000
>>>

在调用函数时,如果使用了错误的参数数量和调用规范时,ctypes尝试保护调用。不幸的是该功能仅在Windows上有用。它通过检查函数返回栈来实现,所以尽管发生了错误,但是函数还是调用了:

>>> windll.kernel32.GetModuleHandleA()
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: Procedure probably called with not enough argument (4 bytes missing)
>>> windll.kernel.GetModuleHandleA(0,0)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: Procedure probably called with too many argument (4 bytes in excess)

这在你使用了错误的调用规范时同样会发生:

>>> cdll.kernel32.GetModuleHandleA(None) # doctest: +WINDOWS
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: Procedure probably called with not enough arguments (4 bytes missing)
>>>

>>> windll.msvcrt.printf("spam") # doctest: +WINDOWS
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: Procedure probably called with too many arguments (4 bytes in excess)
>>>

想要找到正确的调用规范,你必须查看C头文件或者函数的文档。

在Windows,ctypes使用win32结构异常处理,避免无保护的挂掉:

>>> windll.kernel32.GetModuleHandleA(32)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
WindowsError: exception: access violation reading 0x00000020

尽管如此,仍然有很多方法用ctypes挂掉Python,所以你必须很小心的使用。

None、整数、长整数、字节串和unicode字符串是可以作为本地Python对象直接传递给函数调用的。None是作为C的NULL指针,字 节串和unicode字符串作为内存块指针传递(char* 或 wchar_t*)。Python整数和长整数作为平台相关的C类型传递。

在调用更多的函数之前,必须了解关于ctypes数据类型的知识。

1.4   基本数据类型

ctypes定义了一系列基本C数据类型:

ctypes类型 C类型 Python类型
c_char char 1个字符的字符串
c_wchar wchar_t 1个字符的unicode字符串
c_byte char int/long
c_ubyte unsigned char int/long
c_short short int/long
c_ushort unsigned short int/long
c_int int int/long
c_uint unsigned int int/long
c_long long int/long
c_ulong unsigned long int/long
c_longlong __int64 或 long long int/long
c_ulonglong unsigned __int64 或 unsigned long long int/long
c_float float float
c_double double float
c_char_p char * (NUL结尾字符串) string或None
c_wchar_p wchar_t * (NUL结尾字符串) unicode或None
c_void_p void * int/long或None

所有这些类型都可以通过调用可选传输初始化值方式指定值:

>>> c_int()
c_long(0)
>>> c_char_p("Hello, world")
c_char_p('Hello, world')
>>> c_ushort(-3)
c_ushort(65533)
>>>

这些类型都是可变的,其值也是随后可变的:

>>> i=c_int(42)
>>> print i
c_long(42)
>>> print i.value
42
>>> i.value=-99
>>> print i.value
-99
>>>

对指针类型 c_char_p/c_wchar_p/c_void_p 的赋值将会改变其指向的内存区域地址,而不是改变内存块的值(当然了,因为Python字符串是只读的):

>>> s="Hello, world"
>>> c_s=c_char_p(s)
>>> print c_s
c_char_p('Hello, world')
>>> c_s.value="Hi, there"
>>> print c_s
c_char_p('Hi, there')
>>> print s #第一个字符串没有改变
Hello, world
>>>

必须小心的是,不要传递这些的指针给可变内存。如果你需要可变内存块,ctypes提供了 create_string_buffer()函数。当前内存块可以存取或改变,如果你想要将其作为NUL结尾字符串方式,使用值的方法:

>>> from ctypes import *
>>> p = create_string_buffer(3) # create a 3 byte buffer, initialized to NUL bytes
>>> print sizeof(p), repr(p.raw)
3 '\x00\x00\x00'
>>> p = create_string_buffer("Hello") # create a buffer containing a NUL terminated string
>>> print sizeof(p), repr(p.raw)
6 'Hello\x00'
>>> print repr(p.value)
'Hello'
>>> p = create_string_buffer("Hello", 10) # create a 10 byte buffer
>>> print sizeof(p), repr(p.raw)
10 'Hello\x00\x00\x00\x00\x00'
>>> p.value = "Hi"
>>> print sizeof(p), repr(p.raw)
10 'Hi\x00lo\x00\x00\x00\x00\x00'
>>>

create_string_buffer() 函数已经替换了 c_buffer() 函数(仍然作为别名存在),有如 c_string() 函数以前,只是出现在以前的版本中。想要创建包含unicode字符(对应C类型wchar_t)的可变内存块,使用 create_unicode_buffer()函数。

1.5   调用函数,继续

需要注意的是,printf打印到真实的标准输出,而不是 sys.stdout ,所以这些例子仅在控制台模式有效,而不是IDLE或PythonWin:

>>> printf=libc.printf
>>> printf("Hello, %s\n","World!")
Hello, World!
14
>>> printf("Hello, %S", u"World!")
Hello, World!
13
>>> printf("%d bottles of beer\n", 42)
42 bottles of beer
19
>>> printf("%f bottles of beer\n", 42.5)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ArgumentError: argument 2: exceptions.TypeError: Don't know how to convert parameter 2
>>>

有如前面所说,除了整数、字符串和unicode字符串以外的Python类型必须使用ctypes类型做包装,所以他们可以转换为必须的C数据类型:

>>> printf("An int %d, a double %f\n",1234,c_double(3.14))
Integer 1234, double 3.1400001049
31
>>>

1.6   使用自定义数据类型调用函数

你可以使用自定义ctypes参数转换,允许你自己的类作为函数参数。ctypes寻找对象的 _as_parameter_ 属性,并将其作为函数参数。当然,必须是整数、字符串或unicode

>>> class Bottles(object):
... def __init__(self, number):
... self._as_parameter_ = number
...
>>> bottles = Bottles(42)
>>> printf("%d bottles of beer\n", bottles)
42 bottles of beer
19
>>>

如果你不想存储实例的数据到 _as_parameter_ 实例变量,你可以定义一个属性确保数据有效。

1.7   指定必须的参数类型(函数原型)

可以通过指定函数的 argtypes 属性来指定函数的参数类型。

argtypes必须是一个C数据类型序列(printf函数在这里不是个好例子,因为它需要依赖于格式化字符串的可变数量和多种类型的参数,反过来说倒是很适合于练手):

>>> printf.argtypes=[c_char_p,c_char_p,c_int,c_double]
>>> printf("String '%s', Int %d, Double %f\n","Hi",10,2.2)
String 'Hi', Int 10, Double 2.200000
37
>>>

指定不兼容的参数类型,和尝试转换参数到到无效类型会出错:

>>> printf("%d %d %d", 1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ArgumentError: argument 2: exceptions.TypeError: wrong type
>>> printf("%s %d %f", "X", 2, 3)
X 2 3.00000012
12
>>>

如果你自定义的类要传递给函数调用,必须实现 from_param 类方法,才能在argtypes序列中使用。 from_param类方法接收Python对象传递到函数调用,需要做类型检查或者其他确保对象可以被接受的工作,然后返回对象本身,_as_parameter_ 属性,或者你想要传递给C函数的参数。再次说明,返回结果必须是整数、字符串、unicode、ctypes实例,或者任何有 _as_parameter_ 属性的东西。

1.8   返回类型

缺省情况假设函数返回C的int类型。其他返回类型可以通过设置函数的 restype 属性来实现。

这里是一个更高级的例子,它使用strchr函数,需要一个字符串指针和一个字符,返回字符串的指针:

>>> strchr = libc.strchr
>>> strchr("abcdef", ord("d")) # doctest: +SKIP
8059983
>>> strchr.restype = c_char_p # c_char_p is a pointer to a string
>>> strchr("abcdef", ord("d"))
'def'
>>> print strchr("abcdef", ord("x"))
None
>>>

如果你想要上面的 ord("x") 调用,你可以设置argtypes属性,而第二个参数的Python字符串会转换成C字符:

>>> strchr.restype = c_char_p
>>> strchr.argtypes = [c_char_p, c_char]
>>> strchr("abcdef", "d")
'def'
>>> strchr("abcdef", "def")
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ArgumentError: argument 2: exceptions.TypeError: one character string expected
>>> print strchr("abcdef", "x")
None
>>> strchr("abcdef", "d")
'def'
>>>

你还可以使用Python的可调用对象(函数或者类的例子)作为restype属性,如果外语函数返回整数。这时在C函数调用结束后会使用其返回的 整数调用这个Python可调用对象,而返回值作为函数调用的返回值。相当于对C函数返回值做了包装。这对于检查错误码而抛出异常的情况非常有用:

>>> GetModuleHandle = windll.kernel32.GetModuleHandleA # doctest: +WINDOWS
>>> def ValidHandle(value):
... if value == 0:
... raise WinError()
... return value
...
>>>
>>> GetModuleHandle.restype = ValidHandle # doctest: +WINDOWS
>>> GetModuleHandle(None) # doctest: +WINDOWS
486539264
>>> GetModuleHandle("something silly") # doctest: +WINDOWS
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 3, in ValidHandle
WindowsError: [Errno 126] The specified module could not be found.
>>>

这里的 WinError 是一个函数,会调用Windows的 FormatMessage() API来获取错误码的字符串描述,并且返回异常。 WinError 接受可选的错误码参数,如果没有指定则调用 GetLastError() 获取。

需要注意的是强大的错误检查机制是通过 errcheck 属性实现的。具体查看手册了解细节。

1.9   传递指针(或者传递参数引用)

有时C函数需要一个指针指向的数据作为参数,还有可能是想向里面写的位置,或者数据太大不适合传递。这也叫做传递参数引用。

ctypes导出 byref() 函数用于传递参数引用。同样也可以用于指针函数,尽管指针对象可以做很多工作,但是如果你并不需要在Python中使用指针对象的话,使用 byref() 会更快:

>>> i = c_int()
>>> f = c_float()
>>> s = create_string_buffer('\000' * 32)
>>> print i.value, f.value, repr(s.value)
0 0.0 ''
>>> libc.sscanf("1 3.14 Hello", "%d %f %s",
... byref(i), byref(f), s)
3
>>> print i.value, f.value, repr(s.value)
1 3.1400001049 'Hello'
>>>

1.10   结构和联合

结构和联合必须继承自ctypes模块的 Structure 和 Union 类。每个子类必须定义 _fields_ 属性,该属性必须是2元素元组的列表,包含字段名和字段类型。

字段类型必须是ctypes类型,例如 c_int ,或者其他派生的ctypes类型:结构、联合、数组、指针。

这里有个POINT结构体的简单例子,包含两个整数叫做x和y,同时展示了如何构造结构体:

>>> from ctypes import *
>>> class POINT(Structure):
... _fields_ = [("x", c_int),
... ("y", c_int)]
...
>>> point = POINT(10, 20)
>>> print point.x, point.y
10 20
>>> point = POINT(y=5)
>>> print point.x, point.y
0 5
>>> POINT(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: too many initializers
>>>

你还可以构造跟多复杂的结构体。结构体可以自包含作为一个字段类型。

@waiting ...

1.11   结构/联合对齐和字节序

默认情况下结构体和联合的对齐使用C编译器相同的方式。这可以通过 _pack_ 类属性来重载其行为。这必须设置一个正数指定字段的最大对齐。这个功能与MSVC中的 #pragma pack(n) 功能一样。

ctypes中的结构体和联合使用本地字节序。想要用非本地字节序,可以使用 BigEndianStructure 、LittleEndianStructure 、 BigEndianUnion 、 LittleEndianUnion 基类。这些类无法包含指针字段。

1.13   数组

数组就是序列,包含固定数量(fixed number of)的相同类型的实例。

推荐的创建数组类型的方式是使用正数和乘号应用到类型:

TenPointsArrayType=POINT*10

这里有个巧妙的例子,一个结构体包含一个字段有4个POINT:

>>> from ctypes import *
>>> class POINT(Structure):
... _fields_ = ("x", c_int), ("y", c_int)
...
>>> class MyStruct(Structure):
... _fields_ = [("a", c_int),
... ("b", c_float),
... ("point_array", POINT * 4)]
>>>
>>> print len(MyStruct().point_array)
4
>>>

@waiting ...

1.14   指针

指针实例使用 pointer() 函数:

>>> from ctypes import *
>>> i=c_int(42)
>>> pi=pointer(i)
>>>

指针实例有一个 contents 属性返回指针指向的内容对象,例如上面的例子:

>>> pi.contents
c_long(42)
>>>

注意ctypes没有OOR(Original Object Return原始对象返回),他在你请求一个属性时构造一个新的、等同的对象:

>>> pi.contents is i
False
>>> pi.contents is pi.contents
False
>>>

@waiting ...