ctypes 是 Python 标准库的外部函数接口:它提供与 C 兼容的类型,并让 Python 调用动态库导出的函数。正确调用需要同时匹配库文件、处理器位数、调用约定、参数类型、返回类型和内存使用规则。本文以 Python 3.11 为基线。
先区分库格式与调用约定
跳转到“先区分库格式与调用约定”ctypes.CDLL 用于 C 调用约定;Windows 的 ctypes.WinDLL 用于 Windows stdcall 接口。不能简单写成“Windows 一律使用 WinDLL”:同一台 Windows 机器上的 C 库仍可能需要 CDLL。应依据头文件和接口文档选择,尤其不要在 32 位接口中混用约定。Windows 64 位采用统一的主要调用约定,也不意味着参数和返回值声明可以省略。
DLL 或 SO 只是动态库文件;C++ 导出的名字和接口布局未必能直接按普通 C 函数调用。常见做法是提供 extern "C" 的 C 接口包装,并使用简单、明确的 ABI 类型。Python 进程和库的架构也必须兼容,例如 64 位 Python 不能直接载入 32 位 DLL。
提供实际可构建的 C 库
跳转到“提供实际可构建的 C 库”原记录调用了 example.dll 中的 function_name,但没有提供这个库。下面补齐一个演示库,保留两整数相加的入口,另加数组、结构体、字符串和较大返回值的例子。
保存为 example.c。function_name 的契约要求两个参数的和能用 C int 表示,Python 侧将在调用前检查这个条件;数组接口还要求调用者提供至少 count 个有效元素。
#include <stddef.h>#include <stdint.h>
#if defined(_WIN32)#define API __declspec(dllexport)#else#define API#endif
#ifdef __cplusplusextern "C" {#endif
typedef struct Point { double x; double y;} Point;
API int function_name(int a, int b){ return a + b;}
API int scale_values(double *values, size_t count, double factor){ if (values == NULL && count != 0) { return 1; } for (size_t i = 0; i < count; ++i) { values[i] *= factor; } return 0;}
API int move_point(Point *point, double dx, double dy){ if (point == NULL) { return 1; } point->x += dx; point->y += dy; return 0;}
API const char *greeting(void){ return "hello from C";}
API uint64_t big_value(void){ return UINT64_C(1099511627776);}
#ifdef __cplusplus}#endif已安装匹配架构的 GCC 时,Windows MinGW 可在该文件所在目录执行:
gcc -std=c11 -Wall -Wextra -Werror -pedantic-errors -shared -o example.dll example.cLinux 的对应 GCC 构建命令为:
gcc -std=c11 -Wall -Wextra -Werror -pedantic-errors -fPIC -shared -o libexample.so example.c本文动态库调用已在 Windows 64 位、GCC 13.1 与 CPython 3.11.5 下执行验证;Linux 命令展示该平台的构建方式,不能据此声称已验证每一种系统或编译器。
声明原型,再调用函数
跳转到“声明原型,再调用函数”将下面保存为与库同目录的 call_example.py,然后执行 python call_example.py。通过脚本所在目录定位库,避免当前工作目录变化时载入另一个同名文件。加载失败时还要检查库的依赖文件,而不只是检查主 DLL 是否存在。
from pathlib import Pathimport ctypesimport mathimport os
class Point(ctypes.Structure): _fields_ = [("x", ctypes.c_double), ("y", ctypes.c_double)]
library_name = "example.dll" if os.name == "nt" else "libexample.so"library_path = Path(__file__).resolve().with_name(library_name)lib = ctypes.CDLL(str(library_path))
function = getattr(lib, "function_name")function.argtypes = [ctypes.c_int, ctypes.c_int]function.restype = ctypes.c_int
def add_checked(a, b): bits = ctypes.sizeof(ctypes.c_int) * 8 lower, upper = -(1 << (bits - 1)), (1 << (bits - 1)) - 1 if type(a) is not int or type(b) is not int: raise TypeError("arguments must be Python integers") if not (lower <= a <= upper and lower <= b <= upper and lower <= a + b <= upper): raise OverflowError("arguments and sum must fit C int") return function(a, b)
assert function(1, 2) == 3assert add_checked(10, 20) == 30print("函数返回的结果是:", add_checked(10, 20))
lib.scale_values.argtypes = [ctypes.POINTER(ctypes.c_double), ctypes.c_size_t, ctypes.c_double]lib.scale_values.restype = ctypes.c_intvalues = (ctypes.c_double * 3)(1.0, 2.0, 3.0)assert lib.scale_values(values, len(values), 2.5) == 0assert list(values) == [2.5, 5.0, 7.5]assert lib.scale_values(None, 1, 2.0) == 1
lib.move_point.argtypes = [ctypes.POINTER(Point), ctypes.c_double, ctypes.c_double]lib.move_point.restype = ctypes.c_intpoint = Point(1.0, 2.0)assert lib.move_point(ctypes.byref(point), 0.5, -1.0) == 0assert math.isclose(point.x, 1.5) and math.isclose(point.y, 1.0)assert lib.move_point(None, 1.0, 1.0) == 1
lib.greeting.argtypes = []lib.greeting.restype = ctypes.c_char_pmessage = lib.greeting()assert message == b"hello from C"assert message.decode("utf-8") == "hello from C"
lib.big_value.argtypes = []lib.big_value.restype = ctypes.c_uint64assert lib.big_value() == 2**40print("array, structure, string and return-type checks passed")argtypes 声明参数转换规则,restype 声明返回值规则;未设置返回类型时默认按 C int 解释,不能用它接收任意指针或 64 位整数。返回 void 的函数应使用 restype = None。类型声明也不会自动验证某个整数范围、数组长度或指针背后的可用内存。
这里数组和结构体由 Python 持有,C 函数只在调用期间使用它们。greeting 返回静态只读字符串,c_char_p 将其读取为 bytes,没有需要调用者释放的分配。若实际库返回需释放的内存,就必须保留正确的地址并调用同一个库规定的释放函数,不能照搬这个字符串例子丢失所有权信息。
基础类型、字符串指针和可写缓冲区
跳转到“基础类型、字符串指针和可写缓冲区”| ctypes 类型或工厂 | 对应概念 | 常见边界 |
|---|---|---|
c_int、c_float、c_double | C int、float、double | 位宽和精度依对应 C 类型,不等于 Python 任意精度整数 |
c_size_t、c_uint64 | size_t、64 位无符号整数 | 按头文件声明,不能凭数值大小随意替换 |
c_char_p | 以 NUL 结尾的 char * 字符串指针 | 读取为 bytes;不是可以随意让 C 改写的 Python 字符串 |
create_string_buffer | 有容量的可写字符数组 | 容量要包含需要的终止符;数据长度和分配容量不同 |
Structure、类型乘整数形成的数组 | 结构体、定长数组 | 布局、对齐、成员顺序必须与 C 端一致 |
POINTER(T)、byref(value) | 指针类型、传入对象地址 | C 若保存地址,Python 对象必须保持存活 |
import ctypesimport math
integer = ctypes.c_int(10)integer.value = 20assert integer.value == 20approximation = ctypes.c_float(0.1).valueassert math.isclose(approximation, 0.1, rel_tol=1e-6)
text = ctypes.c_char_p(b"hello")assert text.value == b"hello"text.value = b"world"assert text.value == b"world"
buffer = ctypes.create_string_buffer(b"hello", 8)assert ctypes.sizeof(buffer) == 8assert buffer.value == b"hello"buffer.value = b"hi"assert buffer.value == b"hi"assert buffer.raw == b"hi\x00lo\x00\x00\x00"buffer[0] = b"H"assert buffer.value == b"Hi"print("ctypes value and buffer checks passed")修改 c_char_p.value 会重新指向数据,不代表改写了先前那块内存。buffer.value 按 NUL 结尾读取;buffer.raw 才展示整个缓冲区,旧内容不一定在较短字符串赋值后全部清零。结构体不能随意增加 _pack_ 来“解决”布局问题,应以真实 ABI 的对齐规则为准。
错误要按库的约定解释
跳转到“错误要按库的约定解释”原记录中的“调用 C 函数出错都会抛出 OSError”不成立。几类失败应分别判断:
- 找不到库、依赖缺失或架构不兼容,加载通常抛出
OSError。 - 找不到导出名称,属性访问通常抛出
AttributeError;getattr不能创造缺失的符号。 - 参数转换失败可能抛出
ctypes.ArgumentError,但错误地址或错误原型仍可能导致进程崩溃。 - 原生函数自己的失败,需要按返回状态、
errno、Windows LastError 或文档指定的机制处理。
上例的两个可变数据接口返回 0 表示成功、1 表示不允许的空指针;返回 1 本身不会变成 Python 异常。若要统一处理,可以为函数设置 errcheck,或在 Python 包装函数中检查返回值。需要 errno 的接口可按文档启用 use_errno=True 并读取 ctypes.get_errno();Windows LastError 对应 use_last_error=True,两者不能互换,也不能假定所有 C 库都按这两种机制报告错误。
原型、回调存活期和库加载的完整规则见 Python ctypes 文档。如果需求只是把字节按格式打包,不需要执行外部函数,可以使用标准库 struct。