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

ctypes 是一个用于Python的外部语言库,ctypes提供了C兼容的数据类型,并且可以Python通过ctypes模块调用DLL(Windows)或者共享链接库(Linux)中的函数。 ctypes模块可以用来将这些库包装在纯Python中。

本教程来自于python3.8.14官方文档,并做了一些改动,外语水平有限,如有翻译不当之处,请多包涵。

注意本教程需要读者有一定的C基础知识,至少应了解C的基本数据类型,数组,指针,结构体和基础的语法。


文章目录

  • ctypes: 一种用于Python的外部函数库【官方文档翻译】
  • 1 加载动态链接库(DLL)
  • 2 访问加载的DLL库中的函数
  • 3 调用函数
  • 3.1 ctypes 数据类型
  • 3.1.1 ctypes基本数据类型
  • 3.1.2 数据类型的创建
  • 3.2 函数的调用
  • 3.2.1 指定所需的参数类型(函数原型)
  • 3.2.2 使用自定义类型调用函数
  • 3.2.3 返回类型
  • 3.2.4 传递指针参数(通过引用传递参数)
  • 4 数组和指针
  • 4.1 数组
  • 4.1.1 数组的定义与使用
  • 4.1.2 大小可变的数据类型
  • 4.2 指针
  • 4.2.1 指针的定义与使用
  • 4.2.2 自动类型转换
  • 5 结构体
  • 5.1 结构体的定义
  • 5.2 结构体作为函数的参数
  • 5.3 结构体/联合体中的对齐和字节顺序
  • 5.4 结构体或联合体中的位域
  • 6 访问DLL中的变量
  • 7 回调函数


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模块怎么安装 python ctypes模块_bc


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)
>>> print(sizeof(p), p.raw)
10 b'Test\x00\x00\x00\x00\x00\x00'
>>> p.value=b"1234"
>>> print(sizeof(p), p.raw)
10 b'1234\x00\x00\x00\x00\x00\x00'
>>> p.raw=b"Test123"
>>> print(sizeof(p), p.raw)
10 b'Test123\x00\x00\x00'
>>> p.raw=b"1234567890123"
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
ValueError: byte string too long

3.2 函数的调用

如前所述,Python中除了integer、string和bytes对象之外的所有Python类型都必须包装在相应的ctypes类型中,以便可以将其转换为所需的C数据类型,如下

import ctypes

libc=ctypes.CDLL("./libtest.so")
doublea = ctypes.c_double(2.3333)

libc.my_print(10, doublea)
# 输出: this is an integer 10, this is a double 2.333300

对应的C源代码如下

#include <stdio.h>

void my_print(int a, double b){
    printf("this is an integer %d, this is a double %lf", a, b);
}

请注意,C的printf函数只打印到真正的标准输出通道,而不是sys.stdout。因此,这些例子只能在命令提示符(windows)或者终端(linux)下运行,而不能在IDLE或PythonWin中运行:

3.2.1 指定所需的参数类型(函数原型)

在调用函数时,除了整数,字符串和字节类型外,均需要显示的做类型转换,当不做类型转换时,就会抛出ArgumentError异常,如下:

from ctypes import *

libc=CDLL("./libtest.so")

libc.my_print(2.333)
# Traceback (most recent call last):
#     File ".../t.py", line 6, in <module>
#         libc.my_print(2.333)
# ctypes.ArgumentError: argument 1: <class 'TypeError'>: Don't know how to convert parameter 1

libc.my_print(c_double(2.333)) # 正常运行

但是这样调用函数较为繁琐,且不符合我们使用函数的习惯,ctypes模块提供了一种方法,可以让我们使用常规的函数调用方法调用DLL中的函数,而不需要我们每次调用都需要做显示的类型转换。可以通过设置argtypes属性,可以指定从dll导出的函数所需的参数类型(类似C语言中的函数原型),argtypes必须是一系列Ctypes数据类型如下

from ctypes import *

