首页
/ CPython C 扩展开发指南:为自定义类型实现循环垃圾回收支持(Py_TPFLAGS_HAVE_GC、tp_traverse 与 PyObject_GC_Track)

CPython C 扩展开发指南:为自定义类型实现循环垃圾回收支持(Py_TPFLAGS_HAVE_GC、tp_traverse 与 PyObject_GC_Track)

2026-09-04 18:26:40作者:伍霜盼Ellen

当你编写 CPython 的 C 扩展或嵌入式应用时,只要你的自定义类型会持有其他对象的引用,就可能与 Python 对象之间形成"引用环"(reference cycle)。这类环无法靠引用计数回收,必须交给 CPython 的分代循环垃圾回收器处理。本篇基于 CPython 仓库中的官方 C API 文档 Doc/c-api/gcsupport.rst,系统讲解如何让你的 C 类型正确接入循环垃圾回收:从类型标志位 Py_TPFLAGS_HAVE_GCtp_traverse/tp_clear 槽位的实现,到 PyObject_GC_NewPyObject_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):

  1. tp_flags 字段必须包含 Py_TPFLAGS_HAVE_GC 标志。设置了该标志的对象必须符合本文档的全部规则,这类对象下文称"容器对象";
  2. 必须提供 tp_traverse 处理器,用于让回收器遍历对象内部持有的引用;
  3. 如果类型的实例是可变的,还必须提供 tp_clear 实现(用于打破已确认存在的引用环);不可变对象不可能直接参与引用环,tp_clear 可以为 NULL

警告(原文档强调):一旦类型设置了 Py_TPFLAGS_HAVE_GC,就必须实现至少一个 tp_traverse 处理器,或者显式复用其父类/子类的 tp_traverse

补充规则:调用 PyType_Ready(或间接调用它的 PyType_FromSpecWithBasesPyType_FromSpec)时,如果类型继承自一个实现了垃圾回收协议(带 Py_TPFLAGS_HAVE_GC)的类,而子类型自身没有设置该标志,解释器会自动为子类型填充 tp_flagstp_traversetp_clear 字段,从父类继承对应实现。

tp_clear 的类型是 inquiry(即 int (*inquiry)(PyObject *self)):

int (*inquiry)(PyObject *self)

其职责是丢弃可能构成引用环的引用。注意:调用该方法后对象仍然必须是有效的(不能只是简单 Py_DECREF 一个引用而留下悬空状态)。回收器在检测到对象参与了引用环时会调用它。

三、构造函数与析构函数的四条配对规则

容器对象在构造和销毁时必须遵守对称的规则,否则回收器的链表会损坏:

构造函数规则:

  1. 对象内存必须用 PyObject_GC_NewPyObject_GC_NewVar 分配;
  2. 当所有可能包含"对其他容器引用"的字段都初始化完毕后,必须调用 PyObject_GC_Track 把对象加入回收器的追踪集合。由于回收器可能在不期望的时机运行,对象在被追踪期间必须始终处于有效状态——因此 PyObject_GC_Track 通常在构造函数末尾调用。

析构函数(tp_dealloc)规则:

  1. 在使 tp_traverse 所访问的字段失效之前,必须先调用 PyObject_GC_UnTrack
  2. 对象内存必须用 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。要求 opPyVarObject *,且尚未被回收器追踪newsizePy_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_traversearg
  • 不得以 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 实现的参数必须恰好命名为 visitarg,不能随意命名。宏在 Include/objimpl.h 中的实际定义为:

#define Py_VISIT(op)                                                    \
    do {                                                                \
        if (op) {                                                       \
            int vret = visit(_PyObject_CAST(op), arg);                   \
            if (vret)                                                   \
                return vret;                                            \
        }                                                               \
    } while (0)

