Python 调用 C 扩展:ctypes 与 CPython API

FreeGuideOnline 最新 2026-06-16

Python 调用 C 扩展:ctypes 与 CPython API

当性能成为瓶颈或需要复用现有 C/C++ 库时,Python 提供了多种与 C 语言交互的方式。本教程聚焦两种主流方案:ctypesCPython 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。必须显式设置 argtypesrestype 以防段错误。

# 假设 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)。编译时通常使用 setuptoolsdistutils

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_FromLongPyFloat_FromDoublePyUnicode_FromStringPy_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 来加速。