libc=CDLL("./libtest.so")

libc.my_print.argtypes=[c_double]
libc.my_print(2.333) # 可正常运行,输出The double is 2.333000
3.2.2 使用自定义类型调用函数

ctypes也可以用于自定义类型,ctypes在处理自定义的Python类时,会在类的定义中查找一个_as_parameter_属性,并将该属性的值用作调用函数的参数。但是要注意的是,_as_parameter_的值不是Python内置类型中的int、str或者bytes中的一个,请注意必须使用ctypes的类型声明包装,不可以直接传值

import ctypes

class MyClass:
    def __init__(self, number):
        self._as_parameter_ = number

libc=ctypes.CDLL("./libtest.so")

c = MyClass(2)
libc.my_print(c) # 输出:The number is 2

c = MyClass(2.333)
libc.my_print(c)

# 输出(若_as_parameter_的值不是Python内置类型中的int、str或bytes,必须使用ctypes的类型声明包装)
# Traceback (most recent call last):
#     File ".../t.py", line 26, in <module>
#         libc.my_print(c)
# ctypes.ArgumentError: argument 1: <class 'TypeError'>: Don't know how to convert parameter 1
// libtest.so的C源代码
#include <stdio.h>

void my_print(int b){
    printf("The number is %d", b);
}

如果你不想将实例的数据存储在_as_parameter_实例变量中,你可以使用Python的内置函数property定义一个属性,使该属性在请求时可用:

import ctypes

class MyClass:
    def __init__(self, d):
        self.da = d
    def _get_data(self):
        return self.da
    _as_parameter_ = property(_get_data)

libc=ctypes.CDLL("./libtest.so")

m = MyClass(ctypes.c_double(2.333333))

libc.my_print.argtypes = [ctypes.c_double]
libc.my_print(m) # The number is 2.333333

# libtest.so C源代码
# include <stdio.h>
# void my_print(double b){
#     printf("The number is %lf", b);
3.2.3 返回类型

默认情况下,ctypes会假定DLL中的函数返回C语言的int类型。其他返回类型可以通过设置函数对象的restype属性来指定。

下面的例子中使用了C标准库中的strchr函数,该函数需要一个字符串指针和一个字符,并返回一个指向字符串的指针:

from ctypes import *

libc=CDLL("./libtest.so")

print(libc.my_strchr(b"abcdef", ord("d")))  # 1072907523

libc.my_strchr.restype = c_char_p
print(libc.my_strchr(b"abcdef", ord("d")))  # b'def'

print(libc.my_strchr(b"abcdef", ord("x")))  # None

上面的例子中之所以使用ord函数,是因为与C的char类型对应的是1-character bytes 但是如果直接使用b"d"的话,则返回的并不是一个1-character bytes,而是bytes(注意1-character bytes在Python中并不是一个类型),因此需要使用ord函数将其转换为ASCII值或者Unicode值。如果你觉得使用Python内置的ord函数ord有些繁琐的话,可以通过设置argtypes属性,则第二个参数将从单个字符的Python bytes对象自动转换为C字符。

from ctypes import *

libc=CDLL("./libtest.so")

libc.my_strchr.argtypes = [c_char_p, c_char]
libc.my_strchr.restype = c_char_p
print(libc.my_strchr(b"abcdef", b"d"))  # b'def'

示例对应的C源代码如下:

// libtest.so
#include <string.h>

char* my_strchr(const char *str, char c){
    return strchr(str, c);
}

如果外部函数返回一个整数,你也可以使用一个可调用的Python对象(callable Python object)作为restype属性,比如例如函数或类。这样在接收到DLL中的函数返回的整数值时,ctypes将会自动的把返回的整数作为可调用对象的参数来调用这个方法,可调用对象返回的结果会作为最终的结果返回给用户。这种机制在检查错误返回值并自动抛出异常时非常有用:

# libtest.so的C源代码
# int test(int a, int b){
#     return a-b;
# }

from ctypes import *

