CPython Monitoring C API:扩展如何手动触发 sys.monitoring 监控事件
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本文基于 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)返回事件 IDev对应的是否为 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.monitoringPython 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 |
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考