CPython C API 进阶指南:异常处理、引用计数与 PyArg_ParseTuple/Py_BuildValue 的源码级剖析
本文聚焦 CPython 官方文档 "Using the C API: Assorted topics"(Doc/extending/extending.rst),系统讲解编写复杂 C 扩展模块必须掌握的四大主题:错误与异常的传播约定、C 代码调用 Python 函数(回调机制)、PyArg_ParseTuple/PyArg_ParseTupleAndKeywords 参数解析、Py_BuildValue 值构建,以及引用计数所有权规则与 Capsule 跨模块 C API 导出。读完本文,你将能够按照 CPython 的引用计数与错误传播规范,独立编写出可接收关键字参数、可回调 Python 函数、可向其他扩展模块导出 C API 的完整扩展。
错误与异常:CPython 的错误传播约定
CPython 解释器有一条贯穿始终的核心约定:函数失败时必须设置异常状态并返回错误值(通常是 -1 或 NULL)。异常信息存储在线程状态的三个成员中(无异常时均为 NULL),分别对应 sys.exc_info() 返回的异常类型、异常实例和 traceback 对象。
设置异常的三个核心函数
Python API 提供了一组设置异常函数,声明均可在 Include/pyerrors.h 中找到:
PyErr_SetString(最常见):参数为异常对象和一个 C 字符串。异常对象通常是预定义对象,如PyExc_ZeroDivisionError;C 字符串描述错误原因,会被转换为 Python 字符串对象并存储为异常的 "associated value"。PyErr_SetFromErrno:只接收一个异常参数,通过检查全局变量errno自动构建关联值,适合包装系统调用失败(声明见 Include/pyerrors.h)。PyErr_SetObject:最通用的形式,接收两个对象参数——异常对象及其关联值。
传入这些函数的对象无需 Py_INCREF。
异常检查、传播与清除
PyErr_Occurred:非破坏性地检查是否已设置异常,返回当前异常对象或NULL。实际上通常不需要调用它——从返回值的错误指示(NULL/-1)就能判断。- 传播规则:当函数 f 调用的函数 g 失败时,f 应直接返回错误值,不应再调用任何
PyErr_*函数——g 已经调用过了。f 的调用者同样继续向上传递错误指示而不调用PyErr_*,因为最详细的错误原因已在最先检测到错误的函数中报告。一旦错误到达解释器主循环,当前正在执行的 Python 代码会中止,并寻找 Python 程序员指定的异常处理程序。当然,模块也可以调用另一个PyErr_*函数给出更详细的错误信息,但作为一般规则这没有必要,反而可能丢失错误原因信息。 PyErr_Clear:如果希望忽略某次失败调用设置的异常,必须显式调用它清除。C 代码只有在"不想把错误传给解释器、而想自己完全处理(重试或假装什么都没发生)"时,才应调用PyErr_Clear。PyErr_NoMemory:每个失败的malloc调用都必须转化为异常——malloc/realloc的直接调用者必须调用PyErr_NoMemory并返回失败指示(见 Include/pyerrors.h)。对象创建函数(如PyLong_FromLong)内部已做此处理,此约定只对直接调用malloc的人重要。- 返回值约定:除
PyArg_ParseTuple一族外,返回整数状态的函数通常遵循 Unix 系统调用风格——成功返回正值或零,失败返回-1。 - 清理垃圾:返回错误指示时,务必对自己已创建的对象调用
Py_XDECREF/Py_DECREF清理!
异常类型的选择完全由你决定:C 层为所有内建 Python 异常都预声明了 C 对象(如 PyExc_ZeroDivisionError)可直接使用。应明智选择——文件打不开应该用 PyExc_OSError 而不是 PyExc_TypeError;参数列表错误通常由 PyArg_ParseTuple 抛出 PyExc_TypeError;参数值不在合法范围内则适合 PyExc_ValueError。
定义模块专属异常
最简单的做法是在文件开头声明一个静态全局对象变量:
static PyObject *SpamError = NULL;
并在模块的 Py_mod_exec 函数中用 PyErr_NewException 初始化它。由于 SpamError 是全局变量,每次模块重新初始化(Py_mod_exec 再次被调用)时都会被覆盖。为避免这个问题,可抛出 ImportError 阻止重复初始化:
static PyObject *SpamError = NULL;
static int
spam_module_exec(PyObject *m)
{
if (SpamError != NULL) {
PyErr_SetString(PyExc_ImportError,
"cannot initialize spam module more than once");
return -1;
}
SpamError = PyErr_NewException("spam.error", NULL, NULL);
if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot spam_module_slots[] = {
{Py_mod_exec, spam_module_exec},
{0, NULL}
};
static struct PyModuleDef spam_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "spam",
.m_size = 0, // non-negative
.m_slots = spam_module_slots,
};
PyMODINIT_FUNC
PyInit_spam(void)
{
return PyModuleDef_Init(&spam_module);
}
注意 Python 中该异常对象的名称是 spam.error。PyErr_NewException(声明见 Include/pyerrors.h)创建的新类默认以 Exception 为基类(除非传入另一个类代替 NULL)。
引用持有是刻意为之:SpamError 变量保留了对新建异常类的引用。因为异常类可能被外部代码从模块中移除,必须持有一个 owned 引用才能保证它不被销毁,从而使 SpamError 悬空——悬空指针会导致抛出异常的 C 代码引发 core dump 或其他未定义行为。相应的 Py_DECREF 调用是缺失的:即使解释器关闭,全局 SpamError 也不会被垃圾回收,它会"泄漏"——但文档已确保每个进程最多发生一次。
在扩展模块中抛出该异常,只需像下面这样调用 PyErr_SetString:
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = system(command);
if (sts < 0) {
PyErr_SetString(SpamError, "System command failed");
return NULL;
}
return PyLong_FromLong(sts);
}
嵌入扩展模块到解释器
如果希望模块成为 Python 解释器的永久组成部分,需要修改构建配置并重新构建解释器。在 Unix 上,把你的源文件(例如 spammodule.c)放进解压后的源码分布的 Modules/ 目录,在 Modules/Setup.local 中加一行描述该文件:
spam spammodule.o
然后在顶层目录运行 make 重新构建解释器。也可以在 Modules/ 子目录中运行 make,但必须先用 make Makefile 重建那里的 Makefile(每次修改 Setup 文件后都需要)。当前仓库中对应的构建入口见 Modules/Setup。
如果模块需要额外的链接库,可以直接在该行上列出,例如:
spam spammodule.o -lX11
从 C 调用 Python 函数(回调机制)
Doc/extending/extending.rst 的前篇教程聚焦于让 C 函数可以被 Python 调用;反过来——从 C 调用 Python 函数——同样重要,尤其是支持 "callback" 回调函数的 C 库:C 接口使用回调时,对应的 Python 接口通常也要向 Python 程序员提供回调机制,实现上就需要从 C 回调中调用 Python 回调函数。
幸运的是,Python 解释器可以被递归调用,并且存在标准的函数调用接口。(若想了解如何用特定字符串作为输入调用 Python 解析器,可参考 Doc/c-api/veryhigh.rst。)
保存回调函数对象
调用 Python 函数很容易:首先 Python 程序必须把 Python 函数对象传给你(应提供一个函数或其他接口来做此事)。被调用时,把指向 Python 函数对象的指针保存到一个全局变量(注意要 Py_INCREF!):
static PyObject *my_callback = NULL;
static PyObject *
my_set_callback(PyObject *dummy, PyObject *args)
{
PyObject *result = NULL;
PyObject *temp;
if (PyArg_ParseTuple(args, "O:set_callback", &temp)) {
if (!PyCallable_Check(temp)) {
PyErr_SetString(PyExc_TypeError, "parameter must be callable");
return NULL;
}
Py_XINCREF(temp); /* Add a reference to new callback */
Py_XDECREF(my_callback); /* Dispose of previous callback */
my_callback = temp; /* Remember new callback */
/* Boilerplate to return "None" */
Py_INCREF(Py_None);
result = Py_None;
}
return result;
}
该函数必须使用 METH_VARARGS 标志注册到 PyMethodDef.ml_flags 中。宏 Py_XINCREF/Py_XDECREF 增加/减少对象引用计数,且在 NULL 指针存在时也是安全的(不过在此上下文中 temp 不会为 NULL)。
用 PyObject_CallObject 执行调用
真正调用函数时,使用 C 函数 PyObject_CallObject(实现在 Objects/call.c)。它有两个参数,都是指向任意 Python 对象的指针:Python 函数和参数列表。参数列表必须始终是一个 tuple 对象,其长度即参数个数。无参调用传 NULL 或空 tuple;单参数调用传单元素 tuple。Py_BuildValue 在格式串是若干格式代码加括号时返回 tuple,例如:
int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* Time to call the callback */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
PyObject_CallObject 返回 Python 对象指针,即该 Python 函数的返回值。它对其参数是"引用计数中性"的(reference-count-neutral):示例中新建了一个 tuple 作为参数列表,在调用后立即 Py_DECREF。
返回值是"新"的:要么是一个全新对象,要么是一个引用计数已递增的既有对象。因此,除非想把它存入全局变量,否则应该以某种方式 Py_DECREF 该结果——即使(尤其!)你不关心它的值。但在此之前,必须先检查返回值不是 NULL:若为 NULL,说明 Python 函数因抛出异常而终止。如果调用 PyObject_CallObject 的 C 代码本身是从 Python 调用的,此时应向 Python 调用者返回错误指示,让解释器打印 stack trace 或让调用方处理异常;如果不可能或不宜这样做,则应调用 PyErr_Clear 清除异常:
if (result == NULL)
return NULL; /* Pass error back */
...use result...
Py_DECREF(result);
根据对 Python 回调函数接口的不同设计,你可能还需要向 PyObject_CallObject 提供参数列表:有时参数列表也由 Python 程序通过指定回调函数的同一接口提供,可以像保存函数对象一样保存并使用;有时则必须新建 tuple,最简单的方式是调用 Py_BuildValue。例如传递一个整型事件码:
PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);
注意 Py_DECREF(arglist) 位于调用之后、错误检查之前!严格来说这段代码并不完整:Py_BuildValue 可能内存耗尽,这一点应当检查。
带关键字参数的调用可以使用 PyObject_Call(实现见 Objects/call.c),它同时支持位置参数和关键字参数。同样用 Py_BuildValue 构造字典:
PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);
扩展函数中的参数解析:PyArg_ParseTuple
教程中使用的是 METH_O 函数,只接受单个 Python 参数。若需要更多参数,改用 METH_VARARGS 标志:此时 C 函数收到的是一个包含全部参数的 tuple,而不是单个对象。
CPython 提供 PyArg_ParseTuple 来解包该 tuple,声明为:
int PyArg_ParseTuple(PyObject *arg, const char *format, ...);
- arg 必须是包含从 Python 传入 C 函数的参数列表的 tuple 对象;
- format 是格式串,其语法在 Python/C API Reference Manual 的 "C Function Argument Parsing Macros" 一节中说明(Doc/c-api/arg.rst);
- 其余参数必须是变量地址,其类型由格式串决定。
该函数实现在 Python/getargs.c。例如,接收单个 Python str 对象并转换为 C 缓冲区,格式串用 "s":
const char *command;
if (!PyArg_ParseTuple(args, "s", &command)) {
return NULL;
}
参数列表出错时 PyArg_ParseTuple 返回 0(即文档所称错误指示);你的函数可以直接返回 NULL,依赖 PyArg_ParseTuple 设置的异常。
两点警示:PyArg_ParseTuple 能检查 Python 参数类型,但无法检查传入的 C 变量地址的合法性——那里写错了,代码大概率崩溃,至少覆盖内存中随机的位,务必小心!另外,提供给调用方的 Python 对象引用都是借用引用(borrowed),不要递减其引用计数!
文档给出的示例调用(变量声明:int ok; int i, j; long k, l; const char *s; Py_ssize_t size;):
ok = PyArg_ParseTuple(args, ""); /* No arguments */
/* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
/* Possible Python call: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */
/* Possible Python call: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
/* A pair of ints and a string, whose size is also returned */
/* Possible Python call: f((1, 2), 'three') */
带默认值与可选参数(| 分隔)的写法:
{
const char *file;
const char *mode = "r";
int bufsize = 0;
ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
/* A string, and optionally another string and an integer */
/* Possible Python calls:
f('spam')
f('spam', 'w')
f('spam', 'wb', 100000) */
}
嵌套 tuple(矩形 + 点):
{
int left, top, right, bottom, h, v;
ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
&left, &top, &right, &bottom, &h, &v);
/* A rectangle and a point */
/* Possible Python call:
f(((0, 0), (400, 300)), (10, 10)) */
}
复数参数并附函数名用于错误信息(: 分隔):
{
Py_complex c;
ok = PyArg_ParseTuple(args, "D:myfunction", &c);
/* a complex, also providing a function name for errors */
/* Possible Python call: myfunction(1+2j) */
}
扩展函数的关键字参数:PyArg_ParseTupleAndKeywords
若希望函数接受关键字参数,把 METH_KEYWORDS 标志与 METH_VARARGS 组合使用(METH_KEYWORDS 也可与其他标志组合)。此时 C 函数应接受第三个 PyObject * 参数——关键字字典——并使用 PyArg_ParseTupleAndKeywords 解析。该函数声明如下(实现见 Python/getargs.c):
int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
const char *format, char * const *kwlist, ...);
arg 和 format 与 PyArg_ParseTuple 相同;kwdict 是从 Python 运行时收到的关键字字典;kwlist 是以 NULL 结尾的字符串列表,标识各参数名,名称与 format 中的类型信息从左到右匹配。成功返回真(非零),否则返回假(0)并抛出相应异常。
注意:使用关键字参数时无法解析嵌套 tuple!传入的、不在 kwlist 中的关键字参数会引发
TypeError。
文档给出的完整示例模块(基于 Geoff Philbrick 的例子):
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
int voltage;
const char *state = "a stiff";
const char *action = "voom";
const char *type = "Norwegian Blue";
static char *kwlist[] = {"voltage", "state", "action", "type", NULL};
if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
&voltage, &state, &action, &type))
return NULL;
printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",
action, voltage);
printf("-- Lovely plumage, the %s -- It's %s!\n", type, state);
Py_RETURN_NONE;
}
static PyMethodDef keywdarg_methods[] = {
/* The cast of the function is necessary since PyCFunction values
* only take two PyObject* parameters, and keywdarg_parrot() takes
* three.
*/
{"parrot", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
"Print a lovely skit to standard output."},
{NULL, NULL, 0, NULL} /* sentinel */
};
Py_BuildValue:构建任意 Python 值
Py_BuildValue 是 PyArg_ParseTuple 的逆操作,声明为:
PyObject *Py_BuildValue(const char *format, ...);
它识别与 PyArg_ParseTuple 类似的格式单元,但参数(是函数的输入而非输出)必须直接是值,不能是指针。它返回一个新的 Python 对象,适合从 C 函数返回给 Python。
与 PyArg_ParseTuple 的一个区别:后者要求首参数是 tuple(Python 参数列表在内部始终以 tuple 表示),而 Py_BuildValue 不总是构建 tuple——只有格式串含两个或以上格式单元时才构建 tuple;格式串为空时返回 None;恰好一个格式单元时返回该单元描述的对象本身。要强制返回长度为 0 或 1 的 tuple,给格式串加括号。
文档示例(左为调用,右为产生的 Python 值):
Py_BuildValue("") None
Py_BuildValue("i", 123) 123
Py_BuildValue("iii", 123, 456, 789) (123, 456, 789)
Py_BuildValue("s", "hello") 'hello'
Py_BuildValue("y", "hello") b'hello'
Py_BuildValue("ss", "hello", "world") ('hello', 'world')
Py_BuildValue("s#", "hello", 4) 'hell'
Py_BuildValue("y#", "hello", 4) b'hell'
Py_BuildValue("()") ()
Py_BuildValue("(i)", 123) (123,)
Py_BuildValue("(ii)", 123, 456) (123, 456)
Py_BuildValue("(i,i)", 123, 456) (123, 456)
Py_BuildValue("[i,i]", 123, 456) [123, 456]
Py_BuildValue("{s:i,s:i}",
"abc", 123, "def", 456) {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
1, 2, 3, 4, 5, 6) (((1, 2), (3, 4)), (5, 6))
引用计数(Reference Counts)
为什么需要引用计数
C/C++ 中程序员负责堆内存的动态分配与释放:C 用 malloc/free。每块 malloc 分配的内存最终必须被恰好一次 free 归还。在正确的时机调用 free 至关重要:地址被遗忘但未 free 就是内存泄漏(该内存直到程序终止都无法复用);对已 free 的块继续使用则与另一次 malloc 的复用冲突,称为使用已释放内存,后果与引用未初始化数据相同——core dump、错误结果、诡异崩溃。
内存泄漏的常见成因是代码的非常规路径。例如某函数分配内存、计算、再释放;后来需求变化加入一个错误检测分支提前返回,很容易忘记在此提前出口处释放——尤其当它是后加的代码。这类泄漏引入后往往长期不被察觉:错误退出只在少数调用中发生,现代机器虚拟内存充裕,泄漏只在长时间运行且频繁使用该函数的进程中才显形。因此要靠编码约定和策略来预防。
Python 大量使用 malloc/free,同样需要避免泄漏和使用已释放内存。所选策略即引用计数:每个对象内含一个计数器,向某处存储对对象的引用时递增,引用被删除时递减;计数归零即最后一个引用被删除,对象随即被释放。替代策略是自动垃圾回收,优点是无需显式 free;但对 C 而言没有真正可移植的自动垃圾收集器,而引用计数可以可移植地实现(只要 malloc/free 可用——C 标准保证了这一点)。
Python 使用传统引用计数实现,同时提供周期检测器(cycle detector)处理引用循环——这正是纯引用计数的弱点:循环中的对象互相(直接或间接)引用,各自计数非零,即使外部不再有引用,内存也无法回收。周期检测器能够发现并回收垃圾循环;gc 模块暴露了手动运行检测器(gc.collect)、配置接口以及运行时禁用检测器的能力。
Python 中的引用计数与所有权规则
两个宏处理引用计数增减:Py_INCREF(x) 与 Py_DECREF(x);Py_DECREF 在计数归零时释放对象。为保持灵活性,它不直接调用 free,而是通过对象**类型对象(type object)**中的函数指针间接调用——因此每个对象还包含一个指向其类型对象的指针。这些宏的定义见 Include/refcount.h。
谁在何时调用 Py_INCREF/Py_DECREF? 先引入术语:没有人"拥有"对象,但可以拥有对对象的引用(own a reference)。对象的引用计数即拥有的引用个数;引用的所有者有责任在不再需要时调用 Py_DECREF。所有权可转移,处理一个 owned 引用的方式有且仅有三种:传递、存储、或 Py_DECREF。忘记处理 owned 引用就会泄漏。
也可以**借用(borrow)**一个引用:借用者不应调用 Py_DECREF,且不得比所有者持有更久。所有者释放后继续使用借用引用属于使用已释放内存,应完全避免(检查引用计数是否 ≥ 1 也不可行——计数本身可能就在已释放内存中,已被复用于别的对象!)
借用的优点是无需在代码所有路径上操心释放,不会因提前出口而泄漏;缺点是在微妙的情况下,看似正确的代码可能在所有者实际已释放后仍使用借用引用。借用引用可通过 Py_INCREF 转为 owned 引用——不影响原所有者的状态,而是创建一个新的 owned 引用,新的所有者必须妥善处置。
文档"所有权规则"(Ownership Rules)一节的要点:
- 对象引用进出函数时,是否随引用转移所有权是函数接口规范的一部分。
- 大多数返回对象引用的函数都随引用转移所有权。特别是所有创建新对象的函数,如
PyLong_FromLong、Py_BuildValue,都把所有权交给接收方。即使对象并非真正新建,你拿到的也是新引用的所有权——例如PyLong_FromLong维护热点值缓存,可能返回缓存项的引用。 - 许多从其他对象中提取对象的函数也转移所有权,如
PyObject_GetAttrString。但有几个常见例外是借用引用:PyTuple_GetItem、PyList_GetItem、PyDict_GetItem、PyDict_GetItemString。PyImport_AddModule也返回借用引用(即使它可能实际创建了对象)——因为sys.modules中存有 owned 引用。 - 把对象引用传入另一个函数时,一般对方是从你这里借用——它需要存储时会自己
Py_INCREF。恰好有两个重要例外:PyTuple_SetItem和PyList_SetItem接管传入项的所有权——即使失败也接管!(PyDict_SetItem及其同类不接管,属于"正常"函数。) - 从 Python 调用的 C 函数,对其参数借用引用;只有需要存储或传递该借用引用时,才必须
Py_INCREF转为 owned。 - 从 Python 调用的 C 函数返回的对象引用必须是 owned 引用——所有权从函数转移给调用者。
薄冰之上(Thin Ice)
几种看似无害的借用引用用法会引发问题,根源都是隐式触发解释器导致所有者释放了引用。
案例一:在持有借用引用期间替换列表项。
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0); /* BUG! */
}
先借用 list[0],再把 list[1] 替换为 0,最后打印借用引用。看起来无害,但:PyList_SetItem 内部对被替换的项调用 Py_DECREF。假设原 list[1] 是定义了 __del__ 方法的用户类实例且引用计数为 1,释放它会同步触发其 tp_dealloc → tp_finalize → __del__ 方法(见 PEP 442)。由于 __del__ 用 Python 编写、可执行任意代码,它完全可以执行 del list[0]——若那是最后引用,对象内存被释放,item 随之悬空。
解决办法是临时增加引用计数:
void
no_bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
Py_INCREF(item);
PyList_SetItem(list, 1, PyLong_FromLong(0L));
PyObject_Print(item, stdout, 0);
Py_DECREF(item);
}
这是一个真实发生过的故事:Python 的旧版本中曾包含此类 bug 的变体,有开发者在 C 调试器里花了大量时间才明白他的 __del__ 方法为何失败。
案例二:线程版本的变体。 正常情况下 GIL 保护着 Python 整个对象空间,多个线程互不干扰;但可以用 Py_BEGIN_ALLOW_THREADS 临时释放锁、Py_END_ALLOW_THREADS 重新获取(宏定义见 Include/ceval.h),这在阻塞 I/O 调用周围很常见,以便等待期间让出处理器给其他线程。因此下面这个函数有同样的问题:
void
bug(PyObject *list)
{
PyObject *item = PyList_GetItem(list, 0);
Py_BEGIN_ALLOW_THREADS
...some blocking I/O call...
Py_END_ALLOW_THREADS
PyObject_Print(item, stdout, 0); /* BUG! */
}
NULL 指针约定
- 接收对象引用的函数一般不期望你传
NULL,传了就会 core dump(或导致之后的 core dump)。返回对象引用的函数一般只在异常发生时返回NULL。不逐个检测NULL参数的原因是函数常把接收的对象继续传给其他函数,逐一检测会引入大量冗余测试、拖慢速度。 - 更好的做法是在"源头"检测
NULL:从malloc或可能抛出异常的函数收到可能为NULL的指针时。 Py_INCREF/Py_DECREF不检查NULL;其变体Py_XINCREF/Py_XDECREF检查。- 类型检查宏(
Pytype_Check())也不检查NULL——经常连续调用多个来测试对象类型,检测会冗余;且不存在带NULL检查的变体。 - C 函数调用机制保证传给 C 函数的参数列表(示例中的
args)永不为NULL——事实上它保证args始终是一个 tuple(使用旧式调用约定时这些保证不成立,但旧式约定仍见于许多既有代码)。 - 让
NULL指针"逃逸"到 Python 用户层是严重错误。
用 C++ 编写扩展模块
扩展模块可以用 C++ 编写,但有限制:
- 若主程序(Python 解释器)由 C 编译器编译并链接,不能使用带构造函数的全局或静态对象;若主程序由 C++ 编译器链接则无此问题。
- 会被 Python 解释器调用的函数(尤其是模块初始化函数)必须声明为
extern "C"。 - 无需把 Python 头文件包在
extern "C" {...}里——当定义了__cplusplus符号时(所有现代 C++ 编译器都会定义),这些头文件已使用该形式。
为扩展模块提供 C API:Capsule 机制
许多扩展模块只是向 Python 提供新的函数与类型,但有时一个扩展模块的代码对其他扩展模块也有用。例如某模块实现了像无序列表那样的 "collection" 类型;就像标准 list 类型有允许扩展模块创建和操作列表的 C API 一样,这个新集合类型也应提供一组 C 函数供其他扩展模块直接调用。
乍看很简单:把函数写出来(不声明 static),提供头文件,写好文档即可——若所有扩展模块都与解释器静态链接这确实可行。但模块以共享库方式使用时,一个模块中定义的符号可能对另一个模块不可见,且可见性细节因操作系统而异(Windows 用一个全局命名空间,AIX 等需要在链接时显式列出导入符号,多数 Unix 提供多种策略可选);即使符号全局可见,目标模块也可能尚未加载!
因此可移植性要求不对符号可见性做任何假设:除模块初始化函数外,扩展模块中所有符号都应声明为 static,以避免与其他扩展模块命名冲突;而应被其他扩展模块访问的符号必须用别的方式导出。
CPython 提供的机制是 Capsule:一种存储 void * 指针的 Python 数据类型。Capsule 只能通过其 C API 创建和访问,但可像任何 Python 对象一样传递——尤其可以赋值到扩展模块命名空间中的某个名字。其他扩展模块导入该模块、取出该名字的值,再从 Capsule 中取出指针即可。
用 Capsule 导出 C API 有很多方式:每个函数一个 Capsule,或把所有 C API 指针存入一个数组、把数组地址发布在一个 Capsule 中;存取任务也可以在提供代码的模块与客户端模块之间以不同方式分配。无论选哪种方式,命名 Capsule 很重要:PyCapsule_New 的 name 参数可以传 NULL,但文档强烈建议指定名称——正确命名的 Capsule 提供一定程度的运行时类型安全,没有名称的 Capsule 之间无法区分。特别是用于暴露 C API 的 Capsule,应按如下约定命名:
modulename.attributename
便捷函数 PyCapsule_Import(实现在 Objects/capsule.c)可以方便地加载经 Capsule 提供的 C API,但仅当 Capsule 名称符合该约定时才生效,这让 C API 使用者能高度确信加载的 Capsule 包含正确的 C API。
完整示例:通过 Capsule 导出 PySpam_System
下面的示例把大部分负担放在导出方模块一侧(适合常用库模块):所有 C API 指针(示例中只有一个)存入一个 void 指针数组,该数组成为 Capsule 的值;头文件提供一个宏,负责导入模块并取回 C API 指针,客户端模块在访问 C API 前只需调用该宏。导出方模块是教程中 spam 模块的改造版:spam.system 不直接调用 C 库的 system,而是调用 PySpam_System(现实中它会做更复杂的事,比如给每条命令加上 "spam"),该函数同时被导出给其他扩展模块。
PySpam_System 是普通 C 函数,和一切其他函数一样声明为 static:
static int
PySpam_System(const char *command)
{
return system(command);
}
spam_system 做相应的平凡修改:
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = PySpam_System(command);
return PyLong_FromLong(sts);
}
在模块开头、#include <Python.h> 之后加两行:
#define SPAM_MODULE
#include "spammodule.h"
#define 用于告知头文件"当前被导出模块而非客户端模块包含"。最后,Py_mod_exec 函数负责初始化 C API 指针数组:
static int
spam_module_exec(PyObject *m)
{
static void *PySpam_API[PySpam_API_pointers];
PyObject *c_api_object;
/* Initialize the C API pointer array */
PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;
/* Create a Capsule containing the API pointer array's address */
c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);
if (PyModule_Add(m, "_C_API", c_api_object) < 0) {
return -1;
}
return 0;
}
注意 PySpam_API 声明为 static,否则指针数组会在 PyInit_spam 返回后消失!
大部分工作集中在头文件 spammodule.h 中:
#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif
/* Header file for spammodule */
/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)
/* Total number of C API pointers */
#define PySpam_API_pointers 1
#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */
static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;
#else
/* This section is used in modules that use spammodule's API */
static void **PySpam_API;
#define PySpam_System \
(*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])
/* Return -1 on error, 0 on success.
* PyCapsule_Import will set an exception if there's an error.
*/
static int
import_spam(void)
{
PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
return (PySpam_API != NULL) ? 0 : -1;
}
#endif
#ifdef __cplusplus
}
#endif
#endif /* !defined(Py_SPAMMODULE_H) */
客户端模块要访问 PySpam_System,只需在自己的 Py_mod_exec 函数中调用(严格说是宏)import_spam:
static int
client_module_exec(PyObject *m)
{
if (import_spam() < 0) {
return -1;
}
/* additional initialization can happen here */
return 0;
}
该方法的主要缺点是 spammodule.h 相当复杂;但每个导出函数的基本结构都相同,只需学习一次。
最后值得提到,Capsule 还提供其他功能,尤其适合 Capsule 中存储指针的内存分配与释放。细节见 Python/C API Reference Manual 的 "Capsules" 一节(Doc/c-api/capsule.rst)以及 Capsule 的实现文件 Include/pycapsule.h 和 Objects/capsule.c。
小结
CPython C API 的进阶核心可以浓缩为三条纪律:
- 错误传播不重复:错误由最先检测到的函数报告,上层只传递
NULL/-1,除非完全自行处理否则不要PyErr_Clear; - 引用所有权明确:每个 owned 引用必须以传递、存储或
Py_DECREF三选一方式终结;Py_*_GetItem系列返回借用引用,PyTuple_SetItem/PyList_SetItem接管所有权,隐式触发解释器的位置(Py_DECREF、释放 GIL、回调)前给借用引用补Py_INCREF; - 参数与返回值对称:
PyArg_ParseTuple/PyArg_ParseTupleAndKeywords解析入口参数,Py_BuildValue构建返回对象,二者格式串语法一致而方向相反。
结合 Capsule 的 modulename.attributename 命名约定,上述机制足以支撑编写生产级的、模块间可互调 C API 的复杂 Python 扩展。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00