def compare(re):
    if re > 0:
        print("a > b")
    elif re == 0:
        print("a = b")
    else:
        print("a < b")

dll = cdll.LoadLibrary("./libtest.so")

dll.test.restype = compare

dll.test(1,2)
dll.test(2,2)
dll.test(3,2)
# 输出
# a < b
# a = b
# a > b
3.2.4 传递指针参数(通过引用传递参数)

有时,C函数需要一个指向数据类型的指针作为参数,可能需要将数据写入相应的位置,或者数据太大而无法通过值传递。这也称为引用传递参数。

Ctypes模块中定义了byref()函数,该函数用于通过引用传递参数。ctypes中还定义了pointer()函数也可以实现相同的效果。但是pointer()会构造出一个真正的指针对象,而byref()仅仅是将地址传递给函数,因此如果你在Python代码中不需要指针对象的话,使用byref()会更快:

from ctypes import *

libc=CDLL("./libtest.so")

a1 = c_int(3)
a2 = c_int(6)

libc.test.restype=c_int

print(libc.test(byref(a1), byref(a2))) # 9
#include <stdio.h>

int test(int *a1, int *a2){
    return *a1+*a2;
}

4 数组和指针

4.1 数组

4.1.1 数组的定义与使用

ctypes也可以使用C语言的数组,数组是序列,包含固定数量的相同类型的实例。

创建数组类型的推荐方法是将数据类型乘以一个正整数:

TenIntArrayType = c_int * 10

上述语句仅仅是声明了一个长度为10的int数组,这句话仅是声明一个类型名称,并没有定义数组的实例。下面是一些数组的示例代码。

>>> from ctypes import *
>>> TenIntArray = c_int * 10  # 定义一个长度为10的int数组,这句话仅是声明一个类型名称,并没有定义数组的实例
>>> TenIntArray
<class '__main__.c_long_Array_10'>

>>> arr = TenIntArray()  # 定义一个数组实例
>>> len(arr)
10
>>> arr
<__main__.c_long_Array_10 object at 0x0000024447FF8B40>
>>> arr[0]
0
>>> arr[0]=123
>>> arr[0]
123
>>> for i in arr: print(i, end=" ")
123 0 0 0 0 0 0 0 0 0

笔者电脑是64位机,在这种类型的系统中,c_int仅仅是c_long的一个别名,因为64位机上的C语言中int和long类型没有区别。详见3.1.1 ctypes基本数据类型。

数组是固定长度的,不可以越界访问,当越界访问时会抛出IndexError异常,数组可以在初始化时指定内容,如下

>>> from ctypes import *
>>> TenDoubleArray = c_double * 10
>>> arr2 = TenDoubleArray(1.2, 2.3, 3.4, 4.5)
>>> for i in arr2: print(i, end=" ")
...
1.2 2.3 3.4 4.5 0.0 0.0 0.0 0.0 0.0 0.0
>>> arr2[11]
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
IndexError: invalid index
>>> arr3 = TenIntArray(1,2,3,4,5,6,7,8,9,10,11)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
IndexError: invalid index
>>> arr3 = TenIntArray(1,2,3,4,5,6,7,8,9,10)
4.1.2 大小可变的数据类型

【写在前面】ctypes 虽然支持可变大小类型,但是通过ctypes改变大小的话,无法访问新增加的部分,比如原来是长度为4的数组[0,1,2,3],通过ctypes将大小改变为8,但是你无法访问第5个以后的元素,即[4,5,6,7]仍然无法访问。所以使用动态类型要么在Python中重新定义一个全新的数据类型,要么通过C语言中的malloc等函数实现。

ctypes虽然提供了resize()函数支持可变大小的数组和结构体。resize()函数可以用来调整现有ctypes对象的内存缓冲区大小。该函数将对象作为第一个参数,请求的字节数作为第二个参数(注意是字节数而不是元素的个数)。内存块不能小于该类型指定的自然大小,(比如short类型,最小应该是2),如果尝试这样做,将引发ValueError。

