CPython Monitoring C API:扩展如何手动触发 sys.monitoring 监控事件

发布时间:2026/9/5 20:54:12
CPython Monitoring C API:扩展如何手动触发 sys.monitoring 监控事件 CPython Monitoring C API扩展如何手动触发 sys.monitoring 监控事件【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文基于 CPython 官方文档Doc/c-api/monitoring.rstPython 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_callbackC API 只负责发射端。事件定义与回调签名详见 Doc/library/sys.monitoring.rst。需要注意的前提限制仅限 3.13 及以上版本文档明确标注 Added in version 3.13非受限 APILimited 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_FirePyStartEventPY_START无(state, codelike, offset)PyMonitoring_FirePyResumeEventPY_RESUME无(state, codelike, offset)PyMonitoring_FirePyReturnEventPY_RETURN返回值(state, codelike, offset, retval)PyMonitoring_FirePyYieldEventPY_YIELD返回值(state, codelike, offset, retval)PyMonitoring_FireCallEventCALL被调对象 第一参数(state, codelike, offset, callable, arg0)PyMonitoring_FireLineEventLINE行号(state, codelike, offset, lineno)PyMonitoring_FireJumpEventJUMP目标偏移(state, codelike, offset, target_offset)PyMonitoring_FireBranchLeftEventBRANCH_LEFT目标偏移(state, codelike, offset, target_offset)PyMonitoring_FireBranchRightEventBRANCH_RIGHT目标偏移(state, codelike, offset, target_offset)PyMonitoring_FireCReturnEventC_RETURN返回值(state, codelike, offset, retval)PyMonitoring_FirePyThrowEventPY_THROW当前异常(state, codelike, offset)PyMonitoring_FireRaiseEventRAISE当前异常(state, codelike, offset)PyMonitoring_FireCRaiseEventC_RAISE当前异常(state, codelike, offset)PyMonitoring_FireReraiseEventRERAISE当前异常(state, codelike, offset)PyMonitoring_FireExceptionHandledEventEXCEPTION_HANDLED当前异常(state, codelike, offset)PyMonitoring_FirePyUnwindEventPY_UNWIND当前异常(state, codelike, offset)PyMonitoring_FireStopIterationEventSTOP_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为负时直接报ValueErroroffset must be non-negativeLINE事件不向回调传 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_teardownPython/instrumentation.c保证调用前必须已有异常处于设置状态否则报ValueError: Firing event N with no exception set事件分发期间先PyErr_GetRaisedException取出异常、分发结束后再PyErr_SetRaisedException还原因此调用前后异常状态保持不变回调若自身抛错则会替换掉原异常对这类事件返回DISABLE是不允许的capi_call_instrumentation中非插桩事件返回DISABLE会报ValueErrorCannot disable %s events. Callback removed.并清除对应回调。Lib/test/test_monitoring.py的TestCApiEventGeneration.CANNOT_DISABLE集合正是对此的测试佐证。3.3 两条特殊语义STOP_ITERATION若value本身是StopIteration实例则直接使用否则新建StopIteration(value)实现见_PyMonitoring_FireStopIterationEventPython/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 0CALL 1 4……见instrumentation.c中add_power2_constant的1 i生成逻辑所以 ID 就是int(math.log2(事件常量))。Lib/test/test_monitoring.py的测试里正是用int(math.log2(event))计算该值传入EnterScopeLib/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_arrayPyMonitoring_ExitScope当前是一个直接返回 0 的占位实现Python/instrumentation.c调用它主要是保持调用约定与未来的对称性。4.2 事件 ID 宏完整表event_types数组使用的宏与 Include/cpython/monitoring.h 中的数值定义对应宏数值对应事件PY_MONITORING_EVENT_PY_START0PY_STARTPY_MONITORING_EVENT_PY_RESUME1PY_RESUMEPY_MONITORING_EVENT_PY_RETURN2PY_RETURNPY_MONITORING_EVENT_PY_YIELD3PY_YIELDPY_MONITORING_EVENT_CALL4CALLPY_MONITORING_EVENT_LINE5LINEPY_MONITORING_EVENT_INSTRUCTION6INSTRUCTIONPY_MONITORING_EVENT_JUMP7JUMPPY_MONITORING_EVENT_BRANCH_LEFT8BRANCH_LEFTPY_MONITORING_EVENT_BRANCH_RIGHT9BRANCH_RIGHTPY_MONITORING_EVENT_STOP_ITERATION10STOP_ITERATIONPY_MONITORING_EVENT_RAISE11RAISEPY_MONITORING_EVENT_EXCEPTION_HANDLED12EXCEPTION_HANDLEDPY_MONITORING_EVENT_PY_UNWIND13PY_UNWINDPY_MONITORING_EVENT_PY_THROW14PY_THROWPY_MONITORING_EVENT_RERAISE15RERAISEPY_MONITORING_EVENT_C_RETURN16C_RETURNPY_MONITORING_EVENT_C_RAISE17C_RAISE头文件中把 0–10 归为Local events. These require bytecode instrumentation11–15 为异常类事件can now be turned on and disabled on a per code object basis16–18 为辅助事件。INSTRUCTION事件在头文件中有 ID6但 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_ITERATIONInclude/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则按预期抛ValueErrortest_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.13PY_MONITORING_IS_INSTRUMENTED_EVENT在 3.14 起软弃用整个 API 不在 Limited API 中发布到 PyPI 的 stable ABI 扩展无法使用。八、延伸阅读仓库内路径内容路径本文对应的 C API 官方文档Doc/c-api/monitoring.rstsys.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.cC API 事件生成测试含 DISABLE/异常边界用例Lib/test/test_monitoring.py【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考