ctypes: 一种用于Python的外部函数库【官方文档翻译】
ctypes 是一个用于Python的外部语言库,ctypes提供了C兼容的数据类型,并且可以Python通过ctypes模块调用DLL(Windows)或者共享链接库(Linux)中的函数。 ctypes模块可以用来将这些库包装在纯Python中。
本教程来自于python3.8.14官方文档,并做了一些改动,外语水平有限,如有翻译不当之处,请多包涵。
注意本教程需要读者有一定的C基础知识,至少应了解C的基本数据类型,数组,指针,结构体和基础的语法。
文章目录
1 加载动态链接库(DLL)
ctypes模块中有cdll对象,在Windows平台上还有windll和oledll用于加载动态链接库,其中
- cdll用于加载根据cdecl调用协议编译的函数(C/C++的默认方式)
- windll用于加载根据stdcall调用协议编译的函数
- oledll用于加载根据stdcall调用协议编译的函数,并假定该函数返回的是Windows HRESULT代码,并当该函数调用失败时,自动根据HRESULT代码抛出一个OSError异常
下面是在Windows平台上的示例。其中msvcrt是微软的标准C库,其中包含了大多数标准C函数,并使用了cdecl调用约定:
>>> from ctypes import *
>>> print(windll.kernel32) # Windows 会自动为文件添加.dll后缀名
<WinDLL 'kernel32', handle ... at ...>
>>> print(cdll.msvcrt)
<CDLL 'msvcrt', handle ... at ...>
>>> libc = cdll.msvcrt
【注】通过
cdll.msvcrt
访问C标准库,可能会使用一个已过时版本的C标准库,有可能与当前Python正在使用的库不兼容。因此在可能的情况下,尽可能使用Python原生的函数,或者通过直接导入Python定义的msvcrt模块来使用C标准库。
当你使用cdecl调用协议调用使用stdcall协议编译的函数时,Python虚拟机会抛出一个ValueError异常,反之亦然。要找到正确的调用约定,您必须查看要调用的函数的C头文件或接口文档
>>> cdll.kernel32.GetModuleHandleA(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Procedure probably called with not enough arguments (4 bytes missing)
>>>
Windows系统会自动添加.dll文件后缀名,但是与Windows平台上不同,在Linux上,加载库时所用的文件名需包含文件的后缀名。在Linux平台上不能使用属性访问的方式来加载库。可以使用dll加载器的LoadLibrary()
方法或者通过调用构造函数创建CDLL
的实例来加载库:
>>> from ctypes import *
>>> cdll.LoadLibrary("libc.so.6")
<CDLL 'libc.so.6', handle ... at ...>
>>> libc = CDLL("libc.so.6")
>>> libc
<CDLL 'libc.so.6', handle ... at ...>
2 访问加载的DLL库中的函数
动态链接库中的函数可以作为dll实例对象的属性访问,如下
from ctypes import *
libc = CDLL("./libtest.so")
print(libc) # 输出 <CDLL './libtest.so', handle 2173fd9c0 at 0x7fa26ed303d0>
print(libc.print_function) # 输出 <_FuncPtr object at 0x7f8fb8740040>
# 调用库中的函数
print(libc.print_function()) # 输出test print function!
以下是libtest.so
库的C源代码
#include <stdio.h>
void print_function(){
printf("test print function!");
}
在有些情况下,有些从dll中导出的函数,可能在Python中不是一个合法的标识符,(比如"??2@YAPAXI@Z",它可能在某一种外部语言中是一个合法的标识符),这种情况下无法直接使用属性的方式访函数。这时必须使用Python内置的getattr()
函数来获取DLL或者SO库中的函数
import ctypes
libc = ctypes.CDLL("./libtest.so")
print(getattr(libc, "print_function"))
# 输出:<_FuncPtr object at 0x7f91d2140040>
print(getattr(libc, "unknown_function"))
# 输出:
# Traceback (most recent call last):
# File ".../t.py", line 7, in <module>
# print(getattr(libc, "unknown_function"))
# File ".../ctypes/__init__.py", line 395, in __getattr__
# func = self.__getitem__(name)
# File ".../ctypes/__init__.py", line 400, in __getitem__
# func = self._FuncPtr((name_or_ordinal, self))
# AttributeError: dlsym(0x20ba2b9c0, unknown_function): symbol not found
3 调用函数
在加载了DLL中的函数后,就可以像调用一般的Python函数一样调用DLL中的函数,下面的例子中调用了C标准库中的time()
函数
import ctypes
libc = ctypes.CDLL("./libtest.so")
print(libc.my_time(None))
# 输出:1667114869
libtest.so的C源代码如下:
#include <time.h>
time_t my_time(time_t *timer){
return time(timer);
}
其中libc.my_time(None)
中将Python的None
关键字作为C语言中的NULL空指针
传递给C函数。
使用ctypes有足够多的方法使Python崩溃,所以无论如何使用外部库时你都应该小心。Python也提供的faulthandler
模块在调试崩溃时也很有帮助。(例如,由错误的C库调用产生的段错误)
3.1 ctypes 数据类型
C语言作为一种强类型语言,函数调用时需要指定参数的类型,但Python作为一种弱类型则不需要显式指定,且Python与C语言所支持的数据类型并不相同,因此在使用Python语言调用C编写的函数时,就需要我们手动处理两种语言之间的数据类型差异。
Python仅有None、integers、bytes对象和字符串(ANSC或者unicode字符串均可)这四种Python原生数据类型可以直接用作这些DLL中的C函数调用参数。其中,None作为C NULL指针传递,字节对象和字符串作为指针传递,指向包含其数据的内存块(char *或wchar_t *)。Python整数作为操作系统默认的C int类型传递,ctypes将自动的处理它们的值以适应C类型。对于其他的数据类型则需要我们显式的处理,在学习如何处理这些参数之前,我们首先来了解一下ctypes数据类型的信息:
3.1.1 ctypes基本数据类型
ctypes数据类型 | C数据类型 | Python数据类型 |
---|---|---|
NULL | None | |
c_bool | _Bool | bool |
c_char | char | 1-character bytes对象 |
c_wchar | wchar_t | 1-character string |
c_byte | char | int |
c_ubyte | unsigned char | int |
c_short | short | int |
c_ushort | unsigned short | int |
c_int | int | int |
c_uint | unsigned int | int |
c_long | long | int |
c_ulong | unsigned long | int |
c_longlong | long long或者__int64 | int |
c_ulonglong | unsigned long long 或者unsigned __64 | int |
c_size_t | size_t | int |
c_ssize_t | ssize_t或者Py_ssize_t | int |
c_float | float | float |
c_double | double | float |
c_longdouble | double | float |
c_char_p | char* | bytes对象或者None |
c_wchar_p | wchar_t* | string或者None |
c_void_p | void* | int或者None |
【注】在引用ctypes中的c_int类型时,在sizeof(long) == sizeof(int)的平台上,c_int只是c_long的一个别名,在这样的平台上,c_int本质上就是c_long类型
3.1.2 数据类型的创建
所有上述的类型,都可以通过调用ctypes类型的构造方法来创建,注意你需要为构造方法提供正确的参数,即提供正确的类型核合法的值,否则Python会抛出一个TypeError异常。
>>> import ctypes
>>> ctypes.c_int()
c_int(0)
>>> ctypes.c_wchar_p("12aaas4")
c_wchar_p(140451566736912)
>>> ctypes.c_ushort(-3)
c_ushort(65533)
>>> ctypes.c_ushort("as")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: an integer is required (got type str)
这些类型是可变的,其值也是可以改变的
>>> import ctypes
>>> i = ctypes.c_int(1)
>>> print(i)
c_int(1)
>>> i.value = 99
>>> print(i)
c_int(99)
但是给指针类型重新赋值时要格外的注意,指针类型存储的是指向变量的地址,因此给指针类型(包括c_char_p,c_wchar_p和c_void_p三种)重新赋值,并不会改变内存块的内容,只是会改变指针指向的位置。(熟悉C语言指针的读者应该对此不陌生)所以使用指针类型时应该小心,不要为指针赋值一个你期望改变内容的内存块。
>>> s = "Hello, World"
>>> c_s = c_wchar_p(s)
>>> print(c_s)
c_wchar_p(139966785747344)
>>> print(c_s.value)
Hello World
>>> c_s.value = "Hi, there"
>>> print(c_s)
c_wchar_p(139966783348904)
>>> print(c_s.value)
Hi, there
>>> print(s)
Hello, World
ctypes也提供了对可变内存块的支持,在ctypes模块中定义了create_string_buffer()
函数,该函数可以创建一个可改变内容的内存块。通过该函数创建的内存块,可以通过访问它的raw属性来访问或者修改它的内容,当然如果你想访问以NUL结尾的字符串,可以使用value属性,但要注意,与C的数组类似,通过create_string_buffer创建的内存块的长度是固定的。
>> from ctypes import *
>>> p = create_string_buffer(5)
>>> print(sizeof(p), p.raw)
5 b'\x00\x00\x00\x00\x00'
>>> p.raw=b"aa"
>>> print(sizeof(p), p.raw)
5 b'aa\x00\x00\x00'
>>> p = create_string_buffer(b"Hello Buffer!")
>>> print(sizeof(p), p.raw)
14 b'Hello Buffer!\x00'
>>> print(p.value)
b'Hello Buffer!'
>>> p = create_string_buffer(b"Test",10)