>>> short_array = (c_short * 4)()
>>> print(sizeof(short_array))
8
>>> resize(short_array, 4)
Traceback (most recent call last):
    ...
ValueError: minimum size is 8
>>> resize(short_array, 32)
>>> sizeof(short_array)
32
>>> sizeof(type(short_array))
8
>>> short_array[:]
[0, 0, 0, 0]
>>> short_array[7]
Traceback (most recent call last):
    ...
IndexError: invalid index

4.2 指针

4.2.1 指针的定义与使用

ctypes模块也支持C语言的指针,通过在ctypes类型上调用pointer()函数可以创建指针实例,指针实例中有一个contents属性,用于指针实际指向的对象

>>> from ctypes import *
>>> i=c_int(123)
>>> pi = pointer(i)
>>> pi
<__main__.LP_c_long object at 0x0000024448042740>
>>> pi.contents
c_long(123)

注意,使用contents属性时,ctypes并没有返回原始的对象,而是会在每次访问contents属性时构造一个新的等价对象

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

将另一个c_int实例赋值给指针的contents属性,将导致该指针指向存储该实例的内存位置:

>>> i = c_int(99)
>>> pi.contents = i
>>> pi.contents
c_long(99)

正如C语言中指针和数组的关系一样,ctypes的指针也可以用整数索引,并且赋值给整数索引会改变所指向的值。

>>> from ctypes import *
>>> a = c_double(2.333)
>>> p = pointer(a)
>>> p[0]
2.333
>>> p[0]=3.444
>>> p[0]
3.444
>>> p[1]
0.0

正如示例中一样,也可以使用不同于0的索引,但你必须知道你在做什么。正如C语言中的指针一样:你可以为指针任意加一个整数来访问或更改任意的内存位置,但是你并不知道你访问的内存内容是什么。一般来说,只有当你从C函数接收到一个指针,并且你知道该指针实际上指向一个数组而不是单个元素时,才会使用此功能。

在pointer()函数的后台中,其必须先创建一个指针类型,再创建指针实例。如果仅仅是想创建一个指针类型的话,可以通过POINTER()函数来完成,它接受任何ctypes类型,并返回一个对应的指针类型:

>>> PI = POINTER(c_int)
>>> PI
<class 'ctypes.LP_c_long'>
>>> PI(42)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: expected c_long instead of int
>>> PI(c_int(42))
<ctypes.LP_c_long object at ...>

调用不带参数的指针类型会创建一个NULL指针(C语言中也称之为野指针)。NULL指针的布尔值为False,ctypes在解引用指针时检查是否为NULL(但解引用无效的非NULL指针同样会使Python崩溃,所以使用指针时必须小心):

>>> null_ptr = POINTER(c_int)() # 等价于C中的 int *p = NULL;
>>> print(bool(null_ptr))
False
>>> null_ptr[0]
Traceback (most recent call last):
    ....
ValueError: NULL pointer access
>>>

>>> null_ptr[0] = 1234
Traceback (most recent call last):
    ....
ValueError: NULL pointer access
>>>
4.2.2 自动类型转换

通常,ctypes会做严格的类型检查。这意味着,如果在函数的argtypes列表中有POINT(c_int),或者在结构体的声明中_field_字段定义的类型,则只接受完全相同类型的实例。这是因为C语言是一种强类型语言

但是这个规则也有一些例外,ctypes可以接受其他对象,正如C有些时候会为一些相互兼容的类型做隐式类型转换一样。例如,对于定义的指针类型,也可以传递兼容的数组实例,而不是指针类型。比如对于POINT(c_int), ctypes同样可以接受一个c_int数组:

from ctypes import *
class Bar(Structure):
    _fields_ = [("count", c_int), ("values", POINTER(c_int))]

bar = Bar()

bar.values = (c_int * 3)(1, 2, 3)
bar.count = 3
for i in range(bar.count):
    print(bar.values[i])
