news 2026/9/5 20:54:00

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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.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_STARTLINERAISE等监控事件;
  • 状态管理PyMonitoring_EnterScope/PyMonitoring_ExitScope,用于同步"哪些事件当前是激活的"这一状态。

事件订阅本身仍然通过 Python 层的sys.monitoring完成(use_tool_idset_eventsregister_callback),C API 只负责"发射"端。事件定义与回调签名详见 Doc/library/sys.monitoring.rst。

需要注意的前提限制:

  1. 仅限 3.13 及以上版本,文档明确标注 "Added in version 3.13";
  2. 非受限 API(Limited API):头文件 Include/cpython/monitoring.h 顶部即注明 "There is currently no limited API for monitoring",且整个头文件被#ifndef Py_LIMITED_API包裹(第 3–8 行),只能在完整 C API 下使用;
  3. 异常状态约定(文档原文明确要求):除下文标注"使用当前异常"的函数外,不得在异常处于设置状态时调用任何 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为负时直接报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_THROWRAISECRaiseRERAISEEXCEPTION_HANDLEDPY_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.pyTestCApiEventGeneration.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 << 0CALL = 1 << 4……,见instrumentation.cadd_power2_constant1 << 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_arrayversion;而当 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_START
PY_MONITORING_EVENT_PY_RESUME1PY_RESUME
PY_MONITORING_EVENT_PY_RETURN2PY_RETURN
PY_MONITORING_EVENT_PY_YIELD3PY_YIELD
PY_MONITORING_EVENT_CALL4CALL
PY_MONITORING_EVENT_LINE5LINE
PY_MONITORING_EVENT_INSTRUCTION6INSTRUCTION
PY_MONITORING_EVENT_JUMP7JUMP
PY_MONITORING_EVENT_BRANCH_LEFT8BRANCH_LEFT
PY_MONITORING_EVENT_BRANCH_RIGHT9BRANCH_RIGHT
PY_MONITORING_EVENT_STOP_ITERATION10STOP_ITERATION
PY_MONITORING_EVENT_RAISE11RAISE
PY_MONITORING_EVENT_EXCEPTION_HANDLED12EXCEPTION_HANDLED
PY_MONITORING_EVENT_PY_UNWIND13PY_UNWIND
PY_MONITORING_EVENT_PY_THROW14PY_THROW
PY_MONITORING_EVENT_RERAISE15RERAISE
PY_MONITORING_EVENT_C_RETURN16C_RETURN
PY_MONITORING_EVENT_C_RAISE17C_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_YIELDPY_UNWIND),验证两个状态的激活位互不影响。

七、实践要点清单

  1. 状态数组与版本号必须由扩展持有:随 codelike/解释器实例一起分配,version初始为 0;不要在两次EnterScope之间手动改动它。
  2. 递归可复用、挂起需重入:模拟递归函数时复用同一state_array/version;模拟生成器暂停-恢复时,需ExitScope后重新EnterScope(恢复时通常会触发PY_RESUME事件)。
  3. 异常事件只在异常上下文中调用:调用前异常必须已设置(PyErr_SetRaisedException之后),函数保证调用后异常原样保留。
  4. 回调可以返回sys.monitoring.DISABLE:对 local 事件(active对应位为插桩事件)会静默禁用该工具在该作用域槽上的事件;对异常类事件则报错并清除回调——扩展不应依赖DISABLE用于异常路径。
  5. C_RETURN/C_RAISE 必须随 CALL 启用set_events对三者的绑定约束在 C 端同样存在,单独触发 C 类事件前先确认CALL已激活。
  6. 版本前提:本 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_instrumentationEnterScope/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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 20:48:17

3分钟搞懂 AGENTS.md:AI编程代理配置实操手册

3分钟搞懂 AGENTS.md&#xff1a;AI编程代理配置实操手册 【免费下载链接】agents.md AGENTS.md — a simple, open format for guiding coding agents 项目地址: https://gitcode.com/GitHub_Trending/ag/agents.md 让AI写代码&#xff0c;改了三遍还是不对&#xff1a…

作者头像 李华
网站建设 2026/9/5 20:47:59

yfinance 教程:5 分钟用 Python 批量获取金融数据与实时行情

yfinance 教程&#xff1a;5 分钟用 Python 批量获取金融数据与实时行情 【免费下载链接】yfinance Download market data from Yahoo! Finances API 项目地址: https://gitcode.com/GitHub_Trending/yf/yfinance yfinance 是一个 Python 金融数据工具&#xff0c;直接从…

作者头像 李华
网站建设 2026/9/5 20:45:52

three.js BatchedMesh 深度指南:用多绘制批次渲染减少 Draw Call

three.js BatchedMesh 深度指南&#xff1a;用多绘制批次渲染减少 Draw Call 【免费下载链接】three.js JavaScript 3D Library. 项目地址: https://gitcode.com/GitHub_Trending/th/three.js 本篇基于 three.js 官方 API 文档与源码实现&#xff0c;系统讲解 BatchedMe…

作者头像 李华
网站建设 2026/9/5 20:45:20

技术分享课如何做到学员可复现:最小闭环与环境自检

评价一次技术讲师授课分享的质量&#xff0c;不能只看老师讲得多顺&#xff0c;还要看现场学员在课程结束后能不能独立还原课堂步骤。常见的情况是&#xff1a;老师在自己的电脑里跑通了三遍示例&#xff0c;学员打开命令行之后第一行命令就报错&#xff1b;老师切到示例代码很…

作者头像 李华