即:opNULL 时调用 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_clearlocal_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_traversevisit 函数本身;
  • Py_VISIT 宏;
  • Py_SIZE
  • Py_TYPE:从 tp_traverse 中调用时,其结果在整个处理器调用期间有效;
  • PyObject_VisitManagedDict
  • PyObject_TypeCheckPyType_IsSubtypePyType_HasFeature
  • Py{<type>}_CheckPy{<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_GetTypeDataPyObject_GetItemDataPyType_GetModuleStatePyModule_GetStatePyModule_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_GetModulePyType_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 能力对象执行传入的 callbackarg 原样传递给每次回调调用。注意两条约束:如果回调期间有新对象被分配/销毁,它们是否会被访问是未定义的;该操作期间 GC 被禁用,在回调中显式触发一次回收会导致未定义行为(例如同一对象被访问多次,或一次都没有)。

回调类型:

int (*gcvisitobjects_t)(PyObject *object, void *arg)

arg 与传给 PyUnstable_GC_VisitObjects 的相同。返回 1 表示继续迭代,返回 0 表示停止;其他返回值目前保留,行为未定义。

七、源码印证:GC 头部、追踪链表与循环判定

以下结合仓库源码,说明上述 API 在 CPython 中如何落地(适用于当前主干源码,部分为内部实现细节):

  1. PyGC_Head 位于对象之前。在 Include/internal/pycore_interp_structs.h 中,PyGC_Head 是一个含两个 uintptr_t_gc_next/_gc_prev)的结构体,注释明确"GC information is stored BEFORE the object structure",最低两位借自结构体对齐,用作 FINALIZEDCOLLECTING 等标志位。PyObject_GC_New 系列宏分配内存时会为这个头部预留空间,这也是它必须与 PyObject_GC_Del 配对的原因。

  2. 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)。

  3. tp_traverse 被回收器直接调用来减去内部引用。在 Python/gc.csubtract_refs 中,回收器对候选容器逐一取出 Py_TYPE(op)->tp_traverse 并调用,回调是 visit_decref(同文件 L438-L456):每当遍历到一个同样属于当前回收代的 GC 对象,就将其 gc_refs 减 1。经过减法后,gc_refs > 0 的对象表示仍被容器外部直接引用、不可收集;gc_refs == 0 的对象则被归入不可达集合,最终触发 tp_clear 打破环并释放。这一实现也解释了为什么 tp_traverse 必须"忠实"地报告所有持有的引用——漏报会让本可存活的对象被误判为垃圾,多报则导致垃圾滞留。

  4. 多代结构的依据Include/internal/pycore_interp_structs.h 中的 gc_generation 结构持有每代链表的头、阈值(threshold)与计数(count),对应 Python 层 gc 模块中三代阈值配置(参见 gc 模块文档)。

八、实践清单:从零接入循环 GC 的检查项

把以上规则浓缩成一份可核对的实现清单:

  1. 类型定义:tp_flags 加入 Py_TPFLAGS_HAVE_GC;如有实例 __dict__ 托管,加 Py_TPFLAGS_MANAGED_DICT 并调用 PyObject_VisitManagedDict
  2. 构造函数:用 tp_alloc(推荐 PyType_GenericAlloc)分配,或用 PyObject_GC_New / PyObject_GC_NewVar 分配;在所有可能被访问的字段就绪后、函数末尾调用 PyObject_GC_Track
  3. tp_traverse:参数命名为 visit / arg;对每个拥有的成员 Py_VISIT;堆类型必须 Py_VISIT(Py_TYPE(self));不传 NULLvisit 非零返回立即透传;全程无副作用,只用第五节白名单中的函数;
  4. tp_clear:可变类型必须实现,丢弃成环引用且保持对象有效;不可变类型可为 NULL
  5. tp_dealloc:先 PyObject_GC_UnTrack,再使字段失效并 Py_DECREF,最后 PyObject_GC_Del(通常经由 tp_free);
  6. 变长对象调整大小用 PyObject_GC_Resize,且必须在追踪之前;
  7. 若需在 C 代码中主动触发或开关回收,使用 PyGC_Collect / PyGC_Enable / PyGC_Disable / PyGC_IsEnabled,并记住 PyGC_Collect 在 GC 被禁用或正在回收时静默返回 0。

遵循这套规则,你的 C 扩展类型就能与 CPython 的分代循环回收器无缝协作:既不会因漏报引用造成对象被误回收,也不会因追踪/解追踪失配导致回收器链表损坏。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.79 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
988
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384