# 输出:
1
2
3

此外,如果函数参数在argtypes中显式声明为指针类型(例如pointer (c_int)),则可以将指针类型的对象(在本例中为c_int)传递给函数。在这种情况下,Ctypes将自动应用所需的byref()转换,如下:

#include <stdio.h>

void print(double *a){
    printf("the int in dll: %lf\n", *a);
}
from ctypes import *

dll = cdll.LoadLibrary("./libtest.so")

dll.print.argtypes = [POINTER(c_double)]

a = c_double(2.3333)
dll.print(a)  # the int in dll: 2.333300

5 结构体

5.1 结构体的定义

结构体和联合体必须继承ctypes模块中定义的结构体基类ctypes.Structure或者联合基类ctypes.Union

在Python中定义的结构体或者联合体类必须定义一个_fields_属性。_fields_必须是一个由含有两个元素的元组组成的列表,包含每一个元组包含字段名称和字段类型。其中字段类型必须是ctypes类型,如c_int,或任何其他派生的ctypes类型,如结构体、联合、数组、指针。

下面时一个结构体的示例,例子中定义了一个名为Point的结构体,在这个结构体中包含了两个整型变量,x和y,示例中展示了如何在构造函数中初始化一个结构体一级如何访问这个结构体,以及如何使用结构体作为字段类型,使结构体本身可以包含其他结构体。

from ctypes import *

class Point(Structure):
    _fields_ = [
        ("x", c_int),
        ("y", c_int)
    ]

p = Point(10, 20)
print(p.x, p.y)  # 输出10 20

3
0 0
0 0

p = Point(y=5)
print(p.x, p.y)  # 输出:0 5

print(p.z)
# 输出:
# Traceback (most recent call last):
#    File ".../t.py", line 22, in #<module>
#    print(p.z)
# AttributeError: 'Point' object has no attribute 'z'

p = Point(1,2,3)
# 输出:
# Traceback (most recent call last):
#    File ".../t.py", line 15, in <module>
#    p = Point(1,2,3)
# TypeError: too many initializers

class Rect(Structure):
    _fields_ = [
        ("upperleft", Point),
        ("lowerright", Point)
    ]

p = Point(x=10, y=20)
r = Rect(p)
print(r.upperleft.x, r.upperleft.y)    # 10 20
print(r.lowerright.x, r.lowerright.y)  # 0 0

r = Rect(Point(1, 2), Point(3, 4))
print(r.upperleft.x, r.upperleft.y)    # 1 2
print(r.lowerright.x, r.lowerright.y)  # 3 4

r = Rect((5, 6), (7, 8))
print(r.upperleft.x, r.upperleft.y)    # 5 6
print(r.lowerright.x, r.lowerright.y)  # 7 8

5.2 结构体作为函数的参数

结构体也可以作为函数的参数传递给DLL库中的函数,具体见如下两个示例:

// libtest.so 源代码
#include <stdio.h>

typedef struct struct_Student{
    char name[10];
    int age;
    float score;
}Student;

void printStudentInfo(Student *s){
    printf("name:%s, arg:%d, score:%.1f\n", s->name, s-> age, s->score);
}

在Python中的调用方式如下:

from ctypes import *

class Student(Structure):
    _fields_ = [
        ('name', c_char*10),
        ('age', c_int),
        ('score', c_float)
    ]

std = Student()
std.name=b"Test"
std.age=18
std.score=86.5

dll = cdll.LoadLibrary("./libtest.so")
dll.printStudentInfo(byref(std))  # 输出:name:Test, arg:18, score:86.5

在Python类中也可以使用结构体作为属性,同时也可以使用_as_parameter_访问:

import ctypes

class E(ctypes.Structure):
    _fields_ = [
        ("a", ctypes.c_double),
    ]

class MyClass:
    def __init__(self, daa):
        self.da = daa
    def _get_daa(self):
        return self.da
    _as_parameter_ = property(_get_daa)

