CPython C API 顶层执行层深度解析:PyRun_*、Py_CompileString 与 PyEval_EvalFrame 的完整实践

发布时间:2026/9/7 7:44:57
CPython C API 顶层执行层深度解析:PyRun_*、Py_CompileString 与 PyEval_EvalFrame 的完整实践 CPython C API 顶层执行层深度解析PyRun_*、Py_CompileString 与 PyEval_EvalFrame 的完整实践【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本篇基于 CPython 官方 C API 文档的 The Very High Level Layer 一章Doc/c-api/veryhigh.rst系统讲解如何用 C 语言在已初始化的 Python 解释器中执行源码从PyRun_*执行函数族、Py_CompileString*编译函数族到PyCompilerFlags编译器标志、四种 start symbol 和字节码栈效应stack effect查询接口并结合 Python/pythonrun.c 源码印证各函数的真实调用链与返回值语义帮助嵌入式 Python 的开发者选对函数、用对参数。一、什么是顶层Very High Level层CPython 的 C API 按抽象程度分层very high level 是其中最上层的一组函数它们允许你执行给定文件或内存缓冲区中的 Python 源码但不提供与解释器更细粒度交互的能力例如不能直接操作对象、帧或代码对象环境。该层接口按输入来源 环境控制分成三族族输入来源环境典型函数脚本/交互执行族文件FILE*或字符串固定使用__main__模块的全局/局部命名空间PyRun_SimpleStringFlags、PyRun_SimpleFileExFlags、PyRun_AnyFileExFlags交互循环族交互设备终端/伪终端上的FILE*__main__使用sys.ps1/sys.ps2提示符PyRun_InteractiveLoopFlags、PyRun_InteractiveOneFlags自由环境执行族字符串或FILE*调用者显式提供globals/locals字典与 start symbolPyRun_StringFlags、PyRun_FileExFlags、Py_CompileStringObject、PyEval_EvalCode多个函数接受一个start symbol文法起始符号参数可用值为Py_eval_input、Py_file_input、Py_single_input、Py_func_type_input详见第六节。从源码看Python/pythonrun.c 中的check_start()严格只接受这四个值其余值会抛出ValueError: invalid start argument。还有一个跨函数族的注意点多个函数接受FILE*参数。不同 C 运行时库的FILE结构体定义可能不同且不兼容——至少在 Windows 上动态链接的扩展模块可能实际使用不同的 CRT 库因此只有在确定FILE*由与 Python 运行时相同的库创建时才能把它传给这些函数。二、从文件执行代码PyRun_AnyFile* 与 PyRun_SimpleFile*2.1 函数族与简化关系这一族遵循完整版 逐级简化版的命名约定int PyRun_AnyFile(FILE *fp, const char *filename); int PyRun_AnyFileFlags(FILE *fp, const char *filename, PyCompilerFlags *flags); int PyRun_AnyFileEx(FILE *fp, const char *filename, int closeit); int PyRun_AnyFileExFlags(FILE *fp, const char *filename, int closeit, PyCompilerFlags *flags);PyRun_AnyFile是PyRun_AnyFileExFlags的简化接口closeit取0、flags取NULLPyRun_AnyFileFlags仅额外传入flagscloseit仍为0PyRun_AnyFileEx仅额外传入closeitflags为NULLPyRun_AnyFileExFlags是最完整的形式。PyRun_AnyFileExFlags的语义如果fp指向与交互设备关联的文件控制台/终端输入或 Unix 伪终端返回PyRun_InteractiveLoop的值否则返回PyRun_SimpleFile的结果。filename从文件系统编码sys.getfilesystemencoding解码filename为NULL时使用???作为文件名closeit为真时文件在PyRun_SimpleFileExFlags()返回前被关闭。源码印证Python/pythonrun.c 中PyRun_AnyFileExFlags先把字节串文件名用PyUnicode_DecodeFSDefault解码再调用内部函数_PyRun_AnyFile后者通过_Py_FdIsInteractive(fp, filename)判断是否交互设备是则走_PyRun_InteractiveLoop否则走_PyRun_SimpleFile与文档描述完全一致。2.2 PyRun_SimpleFile* 族int PyRun_SimpleFile(FILE *fp, const char *filename); int PyRun_SimpleFileEx(FILE *fp, const char *filename, int closeit); int PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit, PyCompilerFlags *flags);PyRun_SimpleFile与PyRun_SimpleFileEx同样分别是PyRun_SimpleFileExFlags的简化接口closeit0、flagsNULL或仅closeit为 0。PyRun_SimpleFileExFlags与PyRun_SimpleStringFlags类似只是源码从fp读取而非内存字符串filename应为文件名从文件系统编码与错误处理程序解码closeit为真时函数返回前关闭文件。Windows 特别注意原文档明确警告fp应以二进制模式打开如fopen(filename, rb)否则 Python 可能无法正确处理使用 LF 行尾的脚本文件。从源码看PyRun_SimpleFileExFlagsPython/pythonrun.c会先用PyUnicode_DecodeFSDefault解码文件名并检查失败成功后交给_PyRun_SimpleFile若结果异常则PyErr_Print()并返回-1。此外内部实现_PyRun_SimpleFile还通过maybe_pyc_file()Python/pythonrun.c检测.pyc后缀或 magic 前缀从而支持直接执行字节码文件。三、从缓冲区执行代码PyRun_SimpleString*int PyRun_SimpleString(const char *command); int PyRun_SimpleStringFlags(const char *command, PyCompilerFlags *flags);PyRun_SimpleString是PyRun_SimpleStringFlags的简化接口flags为NULL。PyRun_SimpleStringFlags按照flags在__main__模块中执行command指向的 Python 源码若__main__尚不存在则创建。成功返回0发生异常返回-1。出错时无法再获取异常信息异常已被打印。源码印证Python/pythonrun.c 中PyRun_SimpleStringFlags通过PyImport_AddModuleRef(__main__)取得或创建__main__模块取其模块字典作为 globals 与 locals再以Py_file_input起始符号调用_PyRun_String失败时PyErr_Print()后返回 -1。一个重要的边界行为若代码抛出未处理的SystemExit只要PyConfig.inspect为零该函数不会返回 -1而是直接退出进程。源码中对应_PyRun_InteractiveLoop的循环体Python/pythonrun.c检查inspect配置与PyErr_ExceptionMatches(PyExc_SystemExit)两者条件满足时直接终止循环并走退出路径进程级处理逻辑集中在_Py_HandleSystemExitAndKeyboardInterrupt()Python/pythonrun.c它会解析SystemExit.code并调用Py_Exit。编写嵌入解释器的工具如 IDE 内嵌 Python时若不希望sys.exit()杀掉宿主进程需要意识到这一行为。四、交互执行PyRun_Interactive* 族与输入钩子4.1 PyRun_InteractiveOne*int PyRun_InteractiveOneObject(FILE *fp, PyObject *filename, PyCompilerFlags *flags); int PyRun_InteractiveOne(FILE *fp, const char *filename); int PyRun_InteractiveOneFlags(FILE *fp, const char *filename, PyCompilerFlags *flags);PyRun_InteractiveOneObject按flags从交互设备读取并执行单条语句使用sys.ps1和sys.ps2提示用户filename必须是 Pythonstr对象。返回0表示执行成功-1表示发生异常解析错误时返回 Include/errcode.h作为 Python 源码发行中定义的错误码——注意errcode.h不包含在Python.h中需要时须显式 include。PyRun_InteractiveOne是PyRun_InteractiveOneFlags的简化接口flags为NULLPyRun_InteractiveOneFlags与PyRun_InteractiveOneObject相同只是filename为const char*从文件系统编码解码。4.2 PyRun_InteractiveLoop*int PyRun_InteractiveLoop(FILE *fp, const char *filename); int PyRun_InteractiveLoopFlags(FILE *fp, const char *filename, PyCompilerFlags *flags);PyRun_InteractiveLoop是PyRun_InteractiveLoopFlags的简化接口。后者从交互设备读取并执行语句直到 EOF 为止同样使用sys.ps1/sys.ps2提示filename从文件系统编码解码EOF 时返回 0失败返回负数。源码印证_PyRun_InteractiveLoopPython/pythonrun.c会先确保sys.ps1/sys.ps2存在缺省为 和... 然后do { _PyRun_InteractiveOne(...) } while (ret ! E_EOF)循环读取执行对连续MemoryError计数超过 16 次则终止以防死循环并在非 inspect 模式下对未处理SystemExit直接退出。E_EOF即来自 Include/errcode.h。4.3 全局输入钩子PyOS_InputHookint (*PyOS_InputHook)(void);可设置为原型为int func(void)的函数指针。当解释器提示符即将空闲并等待终端用户输入时该函数会被调用返回值被忽略。覆盖此钩子可用于把解释器提示符接入其他事件循环Modules/_tkinter.c 就是这么做的。Python 3.12 起该函数只从主解释器main interpreter被调用。PyOS_ReadlineFunctionPointerchar* (*PyOS_ReadlineFunctionPointer)(FILE *, FILE *, const char *);可设置为原型为char *func(FILE *stdin, FILE *stdout, char *prompt)的函数覆盖解释器提示符处读取单行输入的默认函数。该函数应输出prompt若非NULL然后从给定标准输入文件读取一行并返回结果字符串。例如readline模块就设置此钩子以提供行编辑与 Tab 补全能力。返回值必须是PyMem_RawMalloc或PyMem_RawRealloc分配的字符串出错时为NULLPython 3.4 起要求用 Raw 系列而非PyMem_Malloc/PyMem_Realloc分配。Python 3.12 起同样只从主解释器被调用。五、指定环境执行与预编译PyRun_String* / PyRun_File* / Py_CompileString* / PyEval_*5.1 PyRun_StringFlags / PyRun_StringPyObject* PyRun_String(const char *str, int start, PyObject *globals, PyObject *locals); PyObject* PyRun_StringFlags(const char *str, int start, PyObject *globals, PyObject *locals, PyCompilerFlags *flags);PyRun_String是PyRun_StringFlags的简化接口flags为NULL。PyRun_StringFlags在globals/locals指定的上下文中执行str中的 Python 源码按flags指定的编译器标志编译。globals必须是字典locals可以是任何实现映射协议的对象。start指定起始符号必须是可用 start symbol 之一。返回执行结果的 Python 对象异常时返回NULL此时异常信息保留在解释器中与PyRun_SimpleStringFlags不同——后者打印异常前者把异常抛回给调用者处理。源码印证_PyRun_StringPython/pythonrun.c先用check_start校验起始符号创建 arena经_PyParser_ASTFromString解析出 AST最后交由run_mod完成编译与执行未提供名称时源码位置显示为string。5.2 PyRun_File 族PyObject* PyRun_File(FILE *fp, const char *filename, int start, PyObject *globals, PyObject *locals); PyObject* PyRun_FileEx(FILE *fp, const char *filename, int start, PyObject *globals, PyObject *locals, int closeit); PyObject* PyRun_FileFlags(FILE *fp, const char *filename, int start, PyObject *globals, PyObject *locals, PyCompilerFlags *flags); PyObject* PyRun_FileExFlags(FILE *fp, const char *filename, int start, PyObject *globals, PyObject *locals, int closeit, PyCompilerFlags *flags);前三个都是PyRun_FileExFlags的简化接口依次省略closeit/flags或再省略closeit。PyRun_FileExFlags与PyRun_StringFlags类似只是源码从fp读取filename应为文件名并从文件系统编码解码closeit为真时PyRun_FileExFlags返回前关闭文件。5.3 编译而不执行Py_CompileString 族PyObject* Py_CompileString(const char *str, const char *filename, int start); PyObject* Py_CompileStringFlags(const char *str, const char *filename, int start, PyCompilerFlags *flags); PyObject* Py_CompileStringObject(const char *str, PyObject *filename, int start, PyCompilerFlags *flags, int optimize); PyObject* Py_CompileStringExFlags(const char *str, const char *filename, int start, PyCompilerFlags *flags, int optimize);Py_CompileString是Py_CompileStringFlags的简化接口flags为NULLPy_CompileStringFlags是Py_CompileStringExFlags的简化接口optimize取-1Py_CompileStringExFlags3.2 加入与Py_CompileStringObject相同只是filename是从文件系统编码解码的字节串。Py_CompileStringObject3.4 加入解析并编译str中的源码返回代码对象code objectstart给出起始符号可用于约束可编译的代码filename用于构造代码对象可能出现在 traceback 或SyntaxError异常信息中代码无法解析或编译时返回NULL。optimize整型指定编译器优化级别-1选用解释器自身的优化级别由-O选项给出显式级别为0不优化__debug__为真、1移除 assert__debug__为假、2同时移除 docstring。源码印证Py_CompileStringObjectPython/pythonrun.c直接委托_Py_CompileString后者校验 start 后调用_PyParser_ASTFromString得到 AST若设置了PyCF_ONLY_AST标志则经_PyCompile_AstPreprocess预处理后用PyAST_mod2obj返回 AST 模块对象而非代码对象——这就是 Python 层compile(..., modeeval)及ast.parse的底层路径否则调用_PyAST_Compile产出PyCodeObject。5.4 求值代码对象PyEval_EvalCode / PyEval_EvalFrame*PyObject* PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals); PyObject* PyEval_EvalCodeEx(PyObject *co, PyObject *globals, PyObject *locals, PyObject *const *args, int argcount, PyObject *const *kws, int kwcount, PyObject *const *defs, int defcount, PyObject *kwdefs, PyObject *closure); PyObject* PyEval_EvalFrame(PyFrameObject *f); PyObject* PyEval_EvalFrameEx(PyFrameObject *f, int throwflag); int PyEval_MergeCompilerFlags(PyCompilerFlags *cf);PyEval_EvalCode是PyEval_EvalCodeEx的简化接口只带代码对象与全局/局部变量其余参数置NULLPyEval_EvalCodeEx在特定环境中求值已编译的代码对象。该环境包括全局变量字典、局部变量映射对象、参数与关键字参数数组、关键字-only 参数的默认值字典、以及单元格组成的闭包元组PyEval_EvalFrame求值一个执行帧是PyEval_EvalFrameEx为向后兼容提供的简化接口PyEval_EvalFrameEx是 Python 求值的核心裸函数执行与帧f关联的代码对象解释字节码并按需执行调用。额外的throwflag参数通常可以忽略——为真时立即抛出异常这是生成器对象throw()方法所用的机制。Python 3.4 起该函数包含一个调试断言帮助确保不会静默丢弃活动异常PyEval_MergeCompilerFlags修改当前求值帧的标志位成功返回 true失败返回 false。六、可用 start 符号start symbols常量用途Py_eval_inputPython 文法中孤立表达式的起始符号供Py_CompileString使用Py_file_input从文件或其他来源读取的语句序列的起始符号。编译任意长度 Python 源码时应用此符号Py_single_input单条语句的起始符号交互解释器循环所用Py_func_type_input3.8 加入函数类型的起始符号用于解析 PEP 484 的签名类型注释signature type comments。要求设置PyCF_ONLY_AST标志Py_func_type_input对应ast.FunctionTypeAST 节点。从源码看四个符号的有效性由check_start()统一把关Python/pythonrun.cPy_eval_input还要求源码是纯表达式不能含语句Py_single_input则允许隐式缩进块闭合——这正是交互 REPL 能逐行输入if块的原因。七、PyCompilerFlags 与编译器标志struct PyCompilerFlags { int cf_flags; int cf_feature_version; };PyCompilerFlags是编译器标志的载体结构。在只编译不执行的场景中它被拆散为int flags传递在执行代码的场景中以PyCompilerFlags *flags传递此时from __future__ import语句可以修改flags把所导入 future 特性的标志并入使同上下文中后续代码继承它。当PyCompilerFlags *flags为NULL时cf_flags视为0且from __future__ import引起的一切修改都会被丢弃。成员说明cf_flags编译器标志位cf_feature_versionPython 次版本号应初始化为PY_MINOR_VERSION3.8 加入该字段。默认情况下该字段被忽略当且仅当cf_flags中设置了PyCF_ONLY_AST时才生效。7.1 公共 PyCF 标志以下四个宏是公共编译器标志在ast模块文档compiler flags 一节中有说明ast模块以同名常量导出PyCF_ALLOW_TOP_LEVEL_AWAIT PyCF_ONLY_AST PyCF_OPTIMIZED_AST PyCF_TYPE_COMMENTSPyCF_ONLY_AST只解析/预编译出 AST不产出字节码compile()与ast.parse()的基础PyCF_OPTIMIZED_AST返回未优化的 AST源码层区分语法检查用 AST与优化 AST的开关PyCF_ALLOW_TOP_LEVEL_AWAIT允许顶层 awaitPyCF_TYPE_COMMENTS处理 PEP 484 类型注释。7.2 低层标志实现细节以下标志与掩码服务于标准库和交互解释器的狭窄需求库外代码几乎没有使用理由属于实现细节可能随时变更PyCF_ALLOW_INCOMPLETE_INPUT3.11 加入编译器与codeop模块之间的私有接口请勿使用其行为不受支持且可能无警告变更。设置该标志后当编译因源文本在预期还有更多输入处结束例如缩进块中部或未终止的字符串字面量而失败时抛出的错误是未公开文档的_IncompleteInputErrorSyntaxError的子类。codeop模块把它与PyCF_DONT_IMPLY_DEDENT一起设置用以区分输入不完整与真正的语法错误让交互解释器知道该继续提示用户输入下一行而不是报错。PyCF_DONT_IMPLY_DEDENT默认情况下用Py_single_input起始符号编译时到达源文本末尾会隐式关闭所有未闭合的缩进块。设置该标志后未闭合块仅当源文本最后一行以换行符结尾时才被关闭否则编译以SyntaxError失败。文档给出的示例PyCompilerFlags flags { .cf_flags 0, .cf_feature_version PY_MINOR_VERSION, }; const char *source if a:\n pass; /* if 块被隐式闭合返回代码对象 */ Py_CompileStringFlags(source, input, Py_single_input, flags); /* 设置标志后由于最后一行不以换行结尾 编译失败并抛 SyntaxError */ flags.cf_flags PyCF_DONT_IMPLY_DEDENT; Py_CompileStringFlags(source, input, Py_single_input, flags);codeop模块用这个标志检测不完整的交互输入当用户仍在缩进块内输入时源文本尚未以换行符结尾编译失败于是提示用户输入下一行。PyCF_IGNORE_COOKIE把源文本当作 UTF-8 读取忽略其中可能存在的 PEP 263 编码声明coding cookiePyCompilerFlags flags { .cf_flags 0, .cf_feature_version PY_MINOR_VERSION, }; const char *source # coding: latin-1\ns \xe9\n; /* 尊重 coding cookie0xE9 字节按 Latin-1 解码 返回一个把 s 设为 é 的代码对象 */ Py_CompileStringFlags(source, input, Py_file_input, flags); /* 设置标志后 cookie 被忽略且 0xE9 不是合法 UTF-8 编译失败并抛 SyntaxError */ flags.cf_flags PyCF_IGNORE_COOKIE; Py_CompileStringFlags(source, input, Py_file_input, flags);compile、eval、exec内建函数在源是str对象时会设置该标志因为它们把文本按 UTF-8 编码传给解析器。源码印证Python/pythonrun.c 的_Py_SourceAsString中PyUnicode_Check(cmd)为真时执行cf-cf_flags | PyCF_IGNORE_COOKIE与文档完全对应。PyCF_SOURCE_IS_UTF8标记源文本已知为 UTF-8 编码。compile、eval、exec会设置该标志但目前无实际效果。这些PyCF标志可以与CO_FUTURE标志如CO_FUTURE_ANNOTATIONS组合以启用通常通过 future 语句from __future__ import ...选择的特性。7.3 标志掩码PyCF_MASK所有CO_FUTURE标志见 C API 文档c_codeobject_flags一节的位掩码用于选择通常由 future 语句启用的特性。当以PyCompilerFlags *flags参数编译的代码包含from __future__ import语句时所导入特性的标志会被加入flags使同上下文中后续执行的代码继承该特性PyCF_MASK_OBSOLETE新代码勿用此掩码仅为让旧代码把标志传给compile时继续工作而保留。它是已废弃、不再产生任何效果的 future 特性标志的位掩码PyCF_COMPILE_MASK所有会改变源编译方式的PyCF标志如PyCF_ONLY_AST的位掩码。compile内建函数用此掩码校验其flags参数。八、字节码栈效应Stack Effects该节接口用于计算单条字节码指令对值栈的影响供工具如反汇编器验证栈平衡与dis.stack_effect相对应PY_INVALID_STACK_EFFECT /* 无效栈效应的哨兵值当前等于 INT_MAX3.8 加入 */ int PyCompile_OpcodeStackEffect(int opcode, int oparg); /* 3.4 加入。计算带参数 oparg 的 opcode 的栈效应 成功返回栈效应失败返回 PY_INVALID_STACK_EFFECT。 */ int PyCompile_OpcodeStackEffectWithJump(int opcode, int oparg, int jump); /* 3.8 加入。类似 PyCompile_OpcodeStackEffect但当 jump 为 0 时 不包含跳转本身的栈效应jump 为 0 时不计入 jump 为 1 或 -1 时计入。失败同样返回 PY_INVALID_STACK_EFFECT。 */九、选型与使用要点小结结合文档与源码实践中可以按以下规则选型只需像python -c一样跑一段代码用PyRun_SimpleStringFlags。它固定使用__main__命名空间异常会被自动打印且无法再获取异常对象且未处理SystemExit会退出进程PyConfig.inspect非零时除外——嵌入式场景需格外留意。需要拿到执行结果或异常对象、需要隔离命名空间用PyRun_StringFlagsPy_file_input/Py_eval_input返回值即结果对象失败时NULL并保留活动异常便于 C 层自行处理。执行脚本文件PyRun_SimpleFileExFlags若入口不确定是脚本还是交互输入等价python file用PyRun_AnyFileExFlags由_Py_FdIsInteractive自动分派。注意 Windows 上以二进制模式打开文件。自定义 REPLPyRun_InteractiveLoopFlags已提供提示符循环接入 GUI 事件循环设置PyOS_InputHook需要行编辑/补全时设置PyOS_ReadlineFunctionPointer返回值必须用PyMem_RawMalloc/PyMem_RawRealloc分配。两者自 3.12 起仅从主解释器回调。需要只取 AST / 区分输入是否完整Py_CompileStringObject加PyCF_ONLY_AST可配合Py_single_input与PyCF_DONT_IMPLY_DEDENT/PyCF_ALLOW_INCOMPLETE_INPUT如codeop模块所为文件名建议如实传入它直接出现在 traceback 中。参数初始化惯例自行构造PyCompilerFlags时.cf_feature_version初始化为PY_MINOR_VERSION如文档示例所示传NULL则等价于全 0 标志且 future 导入不生效。FILE*跨库传递风险只在确信调用方与 Python 运行时使用同一 C 运行时库时才传递FILE*Windows 上尤甚。所有函数的实现集中在 Python/pythonrun.c错误码定义在 Include/errcode.h注意它不被Python.h包含readline钩子的典型用法可参考 Modules/_tkinter.c 与Lib/readline.py相关的readline模块实现。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考