CPython C 扩展开发指南:为自定义类型实现循环垃圾回收支持(Py_TPFLAGS_HAVE_GC、tp_traverse 与 PyObject_GC_Track)
当你编写 CPython 的 C 扩展或嵌入式应用时,只要你的自定义类型会持有其他对象的引用,就可能与 Python 对象之间形成"引用环"(reference cycle)。这类环无法靠引用计数回收,必须交给 CPython 的分代循环垃圾回收器处理。本篇基于 CPython 仓库中的官方 C API 文档 Doc/c-api/gcsupport.rst,系统讲解如何让你的 C 类型正确接入循环垃圾回收:从类型标志位 Py_TPFLAGS_HAVE_GC、tp_traverse/tp_clear 槽位的实现,到 PyObject_GC_New、PyObject_GC_Track 等 API 的配对使用规则,再到遍历过程中的"无副作用"约束与可用的安全函数清单,并结合 Python/gc.c 等源码印证底层实现。
一、哪些类型需要支持循环垃圾回收
Python 对"包含其他对象(容器对象)的类型"提供循环引用检测。判断标准很明确:
- 不需要支持:不存储对其他对象引用的类型,或者只引用原子类型(数字、字符串等无法参与引用环的类型)的类型,无需为垃圾回收做任何事情;
- 必须支持:存储了可能指向其他容器的引用的"容器类型"(container type)。
从源码结构看,判断一个类型是否属于 GC 类型的依据是 Py_TPFLAGS_HAVE_GC 标志。该标志在 Include/object.h 中定义:
#define Py_TPFLAGS_HAVE_GC (1UL << 14)
而 Include/objimpl.h 提供了类型级别的判断宏 PyType_IS_GC(t),其实现就是检查该标志位。对象级别的判断则用 PyObject_IS_GC(),其在 Python/gc.c 中的实现直接转发给内部函数 _PyObject_IS_GC。
二、创建容器类型的三条硬性要求
要让一个 C 类型成为容器类型,PyTypeObject 必须满足以下要求(来自 Doc/c-api/gcsupport.rst):
tp_flags字段必须包含Py_TPFLAGS_HAVE_GC标志。设置了该标志的对象必须符合本文档的全部规则,这类对象下文称"容器对象";- 必须提供
tp_traverse处理器,用于让回收器遍历对象内部持有的引用; - 如果类型的实例是可变的,还必须提供
tp_clear实现(用于打破已确认存在的引用环);不可变对象不可能直接参与引用环,tp_clear可以为NULL。
警告(原文档强调):一旦类型设置了
Py_TPFLAGS_HAVE_GC,就必须实现至少一个tp_traverse处理器,或者显式复用其父类/子类的tp_traverse。补充规则:调用
PyType_Ready(或间接调用它的PyType_FromSpecWithBases、PyType_FromSpec)时,如果类型继承自一个实现了垃圾回收协议(带Py_TPFLAGS_HAVE_GC)的类,而子类型自身没有设置该标志,解释器会自动为子类型填充tp_flags、tp_traverse和tp_clear字段,从父类继承对应实现。
tp_clear 的类型是 inquiry(即 int (*inquiry)(PyObject *self)):
int (*inquiry)(PyObject *self)
其职责是丢弃可能构成引用环的引用。注意:调用该方法后对象仍然必须是有效的(不能只是简单 Py_DECREF 一个引用而留下悬空状态)。回收器在检测到对象参与了引用环时会调用它。
三、构造函数与析构函数的四条配对规则
容器对象在构造和销毁时必须遵守对称的规则,否则回收器的链表会损坏:
构造函数规则:
- 对象内存必须用
PyObject_GC_New或PyObject_GC_NewVar分配; - 当所有可能包含"对其他容器引用"的字段都初始化完毕后,必须调用
PyObject_GC_Track把对象加入回收器的追踪集合。由于回收器可能在不期望的时机运行,对象在被追踪期间必须始终处于有效状态——因此PyObject_GC_Track通常在构造函数末尾调用。
析构函数(tp_dealloc)规则:
- 在使
tp_traverse所访问的字段失效之前,必须先调用PyObject_GC_UnTrack; - 对象内存必须用
PyObject_GC_Del释放。
补充版本沿革:
_PyObject_GC_TRACK/_PyObject_GC_UNTRACK这两个宏自 Python 3.8 起已从公共 C API 中移除,现在应统一使用PyObject_GC_Track/PyObject_GC_UnTrack函数。
3.1 分配宏:PyObject_GC_New / PyObject_GC_NewVar
PyObject_GC_New(TYPE, typeobj)
PyObject_GC_NewVar(TYPE, typeobj, size)
两者分别类似于 PyObject_New / PyObject_NewVar,但专用于设置了 Py_TPFLAGS_HAVE_GC 的容器对象。文档同时给出两条实操建议:
- 不要直接调用它们来分配对象内存,而应调用类型的
tp_alloc槽位;在填充tp_alloc时,优先使用PyType_GenericAlloc,而不是写一个只调用这些宏的自定义函数; - 用这些宏分配的内存必须用
PyObject_GC_Del释放(通常经由对象的tp_free槽位);不要用它来释放PyObject_New/PyObject_NewVar等非 GC 分配函数分配的内存(那些要用PyObject_Free)。
这些宏在 Include/objimpl.h 中的定义是:
#define PyObject_GC_New(type, typeobj) \
_Py_CAST(type*, _PyObject_GC_New(typeobj))
#define PyObject_GC_NewVar(type, typeobj, n) \
_Py_CAST(type*, _PyObject_GC_NewVar((typeobj), (n)))
3.2 其他分配/调整辅助 API
-
PyObject* PyUnstable_Object_GC_NewWithExtraData(PyTypeObject *type, size_t extra_size)(3.12 新增):类似PyObject_GC_New,但在对象末尾(偏移tp_basicsize处)额外分配extra_size字节。除对象头(PyObject)外的内存初始化为零。额外数据随对象一起释放,但 Python 不管理其内容。注意它被标记为 unstable,因为"在实例后预留额外数据"的最终机制尚未定案;如果需要分配可变数量的字段,应优先使用PyVarObject+tp_itemsize。 -
PyObject_GC_Resize(TYPE, op, newsize):调整由PyObject_NewVar(实际要求为PyObject_GC_NewVar系)分配的变长对象大小,返回调整后的TYPE*或失败时的NULL。要求op是PyVarObject *,且尚未被回收器追踪;newsize为Py_ssize_t。
3.3 追踪/解追踪与释放
void PyObject_GC_Track(PyObject *op); // 加入追踪集合
void PyObject_GC_UnTrack(void *op); // 移出追踪集合
void PyObject_GC_Del(void *op); // 释放 GC 对象内存
PyObject_GC_UnTrack 之后,可以再次对该对象调用 PyObject_GC_Track 重新加入追踪。tp_dealloc 必须在任何 tp_traverse 会用到的字段失效前调用 UnTrack。
3.4 状态查询
int PyObject_IS_GC(PyObject *obj); // 类型是否实现 GC 协议;返回 0 则该对象不可能被追踪
int PyObject_GC_IsTracked(PyObject *op); // 3.9 新增,对应 Python 层 gc.is_tracked
int PyObject_GC_IsFinalized(PyObject *op); // 3.9 新增,对应 Python 层 gc.is_finalized
PyObject_GC_IsTracked 仅在"对象类型实现了 GC 协议且该对象当前正被追踪"时返回 1;PyObject_GC_IsFinalized 则指示对象是否已被回收器 finalizer 处理过。
四、tp_traverse:让回收器看到你持有的引用
tp_traverse 处理器必须具有如下函数指针类型:
int (*traverseproc)(PyObject *self, visitproc visit, void *arg)
实现要求:
- 对
self直接包含的每个对象调用visit回调,参数依次为被包含对象和传给tp_traverse的arg; - 不得以
NULL作为对象参数调用visit(用Py_VISIT宏可以自动处理 NULL 检查); - 如果
visit返回非零值,必须立即将该值返回。
visit 回调的类型为:
int (*visitproc)(PyObject *object, void *arg)
Python 核心用多个访问者函数(visitor)实现循环检测,通常不需要扩展作者自己编写 visitor 函数,只需在 tp_traverse 中调用 visit 即可。
4.1 Py_VISIT 便捷宏
为统一各扩展的写法,CPython 提供 Py_VISIT 宏。前提:tp_traverse 实现的参数必须恰好命名为 visit 和 arg,不能随意命名。宏在 Include/objimpl.h 中的实际定义为:
#define Py_VISIT(op) \
do { \
if (op) { \
int vret = visit(_PyObject_CAST(op), arg); \
if (vret) \
return vret; \
} \
} while (0)
即:op 非 NULL 时调用 visit(op, arg);若 visit 返回非零则原样返回。
典型的 tp_traverse 会对实例"拥有的"每个 Python 对象成员调用 Py_VISIT。文档给出的经典示例是(略旧的)threading.local 类的遍历函数:
static int
local_traverse(PyObject *op, visitproc visit, void *arg)
{
localobject *self = (localobject *) op;
Py_VISIT(Py_TYPE(self));
Py_VISIT(self->args);
Py_VISIT(self->kw);
Py_VISIT(self->dict);
return 0;
}
当前 CPython 中 threading.local 的实现依然遵循这个模式,可对照 Modules/_threadmodule.c 中的 local_traverse(以及配套的 local_clear、local_dealloc,分别位于 L1574、L1585)。
4.2 堆分配类型必须访问 Py_TYPE(self)
堆分配类型(heap-allocated types)的实例持有对自身类型对象的引用,因此其遍历函数必须访问该类型:
Py_VISIT(Py_TYPE(self));
替代做法是让类型的 tp_traverse 转调某个堆分配父类(或其他适用的堆分配类型)的 tp_traverse 来委托这一职责。如果两者都不做,类型对象本身将无法被垃圾回收。自 Python 3.9 起,堆分配类型被明确要求在 tp_traverse 中访问 Py_TYPE(self)(更早版本中这样做可能因历史 bug 导致子类崩溃,3.9 之后是安全且必须的)。
4.3 Py_TPFLAGS_MANAGED_DICT:托管实例字典
如果 tp_flags 中设置了 Py_TPFLAGS_MANAGED_DICT 位,遍历函数必须显式调用 PyObject_VisitManagedDict 来访问托管的 __dict__:
int err = PyObject_VisitManagedDict((PyObject*)self, visit, arg);
if (err) {
return err;
}
4.4 只遍历"拥有的"成员
只有实例拥有强引用(strong reference)的成员才需要访问。例如:如果对象通过 tp_weaklist 槽支持弱引用,那么支撑弱引用链表的指针(tp_weaklist 指向的东西)不能被访问,因为实例并不直接拥有这些指向自身的弱引用。
两个可操作的优化提示:
- 对于可证明不可能参与引用环的成员,
Py_VISIT可以跳过。例如threading.local还有一个self->key成员,它只能是NULL或一个字符串,因此不可能处于引用环中; - 反过来,即使你知道某成员永远不可能成环,也可以为了调试便利而访问它,这样
gc模块的gc.get_referents(见 gc 模块文档)就能列出它。
4.5 硬性约束:tp_traverse 不得有任何副作用
原文档以 warning 级别强调:遍历函数不得有任何副作用。实现中不允许修改任何 Python 对象的引用计数,也不允许直接或间接创建/销毁任何 Python 对象。这意味着:
- 绝大多数 C API 函数都不能在
tp_traverse中使用,因为它们可能抛出异常、返回结果对象的新引用、或有带副作用的内部逻辑; - 即便某些函数碰巧当前没有副作用,除非文档另有说明,未来版本也可能不加警告地引入副作用。
因此官方专门维护了一份"traversal-safe"白名单(见下一节),这是编写 tp_traverse 时必须遵守的边界。
另外注意线程语义:tp_traverse 可以从任意线程被调用。从实现细节看,垃圾回收是"stop-the-world"操作——即使在 free-threading(无 GIL)构建中,执行 tp_traverse 处理器时也只有一个线程状态处于 attached 状态。
五、traversal-safe 函数与 "DuringGC" 函数
5.1 允许在 tp_traverse 中使用的函数和宏
以下函数与宏可以安全地在 tp_traverse 处理器中使用:
- 传给
tp_traverse的visit函数本身; Py_VISIT宏;Py_SIZE;Py_TYPE:从tp_traverse中调用时,其结果在整个处理器调用期间有效;PyObject_VisitManagedDict;PyObject_TypeCheck、PyType_IsSubtype、PyType_HasFeature;- 各
Py{<type>}_Check与Py{<type>}_CheckExact(例如PyTuple_Check)。
5.2 只能在 tp_traverse 中使用的 "DuringGC" 函数
以下函数只能在 tp_traverse 处理器中使用,在其他上下文中调用可能产生未预期的后果。它们的行为与不带 _DuringGC 后缀的对应函数类似,但保证没有副作用、失败时不设置异常,并返回/设置借用引用(borrowed reference),具体见各自文档。注意这些函数可能失败(返回 NULL 或 -1),但由于不设置异常,拿不到任何错误信息;某些情况下失败与"成功的 NULL 结果"无法区分:
void *PyObject_GetTypeData_DuringGC(PyObject *o, PyTypeObject *cls);
void *PyObject_GetItemData_DuringGC(PyObject *o);
void *PyType_GetModuleState_DuringGC(PyTypeObject *type);
void *PyModule_GetState_DuringGC(PyObject *module);
int PyModule_GetToken_DuringGC(PyObject *module, void **result);
以上五个(3.15 新增),对应 PyObject_GetTypeData、PyObject_GetItemData、PyType_GetModuleState、PyModule_GetState、PyModule_GetToken。
int PyType_GetBaseByToken_DuringGC(PyTypeObject *type, void *tp_token, PyTypeObject **result);
(3.15 新增)对应 PyType_GetBaseByToken,差别在于它把 *result 设置为借用引用而非强引用,引用在整个 tp_traverse 调用期间有效。
PyObject *PyType_GetModule_DuringGC(PyTypeObject *type);
PyObject *PyType_GetModuleByToken_DuringGC(PyTypeObject *type, const void *mod_token);
(3.15 新增)对应 PyType_GetModule、PyType_GetModuleByToken,同样返回在整个 tp_traverse 调用期间有效的借用引用。
六、控制与查询垃圾回收器状态
C API 提供了一组函数用于在 C 代码中控制回收行为,其声明集中在 Include/objimpl.h:
Py_ssize_t PyGC_Collect(void); // 执行一次完整 GC(若 GC 已启用)
int PyGC_Enable(void); // 3.10 新增,类似 gc.enable
int PyGC_Disable(void); // 3.10 新增,类似 gc.disable
int PyGC_IsEnabled(void); // 3.10 新增,类似 gc.isenabled
PyGC_Collect():如果垃圾回收器已启用,则执行一次完整垃圾回收(注意 Python 层的gc.collect是无条件执行的)。返回"已收集对象数 + 不可收集对象数"之和;如果 GC 已被禁用或正处于回收过程中,则立即返回 0。回收过程中的错误会转交给sys.unraisablehook,该函数本身不抛出异常。PyGC_Enable/PyGC_Disable:分别启用/禁用回收器,返回调用前的状态(0 禁用、1 启用)。PyGC_IsEnabled:查询当前状态,返回 0 或 1。
6.1 遍历所有存活 GC 对象
void PyUnstable_GC_VisitObjects(gcvisitobjects_t callback, void *arg);
(3.12 新增)对所有存活的 GC 能力对象执行传入的 callback,arg 原样传递给每次回调调用。注意两条约束:如果回调期间有新对象被分配/销毁,它们是否会被访问是未定义的;该操作期间 GC 被禁用,在回调中显式触发一次回收会导致未定义行为(例如同一对象被访问多次,或一次都没有)。
回调类型:
int (*gcvisitobjects_t)(PyObject *object, void *arg)
arg 与传给 PyUnstable_GC_VisitObjects 的相同。返回 1 表示继续迭代,返回 0 表示停止;其他返回值目前保留,行为未定义。
七、源码印证:GC 头部、追踪链表与循环判定
以下结合仓库源码,说明上述 API 在 CPython 中如何落地(适用于当前主干源码,部分为内部实现细节):
-
PyGC_Head位于对象之前。在 Include/internal/pycore_interp_structs.h 中,PyGC_Head是一个含两个uintptr_t(_gc_next/_gc_prev)的结构体,注释明确"GC information is stored BEFORE the object structure",最低两位借自结构体对齐,用作FINALIZED、COLLECTING等标志位。PyObject_GC_New系列宏分配内存时会为这个头部预留空间,这也是它必须与PyObject_GC_Del配对的原因。 -
PyObject_GC_Track的本质是挂入第 0 代双向链表。内部实现_PyObject_GC_TRACK定义在 Include/internal/pycore_gc.h:经典(GIL)构建下它把对象挂到解释器gc状态的generation0链表上并递增heap_size,同时断言对象未被追踪、且不在正在回收的代数中;free-threading 构建下则只置位ob_gc_bits的_PyGC_BITS_TRACKED位。_PyObject_GC_UNTRACK(同文件 L247 起)是其逆操作。PyObject_GC_IsTracked则检查类型是否为 GC 类型且头部标记为已追踪(Python/gc.c)。 -
tp_traverse被回收器直接调用来减去内部引用。在 Python/gc.c 的subtract_refs中,回收器对候选容器逐一取出Py_TYPE(op)->tp_traverse并调用,回调是visit_decref(同文件 L438-L456):每当遍历到一个同样属于当前回收代的 GC 对象,就将其gc_refs减 1。经过减法后,gc_refs > 0的对象表示仍被容器外部直接引用、不可收集;gc_refs == 0的对象则被归入不可达集合,最终触发tp_clear打破环并释放。这一实现也解释了为什么tp_traverse必须"忠实"地报告所有持有的引用——漏报会让本可存活的对象被误判为垃圾,多报则导致垃圾滞留。 -
多代结构的依据。Include/internal/pycore_interp_structs.h 中的
gc_generation结构持有每代链表的头、阈值(threshold)与计数(count),对应 Python 层gc模块中三代阈值配置(参见 gc 模块文档)。
八、实践清单:从零接入循环 GC 的检查项
把以上规则浓缩成一份可核对的实现清单:
- 类型定义:
tp_flags加入Py_TPFLAGS_HAVE_GC;如有实例__dict__托管,加Py_TPFLAGS_MANAGED_DICT并调用PyObject_VisitManagedDict; - 构造函数:用
tp_alloc(推荐PyType_GenericAlloc)分配,或用PyObject_GC_New/PyObject_GC_NewVar分配;在所有可能被访问的字段就绪后、函数末尾调用PyObject_GC_Track; tp_traverse:参数命名为visit/arg;对每个拥有的成员Py_VISIT;堆类型必须Py_VISIT(Py_TYPE(self));不传NULL;visit非零返回立即透传;全程无副作用,只用第五节白名单中的函数;tp_clear:可变类型必须实现,丢弃成环引用且保持对象有效;不可变类型可为NULL;tp_dealloc:先PyObject_GC_UnTrack,再使字段失效并Py_DECREF,最后PyObject_GC_Del(通常经由tp_free);- 变长对象调整大小用
PyObject_GC_Resize,且必须在追踪之前; - 若需在 C 代码中主动触发或开关回收,使用
PyGC_Collect/PyGC_Enable/PyGC_Disable/PyGC_IsEnabled,并记住PyGC_Collect在 GC 被禁用或正在回收时静默返回 0。
遵循这套规则,你的 C 扩展类型就能与 CPython 的分代循环回收器无缝协作:既不会因漏报引用造成对象被误回收,也不会因追踪/解追踪失配导致回收器链表损坏。
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