libc=ctypes.CDLL("./e.so")

e = E(2.03)
mm = MyClass(ctypes.pointer(e))

libc.my_print.argtypes = [ctypes.POINTER(E)]
libc.my_print(mm)

libtest.so的源代码如下:

#include <stdio.h>

void my_print(E *b){
    printf("The number is %lf", b->a);
}

结构体的传递是以指针的方式进行的,需要在C中和Python中均声明结构体类型,然后使用byref以传址的方式进行函数的调用。下面是一个更复杂一点的例子

// libtest.so源代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

typedef struct struct_Student{
    char name[10];
    int age;
    float score;
}Student;

typedef struct struct_AllStudents{
    Student *student;
    int number;
}AllStudents;

int initAllStudes(AllStudents* all){
    Student s[2] = {{"Test1", 19, 23.4}, {"Test2", 17, 45.6}};
    
    all->student = (Student*) malloc(sizeof(Student)*2);

    memcpy(all->student, s, sizeof(Student)*2);

    all->number = sizeof(s) / sizeof(Student);
    
    return all->number;
}

void printStudents(AllStudents* all){
    int i = 0;
    for(i=0; i<all->number; i++){
        printf("name:%s, arg:%d, score:%.1f\n", (i+all->student)->name, (i+all->student)->age, (i+all->student)->score);
    }
}
from ctypes import *

class Student(Structure):
    _fields_=[
        ('name', c_char*10),
        ('age', c_int),
        ('score', c_float)
    ]

class AllStudent(Structure):
    _fields_ = [
        ('student', POINTER(Student)),
        ('number', c_int)
    ]

std = AllStudent()

dll = cdll.LoadLibrary("./libtest.so")
dll.initAllStudes.restypes = c_int
print(dll.initAllStudes(byref(std)))

dll.printStudents(byref(std))
# 输出:
# 2
# name:Test1, arg:19, score:23.4
# name:Test2, arg:17, score:45.6

5.3 结构体/联合体中的对齐和字节顺序

默认情况下,结构体和联合体的对齐方式与C编译器相同。可以通过在子类定义中指定一个_pack_类属性来覆盖此行为。这必须设置为一个正整数,并指定字段的最大对齐方式。这也是#pragma pack(n)在MSVC中的作用。

ctypes对结构体和联合体使用原生的字节顺序。要构建非原生字节顺序的结构,可以使用BigEndianStructureLittleEndianStructureBigEndianUnionLittleEndianUnion基类中的一个。这些类不能包含指针字段。

5.4 结构体或联合体中的位域

在C语言中提供了一种数据类型——位域(bit-fields),允许在结构体中以位为单位来指定其成员长度,位域是指信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个布尔量时,只有0和1两种状态,用一位二进位即可,位域往往用于节省内存空间。

struct dataA{

	unsigned int a : 1;
//  :冒号后面的数字表示使用几个bit位。a成员占用一个bit位。
// 如果是unsigned int a : 3;就表示a占用是三个bit位
	unsigned int b : 1;
	unsigned int c : 1;
	unsigned int d : 1;
};

ctypes中也可以创建包含位域的结构体和联合体。位域字段只适用于整数字段,位宽指定为_fields_元组中的第三项:

>>> from ctypes import *
>>> class Int(Structure):
...     _fields_ = [("first_16", c_int, 16),
...                 ("second_16", c_int, 16)]
>>> print(Int.first_16)
<Field type=c_long, ofs=0:0, bits=16>
>>> print(Int.second_16)
<Field type=c_long, ofs=0:16, bits=16>

但是ctypes不支持通过值向函数传递带有位字段的联合体或结构体,带有位域的联合体和结构体总是应该通过指针传递给函数。

6 访问DLL中的变量

在有些共享库不仅导出函数,还导出变量。比如Python库本身的一个例子,Python库中有Py_OptimizeFlag变量,它是一个设置为0、1或2的整数,取决于启动时给出的-O或-OO标志。

