1. 概述

ctypes 提供与 C 语言兼容的数据类型,可以直接调用动态链接库中的导出函数。

为使用 ctypes,必须依次完成以下步骤:


2. 动态链接库加载方式

它们的不同之处是动态链接库中的函数所遵守的函数调用方式(calling convention)以及返回方式

cdll 用于加载遵循 cdecl 调用约定的动态链接库;windll 用于加载遵循 stdcall 调用约定的动态链接库;oledllwindll 的唯一不同是默认其载入的函数统一返回 Windows HRESULT 错误编码。

函数调用约定

函数调用约定是指函数参数入栈的顺序、哪些参数入栈、哪些通过寄存器传值、函数返回时帧的回收方式(由调用者负责清理,还是被调用者负责清理)、函数名称的修饰方法等。

常见的调用约定有 cdeclstdcall 两种。在《程序员的自我修养 -- 链接、装载与库》一书的第 10 章有对函数调用约定的更详细介绍。

cdecl 规定函数参数列表以从右到左的方式入栈,并且由函数的调用者负责清除帧上的参数。stdcall 的参数入栈方式与 cdecl一致,但由被调用者负责清理帧。stdcall 是 Win32 API 函数所使用的调用约定。


3. 示例

Linux:

或者:

其它示例:


4. 完整示例

4.1. 编写动态链接库

foo.c 编译为动态链接库:

4.2. 使用 ctypes 调用 foo.so

执行结果:


5. ctypes 数据类型与 C 数据类型对照表

ctypes-c


6. 查找动态链接库


7. 函数返回类型

函数默认返回 C int 类型,如果需要返回其它类型,那么需要设置 restype 属性。


8. 回调函数

在 Python 中,定义回调函数类型,类似于 C 中的函数指针。比如:

定义为

其中,None 表示返回类型是 void,也可以是其它类型;其余两个参数与 C 中的回调参数一致。

使用如下方式注册回调函数:

另外,使用 ctypes 可以避免 GIL 带来的问题。

回调函数示例如下:

编译成动态链接库:

测试代码如下:

执行:


9. 结构体和联合

Union(联合体/共用体):

  1. Union 中可以定义多个成员,其大小由最大的成员决定。

  2. Union 成员共享同一块内存,一次只能使用其中一个成员。

  3. 对某个成员赋值,将覆盖其它成员的值(因为它们共享一块内存。前提是成员所占字节数相同,当成员所占字节数不同时只覆盖相应字节上的值,比如对 char 成员赋值不会将整个 int 成员覆盖掉,因为 char 只占一个字节,而 int 占四个字节)

  4. 所有成员都从低地址开始存放

结构体和联合必须从 Structure 和 Union 继承,子类必须定义 __fields__ 属性,__fields__ 属性必须是二元组列表,包含 Field 名称和 Field 类型,Field 类型必须是 ctypes 类型,比如 c_int, 或者其它继承自 ctypes 的类型,比如结构体、联合、数组、指针。

运行:


10. 数组

定义有 10 个 Point 元素的数组:

初始化和使用数组:


11. 指针

pointer() 可以创建指针,Pointer 实例的 contents 属性返回指针指向的内容。

可以改变指针指向的内容:

可以按数组的方式访问,并且改变值:


12. 传递指针或引用

很多情况下,C 函数需要传递指针或引用,ctypes 也完美支持这一点。

byref() 用来传递引用参数,pointer() 也可以完成同样的工作,但是 pointer() 将创建实际的指针对象,如果不需要指针对象,用 byref() 将快很多。


13. 可变字符串

如果需要可变字符串,那么使用 create_string_buffer()


14. 为 c_char_pc_wchar_pc_void_p 赋值

只改变它们指向的内存地址,而不改变内存的内容:


15. 数据都改变


16. 遇到的问题

16.1. 当动态库的导出函数返回 char * 时,如何释放内存

如果将 restype 设置为 c_char_p,ctypes 将返回常规的 Python 字符串对象。一种简单的方式是使用 void * 和强制转换。

string.c:

在 Python 中使用:

也可以使用 c_char_p 的子类,因为 ctypes 不对简单类型的子类调用 getfunc

访问 value 属性将返回字符串。在本示例中,可以将 freeme 的参数改为更通用的 c_void_p,它接受任何指针类型或整型地址。

16.2. 如何将包含 '\0' 的 char* 转换成 Python 字符串

原文地址:

How do you convert a char * with 0-value bytes into a python string?

翻译如下:

使用 POINTER(c_char) 获取指向二进制数据数组的指针。为将其放进字符串,只需获取其切片,因为对于 ctypes 指针,数组索引可按预期工作:


17. 参考资料