Python 调用 C 扩展:ctypes 与 CPython API
Python 调用 C 扩展:ctypes 与 CPython API
当性能成为瓶颈或需要复用现有 C/C++ 库时,Python 提供了多种与 C 语言交互的方式。本教程聚焦两种主流方案:ctypes 与 CPython API。前者无需编写 C 代码即可调用动态链接库中的函数;后者通过编写 Python C 扩展模块,将 C 函数封装为 Python 可调用的对象。通过本教程,你将学会根据场景权衡选择,并掌握二者的核心用法。
1. 快速认识两种方式
| 特性 | ctypes | CPython API |
|---|---|---|
| 编写 C 代码 | 不需要,直接调用已有 .so/.dll | 需要编写 C 扩展代码 |
| 性能 | 调用开销大,适合少量调用 | 接近原生 C 性能 |
| 复杂度 | 低,纯 Python 操作 | 高,需处理引用计数、类型转换 |
| 适用场景 | 调用系统 API、现有 C 库 | 高性能计算、深度嵌入 Python 对象模型 |
2. ctypes:零 C 代码调用动态库
ctypes 是 Python 标准库的一部分,让你能够以纯 Python 的方式加载 C 共享库、调用函数、传递参数,无需编译任何 C 代码。
2.1 加载共享库
import ctypes
# Linux/macOS 加载 .so 文件
lib = ctypes.CDLL("./mylib.so")
# Windows 加载 .dll 文件
# lib = ctypes.CDLL("./mylib.dll")
如果库不在标准路径,可以使用 ctypes.util.find_library 或直接提供绝对路径。
2.2 指定参数类型与返回类型
默认情况下,ctypes 假定函数返回 int,参数均为 int。必须显式设置 argtypes 和 restype 以防段错误。
# 假设 C 函数:double add(double a, double b);
lib.add.argtypes = [ctypes.c_double, ctypes.c_double]
lib.add.restype = ctypes.c_double
result = lib.add(3.5, 2.5) # 6.0
常用类型映射:
| ctypes 类型 | C 类型 | Python 类型 |
|---|---|---|
| c_int | int | int |
| c_double | double | float |
| c_char_p | char* | bytes/str |
| c_void_p | void* | int/None |
2.3 传递指针与字符串
对于需要修改参数的 C 函数(如输出参数),可使用 byref() 传递指针。
// C 函数:void divide(int a, int b, int *quotient, int *remainder);
Python 调用:
quotient = ctypes.c_int()
remainder = ctypes.c_int()
lib.divide(10, 3, ctypes.byref(quotient), ctypes.byref(remainder))
print(quotient.value, remainder.value) # 3 1
传递字符串时,若 C 函数不修改字符串,可直接传 bytes;若需要可修改缓冲区,使用 create_string_buffer。
buf = ctypes.create_string_buffer(b"hello")
lib.modify_string(buf)
print(buf.value) # b'...'
2.4 处理结构体
ctypes 支持定义与 C 结构体布局一致的结构体。
typedef struct {
int x;
int y;
} Point;
double distance(Point p1, Point p2);
Python 映射:
class Point(ctypes.Structure):
_fields_ = [("x", ctypes.c_int),
("y", ctypes.c_int)]
lib.distance.argtypes = [Point, Point]
lib.distance.restype = ctypes.c_double
p1 = Point(0, 0)
p2 = Point(3, 4)
print(lib.distance(p1, p2)) # 5.0
2.5 回调函数
可以将 Python 函数作为回调传递给 C 函数。
typedef void (*callback_t)(int value);
void register_callback(callback_t cb);
Python 实现:
CALLBACK_TYPE = ctypes.CFUNCTYPE(None, ctypes.c_int) # 返回 void,接收 int
def my_callback(value):
print(f"Callback received: {value}")
lib.register_callback(CALLBACK_TYPE(my_callback))
注意:必须保持对回调对象的引用,否则会被垃圾回收导致段错误。
2.6 ctypes 的优势与局限
优势:
- 无需编译器,纯 Python 即可实现。
- 快速原型开发,适合测试现有 C 库。
局限:
- 调用开销较大,不宜在热点循环中频繁使用。
- 无法直接访问 Python C API,不能创建 Python 类型或引发自定义异常。
- 线程安全和 GIL 处理需手动管理。
3. CPython API:编写原生 C 扩展
当需要极致性能或深度集成 Python 对象时,直接使用 Python 提供的 C API 编写扩展模块是理想选择。你可以定义新类型、处理异常、管理 GIL。
3.1 基础开发环境
需要安装 Python 开发头文件(如 python3-dev)。编译时通常使用 setuptools 或 distutils。
3.2 编写第一个扩展模块
创建一个计算斐波那契数列的 C 函数并封装。
// fibmodule.c
#define PY_SSIZE_T_CLEAN
#include <Python.h>
// 纯 C 函数
long long fib(int n) {
if (n < 2) return n;
long long a = 0, b = 1, temp;
for (int i = 2; i <= n; i++) {
temp = a + b;
a = b;
b = temp;
}
return b;
}
// 包装函数,使 Python 可以调用
static PyObject* fib_fib(PyObject* self, PyObject* args) {
int n;
if (!PyArg_ParseTuple(args, "i", &n)) // "i" 表示 int
return NULL; // 异常已设置
long long result = fib(n);
return PyLong_FromLongLong(result);
}
// 方法定义表
static PyMethodDef FibMethods[] = {
{"fib", fib_fib, METH_VARARGS, "Compute the nth Fibonacci number."},
{NULL, NULL, 0, NULL} // 哨兵
};
// 模块定义
static struct PyModuleDef fibmodule = {
PyModuleDef_HEAD_INIT,
"fib", // 模块名
NULL, // 模块文档
-1, // 每个解释器状态大小,-1 表示模块不支持子解释器
FibMethods
};
// 模块初始化函数
PyMODINIT_FUNC PyInit_fib(void) {
return PyModule_Create(&fibmodule);
}
3.3 编译与安装
使用 setuptools 构建:
# setup.py
from setuptools import setup, Extension
module = Extension('fib', sources=['fibmodule.c'])
setup(
name='fib',
version='1.0',
ext_modules=[module]
)
执行:
python setup.py build_ext --inplace
即可在本地生成 fib.cpython-3x-x86_64-linux-gnu.so,直接 import fib 使用。
import fib
print(fib.fib(10)) # 55
3.4 参数解析与返回值构建
PyArg_ParseTuple 使用格式字符串解析参数,常用格式:
| 格式 | C 类型 | 说明 |
|---|---|---|
| "i" | int | 整数 |
| "s" | const char* | UTF-8 字符串(不可修改) |
| "s#" | const char*, Py_ssize_t* | 字符串及长度 |
| "O" | PyObject* | 任意 Python 对象 |
返回值构建常用函数:PyLong_FromLong、PyFloat_FromDouble、PyUnicode_FromString、Py_BuildValue 等。
例:解析两个整数返回其和:
static PyObject* add(PyObject* self, PyObject* args) {
int a, b;
if (!PyArg_ParseTuple(args, "ii", &a, &b))
return NULL;
return PyLong_FromLong(a + b);
}
3.5 异常处理
C API 通过返回 NULL 表示出错,并设置异常状态。可使用 PyErr_SetString 等函数。
if (n < 0) {
PyErr_SetString(PyExc_ValueError, "n must be non-negative");
return NULL;
}
3.6 引用计数与内存管理
CPython 使用引用计数,必须遵循 “拥有” 与 “借用” 规则。
- Py_INCREF(obj) 增加引用;
- Py_DECREF(obj) 减少引用;
- 返回给 Python 调用者的新对象通常已经拥有所有引用权,不需额外操作。
- 务必注意函数中的临时对象,避免内存泄漏。
简记:如果你手动创建了 Python 对象且不再使用,调用 Py_DECREF;如果从其他函数获取“借用”引用且需长期持有,使用 Py_INCREF。
3.7 定义新类型(可选)
CPython API 允许定义纯 C 实现的 Python 类型,通过 PyTypeObject 结构体。
简化示例框架:
typedef struct {
PyObject_HEAD
int value;
} MyObject;
static void MyObject_dealloc(MyObject* self) {
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyTypeObject MyType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "mymod.MyObject",
.tp_basicsize = sizeof(MyObject),
.tp_dealloc = (destructor)MyObject_dealloc,
.tp_flags = Py_TPFLAGS_DEFAULT,
// 其他方法...
};
这部分更深入,适合对性能要求极高的场景。
3.8 CPython API 的优势与局限
优势:
- 完整的 Python 对象模型控制,性能可达到 C 级别。
- 可以引发异常、定义新类型、操作 GIL。
局限:
- 学习曲线陡峭,必须处理引用计数和复杂的内存管理。
- 不同 Python 版本间 API 变化(尽管核心稳定)。
- 编译和分发依赖 C 工具链和开发头文件。
4. 如何选择:ctypes 还是 CPython API?
- 优先使用 ctypes:当你想快速调用已有的 C 库(如系统 API、第三方 DLL),且调用频率不高,或原型开发阶段。
- 转向 CPython API:当性能要求极高,需要将核心算法封装为 Python 模块,或需要创建自定义 Python 类型、精细控制时。
- 混合方案:可以在 CPython 扩展中内部使用 ctypes 调用其他库,但更推荐直接在 C 层链接库。
5. 总结
ctypes 与 CPython API 构成了 Python 调用 C 代码的两条互补路径。前者让调用动态库如同 Python 原生操作一样简单;后者赋予开发者全部 C 语言能力,用于构建高性能扩展。掌握这两种技术,你可以在开发效率与运行效率之间自如切换,充分发挥 Python 的胶水语言特性。
若你刚开始接触,建议从 ctypes 入手;一旦需要更高控制力,再深入 CPython API。两种方式均已有大量成熟项目验证,你的下一个性能关键模块,不妨试试用 C 来加速。