ctypes可以使用类型的in_dll()类方法来访问DLL中定义的变量。比如访问DLL中定义的一个double型变量,则使用c_double.in_dll(lib_name, var_name)调用。

下面的C源代码中定义了一个全局变量INT_VAR_IN_DLL

// libtest.so 源代码
#include <stdio.h>

int INT_VAR_IN_DLL = 123;

void print(int a){
    printf("the int in dll: %d\n", a);
}

在ctypes访问这个变量的方法为:

from ctypes import *

dll = cdll.LoadLibrary("./libtest.so")

a = c_int.in_dll(dll, "INT_VAR_IN_DLL")
dll.print(a)  # 输出:the int in dll: 123

7 回调函数

C语言中有函数指针,可以通过函数指针来调用函数,这种通过函数指针调用的函数被称为回调函数。ctypes中也有这样的机制,ctypes允许基于Python callable object(Python可调用对象如函数,callable类等)创建C可调用函数指针。

首先,必须为回调函数创建一个类。这个类知道函数的调用约定(cdecl,stdcall等)、返回类型以及这个函数将接收的参数的数量和类型。

ctypes使用CFUNCTYPE()工厂方法使用cdecl调用约定为回调函数创建类型。在Windows上使用WINFUNCTYPE()工厂函数使用stdcall约定为回调函数创建类型。这两个工厂方法调用时均将回调函数的返回类型作为第一个参数,将回调函数所期望的参数类型作为剩余参数。

// libtest.so 源代码
#include <stdio.h>

void print(int (*add)(int, int), int a1, int a2){
    printf("the sum : %d\n", (*add)(a1, a2));
}
from ctypes import *

def my_add(a, b):
    return 10*(a+b)

ADD_FUNC = CFUNCTYPE(c_int, c_int, c_int)
add = ADD_FUNC(my_add)

dll = cdll.LoadLibrary("./libtest.so")
dll.print(add, c_int(23), c_int(10))    # 输出:the sum : 330

下面是一个更实际的例子,示例中将展示一个使用标准C库的qsort()函数的示例,该函数在回调函数的帮助下用于对元素进行排序。Qsort()将用于对整数数组进行排序:

// libtest.so C源代码
#include <stdlib.h>

// qsort函数的原型
// void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
//
// base-- 指向要排序的数组的第一个元素的指针。
// nitems-- 由 base 指向的数组中元素的个数。
// size-- 数组中每个元素的大小,以字节为单位。
// compar-- 用来比较两个元素的函数,即函数指针(回调函数)如果第一个元素小于第二个元素,则返回负整数,如果两个元素相等则返回0,否则返回正整数。

void int_qsort(int *base, size_t nitems, size_t size, int (*compar)(const int*, const int*)){
    qsort(base, nitems, size, compar);
}
from ctypes import *

# 定义用于比较大小的回调函数
def compare(a, b):
    print("compare_func ", a[0], b[0])
    return a[0] - b[0]

IntArray5 = c_int * 5
ia = IntArray5(5, 1, 7, 33, 99)

# 创建回调函数类型
CMPFUNC = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))

dll = cdll.LoadLibrary("./libtest.so")
dll.int_qsort(ia, len(ia), sizeof(c_int), CMPFUNC(compare))
for i in ia: print(i, end=" ")
# 输出:
# compare_func  5 1
# compare_func  5 7
# compare_func  7 33
# compare_func  33 99
# 1 5 7 33 99

【注意】只要在C代码中使用了CFUNCTYPE()对象,就一定要保留对它们的引用。Ctypes不会,如果你不这样做,它们可能会被垃圾回收,在回调时使程序崩溃。

此外,请注意,如果回调函数是在Python控制之外创建的线程中调用的,ctypes会在每次调用时创建一个新的虚拟Python线程。这种行为在大多数情况下都是正确的,但它意味着值存储在线程中。即使这些调用来自同一个C线程,theading.local也不会在不同的回调函数之间存在。