CPython Monitoring C API:扩展如何手动触发 sys.monitoring 监控事件
本文基于 CPython 官方文档 Doc/c-api/monitoring.rst(Python 3.13 引入),系统讲解 Monitoring C API 的完整用法:PyMonitoringState 状态结构、PyMonitoring_Fire*Event 系列事件触发函数、PyMonitoring_EnterScope/PyMonitoring_ExitScope 作用域管理,以及事件 ID 宏的定义。读完本文,你将掌握如何在 C 扩展中"模拟 Python 代码执行"并向 sys.monitoring 的回调分发事件(例如为 WASM/Pyodide 解释器、字节码虚拟机或测试桩暴露监控点),并能结合源码理解事件分发的底层机制与 DISABLE 优化的实现细节。
一、背景:C 扩展为什么要主动触发监控事件
CPython 3.13 引入的 sys.monitoring 模块为调试器、覆盖率工具、分析器提供了统一的事件订阅体系。当 C 扩展自己"模拟"执行 Python 代码时(文档原文措辞为 "as it emulates the execution of Python code"),它需要把执行中的关键节点——函数开始、行执行、调用、异常抛出等——主动报告给监控系统。
为此,CPython 提供了一组 C API(文档入口见 Doc/c-api/monitoring.rst):
- 事件触发:
PyMonitoring_Fire*Event系列函数,允许扩展手动发出PY_START、LINE、RAISE等监控事件; - 状态管理:
PyMonitoring_EnterScope/PyMonitoring_ExitScope,用于同步"哪些事件当前是激活的"这一状态。
事件订阅本身仍然通过 Python 层的 sys.monitoring 完成(use_tool_id、set_events、register_callback),C API 只负责"发射"端。事件定义与回调签名详见 Doc/library/sys.monitoring.rst。
需要注意的前提限制:
- 仅限 3.13 及以上版本,文档明确标注 "Added in version 3.13";
- 非受限 API(Limited API):头文件 Include/cpython/monitoring.h 顶部即注明 "There is currently no limited API for monitoring",且整个头文件被
#ifndef Py_LIMITED_API包裹(第 3–8 行),只能在完整 C API 下使用; - 异常状态约定(文档原文明确要求):除下文标注"使用当前异常"的函数外,不得在异常处于设置状态时调用任何 monitoring 函数。
二、PyMonitoringState:事件激活状态的紧凑表示
文档定义的 PyMonitoringState 类型表示"某一事件类型在某一作用域内的状态":内存由用户(扩展)自行分配,内容则由 monitoring API 函数维护。头文件中的实际定义非常小:
// Include/cpython/monitoring.h (L49-L52)
typedef struct _PyMonitoringState {
uint8_t active; // 激活位图:哪些工具订阅了该事件
uint8_t opaque;
} PyMonitoringState;
从源码实现看,active 是一个 8 位"工具位图"(每个 bit 对应一个 tool ID)。PyMonitoring_EnterScope 会用解释器全局的 interp->monitors.tools[event] 位图填充它(见 Python/instrumentation.c 中 state_array[i].active = m->tools[event])。所有 Fire 函数在内部会检查这个位图:没有工具订阅时直接返回 0,不产生任何 Python 调用开销——这正是"紧凑信息"设计带来的快速路径。
三、事件触发函数:PyMonitoring_Fire*Event 全表
文档规定:这些函数成功返回 0,出错返回 -1 并设置异常("All of the functions below return 0 on success and -1 (with an exception set) on error")。每个函数接受一个 PyMonitoringState*、一个 codelike(必须是 types.CodeType 实例或模拟它的对象)、一个 int32_t offset 指令偏移,以及事件特有的附加参数。
文档中给出的完整函数签名如下(与 Include/cpython/monitoring.h 中的声明一一对应):
| 函数 | 事件 | 附加参数 | 签名 |
|---|---|---|---|
PyMonitoring_FirePyStartEvent |
PY_START |
无 | (state, codelike, offset) |
PyMonitoring_FirePyResumeEvent |
PY_RESUME |
无 | (state, codelike, offset) |
PyMonitoring_FirePyReturnEvent |
PY_RETURN |
返回值 | (state, codelike, offset, retval) |
PyMonitoring_FirePyYieldEvent |
PY_YIELD |
返回值 | (state, codelike, offset, retval) |
PyMonitoring_FireCallEvent |
CALL |
被调对象 + 第一参数 | (state, codelike, offset, callable, arg0) |
PyMonitoring_FireLineEvent |
LINE |
行号 | (state, codelike, offset, lineno) |
PyMonitoring_FireJumpEvent |
JUMP |
目标偏移 | (state, codelike, offset, target_offset) |
PyMonitoring_FireBranchLeftEvent |
BRANCH_LEFT |
目标偏移 | (state, codelike, offset, target_offset) |
PyMonitoring_FireBranchRightEvent |
BRANCH_RIGHT |
目标偏移 | (state, codelike, offset, target_offset) |
PyMonitoring_FireCReturnEvent |
C_RETURN |
返回值 | (state, codelike, offset, retval) |
PyMonitoring_FirePyThrowEvent |
PY_THROW |
当前异常 | (state, codelike, offset) |
PyMonitoring_FireRaiseEvent |
RAISE |
当前异常 | (state, codelike, offset) |
PyMonitoring_FireCRaiseEvent |
C_RAISE |
当前异常 | (state, codelike, offset) |
PyMonitoring_FireReraiseEvent |
RERAISE |
当前异常 | (state, codelike, offset) |
PyMonitoring_FireExceptionHandledEvent |
EXCEPTION_HANDLED |
当前异常 | (state, codelike, offset) |
PyMonitoring_FirePyUnwindEvent |
PY_UNWIND |
当前异常 | (state, codelike, offset) |
PyMonitoring_FireStopIterationEvent |
STOP_ITERATION |
迭代值 | (state, codelike, offset, value) |
回调实际收到的参数与 Python 端签名一致(文档要求参见 sys.monitoring):例如 PY_START/PY_RESUME 回调收到 (code, instruction_offset);CALL 回调收到 (code, instruction_offset, callable, arg0);LINE 回调收到 (code, line_number);异常类事件回调收到 (code, instruction_offset, exception)。完整签名列表见 Doc/library/sys.monitoring.rst。
3.1 底层分发机制:vectorcall 直调回调
从 Python/instrumentation.c 的 capi_call_instrumentation 可以看到实现细节:
offset为负时直接报ValueError("offset must be non-negative");LINE事件不向回调传 offset,而是把lineno装箱为int作为第二个参数(对应 Python 端func(code, line_number)的签名),其余事件将offset装箱传入;- 随后按
state->active位图从最高位到最低位逐个工具通过_PyObject_VectorcallTstate向量调用各工具注册的回调,回调返回sys.monitoring.DISABLE时,直接对该事件state->active &= ~(1 << tool)——即"按位置禁用",无需 StopTheWorld(区别于解释器内联插桩路径需要停世界来改写字节码)。
3.2 异常类事件:自动读取"当前异常"
PY_THROW、RAISE、CRaise、RERAISE、EXCEPTION_HANDLED、PY_UNWIND 六个函数不接收异常参数,而是内部通过 PyErr_GetRaisedException() 读取当前异常。源码中的 exception_event_setup / exception_event_teardown(Python/instrumentation.c)保证:
- 调用前必须已有异常处于设置状态,否则报
ValueError: "Firing event N with no exception set"; - 事件分发期间先
PyErr_GetRaisedException取出异常、分发结束后再PyErr_SetRaisedException还原,因此调用前后异常状态保持不变(回调若自身抛错则会替换掉原异常); - 对这类事件返回
DISABLE是不允许的:capi_call_instrumentation中,非插桩事件返回DISABLE会报ValueError("Cannot disable %s events. Callback removed.")并清除对应回调。Lib/test/test_monitoring.py的TestCApiEventGeneration.CANNOT_DISABLE集合正是对此的测试佐证。
3.3 两条特殊语义
- STOP_ITERATION:若
value本身是StopIteration实例则直接使用,否则新建StopIteration(value)(实现见_PyMonitoring_FireStopIterationEvent,Python/instrumentation.c 中先PyErr_SetObject(PyExc_StopIteration, value)再走异常事件路径)。 - CALL 与 C_RETURN/C_RAISE 的绑定关系:与 Python 端
set_events的约束一致("cannot set C_RETURN or C_RAISE events independently"),源码中C_CALL_EVENTS宏将CALL | C_RETURN | C_RAISE视为一个整体(Python/instrumentation.c)。C_RETURN/C_RAISE事件只能随CALL一起启用。
四、作用域管理:PyMonitoring_EnterScope / PyMonitoring_ExitScope
文档原文:"Monitoring states can be managed with the help of monitoring scopes. A scope would typically correspond to a Python function."(monitoring 状态可以通过 monitoring 作用域管理,一个作用域通常对应一个 Python 函数。)
4.1 参数详解
int PyMonitoring_EnterScope(
PyMonitoringState *state_array, // 用户分配、API 填充的状态数组
uint64_t *version, // 用户分配并初始化为 0 的版本号
const uint8_t *event_types, // 该作用域内可能触发的事件 ID 数组
Py_ssize_t length); // event_types(从而也是 state_array)的长度
event_types:事件 ID 数组。ID 的取值规则文档明确给出:PY_START事件的 ID 是PY_MONITORING_EVENT_PY_START,其数值等于sys.monitoring.events.PY_START的二进制对数——因为 Python 端事件常量按位定义(PY_START = 1 << 0,CALL = 1 << 4……,见instrumentation.c中add_power2_constant的1 << i生成逻辑),所以 ID 就是int(math.log2(事件常量))。Lib/test/test_monitoring.py的测试里正是用int(math.log2(event))计算该值传入EnterScope(Lib/test/test_monitoring.py)。state_array:与event_types等长的状态数组,由用户分配,PyMonitoring_EnterScope负责用各事件的激活位图填充它。version:指针指向的值须由用户与state_array一起分配并初始化为 0,之后只允许PyMonitoring_EnterScope修改。它实现了一个版本快速路径:若解释器全局版本未变化,EnterScope直接返回 0,不做任何刷新(Python/instrumentation.c 中if (global_version(interp) == *version) return 0;)。- 作用域语义:文档强调这里的 scope 是词法作用域(函数、类或方法)。每次进入词法作用域都应调用一次
EnterScope;作用域可以重入——模拟递归 Python 函数时可复用同一组state_array与version;而当 code-like 的执行被暂停时(如模拟生成器挂起),需要先退出作用域再重新进入。
实现上的细节:PyMonitoring_EnterScope 每次刷新只是把 interp->monitors.tools[event](全局工具位图)拷贝进 state_array;PyMonitoring_ExitScope 当前是一个直接返回 0 的占位实现(Python/instrumentation.c),调用它主要是保持调用约定与未来的对称性。
4.2 事件 ID 宏完整表
event_types 数组使用的宏(与 Include/cpython/monitoring.h 中的数值定义对应):
| 宏 | 数值 | 对应事件 |
|---|---|---|
PY_MONITORING_EVENT_PY_START |
0 | PY_START |
PY_MONITORING_EVENT_PY_RESUME |
1 | PY_RESUME |
PY_MONITORING_EVENT_PY_RETURN |
2 | PY_RETURN |
PY_MONITORING_EVENT_PY_YIELD |
3 | PY_YIELD |
PY_MONITORING_EVENT_CALL |
4 | CALL |
PY_MONITORING_EVENT_LINE |
5 | LINE |
PY_MONITORING_EVENT_INSTRUCTION |
6 | INSTRUCTION |
PY_MONITORING_EVENT_JUMP |
7 | JUMP |
PY_MONITORING_EVENT_BRANCH_LEFT |
8 | BRANCH_LEFT |
PY_MONITORING_EVENT_BRANCH_RIGHT |
9 | BRANCH_RIGHT |
PY_MONITORING_EVENT_STOP_ITERATION |
10 | STOP_ITERATION |
PY_MONITORING_EVENT_RAISE |
11 | RAISE |
PY_MONITORING_EVENT_EXCEPTION_HANDLED |
12 | EXCEPTION_HANDLED |
PY_MONITORING_EVENT_PY_UNWIND |
13 | PY_UNWIND |
PY_MONITORING_EVENT_PY_THROW |
14 | PY_THROW |
PY_MONITORING_EVENT_RERAISE |
15 | RERAISE |
PY_MONITORING_EVENT_C_RETURN |
16 | C_RETURN |
PY_MONITORING_EVENT_C_RAISE |
17 | C_RAISE |
头文件中把 0–10 归为"Local events. These require bytecode instrumentation",11–15 为异常类事件("can now be turned on and disabled on a per code object basis"),16–18 为辅助事件。INSTRUCTION 事件在头文件中有 ID(6),但 C API 没有为其提供 Fire 函数——它面向逐指令插桩,属于纯解释器内部路径。另外,文档表中列出的 PY_MONITORING_EVENT_INSTRUCTION 等 17 个宏即上表内容(头文件还存在第 18 号 PY_MONITORING_EVENT_BRANCH 辅助宏,用于兼容旧的 BRANCH 语义,C API 文档未列出)。
4.3 PY_MONITORING_IS_INSTRUMENTED_EVENT 宏
int PY_MONITORING_IS_INSTRUMENTED_EVENT(uint8_t ev)
返回事件 ID ev 对应的是否为 local event(即 Doc/library/sys.monitoring.rst 中标注为 local 的事件,需要字节码插桩支撑的事件)。头文件实现为 (ev) <= PY_MONITORING_EVENT_STOP_ITERATION(Include/cpython/monitoring.h)。该宏 3.13 加入,文档标注 3.14 起软弃用(soft-deprecated),扩展代码应避免在新代码中依赖它来判断事件类别。
五、内联快速路径:Fire 函数的双层结构
阅读头文件时会注意到一个容易忽视的细节:PyMonitoring_Fire*Event 在 Include/cpython/monitoring.h 中是 static inline 函数,真正的实现是带下划线前缀的 _PyMonitoring_Fire*Event:
// Include/cpython/monitoring.h
#define _PYMONITORING_IF_ACTIVE(STATE, X) \
if ((STATE)->active) { \
return (X); \
} \
else { \
return 0; \
}
static inline int
PyMonitoring_FirePyStartEvent(PyMonitoringState *state, PyObject *codelike, int32_t offset)
{
_PYMONITORING_IF_ACTIVE(
state,
_PyMonitoring_FirePyStartEvent(state, codelike, offset));
}
也就是说:如果 EnterScope 填充的 state->active 为 0(没有任何工具订阅该事件),Fire 函数在头文件内联层就返回 0,连解释器内部的参数装箱都不会发生。这让"未启用监控"时的热路径开销几乎为零。同时,各 _PyMonitoring_Fire* 实现内部都有 assert(state->active)——直接调用下划线版本时 active 必须非零。
六、可运行的最小示例与仓库参考实现
仓库中自带一份可直接参考的 C API 用法示例:Modules/_testcapi/monitoring.c。它定义了一个 CodeLike 对象,内部持有 PyMonitoringState 数组和一个 version 字段——正是文档推荐的"状态数组 + 版本号"的用户侧存储方式;并提供 monitoring_enter_scope / monitoring_exit_scope 与全部 fire_event_* 包装函数。核心模式如下:
// 每个 codelike 对象保存自己的状态数组与版本号(用户分配)
typedef struct {
PyObject_HEAD
PyMonitoringState *monitoring_states;
uint64_t version; // 初始化为 0
int num_events;
} PyCodeLikeObject;
// 进入作用域:声明本作用域可能触发哪些事件
PyMonitoringState *state = &cl->monitoring_states[offset];
int res = PyMonitoring_FirePyStartEvent(state, codelike, offset);
在 Python 端订阅并验证的完整流程(与 Lib/test/test_monitoring.py 的 TestCApiEventGeneration 一致):
import sys, sys.monitoring, math
import _testcapi
TOOL = 0
sys.monitoring.use_tool_id(TOOL, "demo.tool")
sys.monitoring.register_callback(TOOL, sys.monitoring.events.PY_START,
lambda code, offset: print("PY_START at", offset))
sys.monitoring.set_events(TOOL, sys.monitoring.events.PY_START)
cl = _testcapi.CodeLike(1) # 1 个事件的状态槽
# event ID = int(log2(PY_START 常量)) = PY_MONITORING_EVENT_PY_START = 0
with _testcapi.monitoring_enter_scope(cl, int(math.log2(sys.monitoring.events.PY_START))):
_testcapi.fire_event_py_start(cl, 0) # 打印 PY_START at 0
测试用例还覆盖了若干边界行为,扩展开发者可直接参考:
test_fire_event:逐一验证 16 种 Fire 函数在事件开启时回调恰好触发 1 次、事件关闭时不触发;test_missing_exception:异常类事件在无异常设置时抛ValueError("Firing event N with no exception set"),与源码exception_event_setup的行为完全对应;test_disable_event:回调返回DISABLE后同一 Fire 调用不再重复触发;对PY_THROW/RAISE/RERAISE/EXCEPTION_HANDLED/PY_UNWIND则按预期抛ValueError;test_enter_scope_two_events:同一作用域注册两个事件(PY_YIELD、PY_UNWIND),验证两个状态的激活位互不影响。
七、实践要点清单
- 状态数组与版本号必须由扩展持有:随 codelike/解释器实例一起分配,
version初始为 0;不要在两次EnterScope之间手动改动它。 - 递归可复用、挂起需重入:模拟递归函数时复用同一
state_array/version;模拟生成器暂停-恢复时,需ExitScope后重新EnterScope(恢复时通常会触发PY_RESUME事件)。 - 异常事件只在异常上下文中调用:调用前异常必须已设置(
PyErr_SetRaisedException之后),函数保证调用后异常原样保留。 - 回调可以返回
sys.monitoring.DISABLE:对 local 事件(active对应位为插桩事件)会静默禁用该工具在该作用域槽上的事件;对异常类事件则报错并清除回调——扩展不应依赖DISABLE用于异常路径。 - C_RETURN/C_RAISE 必须随 CALL 启用:
set_events对三者的绑定约束在 C 端同样存在,单独触发 C 类事件前先确认CALL已激活。 - 版本前提:本 API 需 CPython ≥ 3.13;
PY_MONITORING_IS_INSTRUMENTED_EVENT在 3.14 起软弃用;整个 API 不在 Limited API 中,发布到 PyPI 的 stable ABI 扩展无法使用。
八、延伸阅读(仓库内路径)
| 内容 | 路径 |
|---|---|
| 本文对应的 C API 官方文档 | Doc/c-api/monitoring.rst |
sys.monitoring Python API 与事件签名 |
Doc/library/sys.monitoring.rst |
头文件:事件 ID 宏、PyMonitoringState、内联 Fire 包装 |
Include/cpython/monitoring.h |
核心实现:capi_call_instrumentation、EnterScope/ExitScope、异常事件路径 |
Python/instrumentation.c |
| 参考实现(CodeLike + fire_event_* 包装) | Modules/_testcapi/monitoring.c |
| C API 事件生成测试(含 DISABLE/异常边界用例) | Lib/test/test_monitoring.py |
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 StartedRust0622
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