Ctypes: 一种用于Python的外部函数库【官方文档翻译】

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用于加载动态链接库,其中

  1. cdll用于加载根据cdecl调用协议编译的函数(C/C++的默认方式)
  2. windll用于加载根据stdcall调用协议编译的函数
  3. 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)
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值