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 symbol | PyRun_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取NULL;PyRun_AnyFileFlags仅额外传入flags,closeit仍为0;PyRun_AnyFileEx仅额外传入closeit,flags为NULL;PyRun_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的简化接口(closeit=0、flags=NULL;或仅closeit为 0)。PyRun_SimpleFileExFlags与PyRun_SimpleStringFlags类似,只是源码从fp读取而非内存字符串;filename应为文件名,从文件系统编码与错误处理程序解码;closeit为真时函数返回前关闭文件。
Windows 特别注意(原文档明确警告):fp应以二进制模式打开(如fopen(filename, "rb")),否则 Python 可能无法正确处理使用 LF 行尾的脚本文件。
从源码看,PyRun_SimpleFileExFlags(Python/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为NULL);PyRun_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_InteractiveLoop(Python/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_InputHook:
int (*PyOS_InputHook)(void);可设置为原型为int func(void)的函数指针。当解释器提示符即将空闲并等待终端用户输入时,该函数会被调用,返回值被忽略。覆盖此钩子可用于把解释器提示符接入其他事件循环,Modules/_tkinter.c 就是这么做的。Python 3.12 起,该函数只从主解释器(main interpreter)被调用。
PyOS_ReadlineFunctionPointer:
char* (*PyOS_ReadlineFunctionPointer)(FILE *, FILE *, const char *);可设置为原型为char *func(FILE *stdin, FILE *stdout, char *prompt)的函数,覆盖解释器提示符处读取单行输入的默认函数。该函数应输出prompt(若非NULL),然后从给定标准输入文件读取一行并返回结果字符串。例如readline模块就设置此钩子以提供行编辑与 Tab 补全能力。返回值必须是PyMem_RawMalloc或PyMem_RawRealloc分配的字符串,出错时为NULL(Python 3.4 起要求用 Raw 系列而非PyMem_Malloc/PyMem_Realloc分配)。Python 3.12 起同样只从主解释器被调用。
五、指定环境执行与预编译:PyRun_String* / PyRun_File* / Py_CompileString* / PyEval_*
5.1 PyRun_StringFlags / PyRun_String
PyObject* 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_String(Python/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为NULL);Py_CompileStringFlags是Py_CompileStringExFlags的简化接口(optimize取-1);Py_CompileStringExFlags(3.2 加入)与Py_CompileStringObject相同,只是filename是从文件系统编码解码的字节串。
Py_CompileStringObject(3.4 加入)解析并编译str中的源码,返回代码对象(code object):start给出起始符号,可用于约束可编译的代码;filename用于构造代码对象,可能出现在 traceback 或SyntaxError异常信息中;代码无法解析或编译时返回NULL。
optimize整型指定编译器优化级别:-1选用解释器自身的优化级别(由-O选项给出);显式级别为0(不优化,__debug__为真)、1(移除 assert,__debug__为假)、2(同时移除 docstring)。
源码印证:Py_CompileStringObject(Python/pythonrun.c)直接委托_Py_CompileString,后者校验 start 后调用_PyParser_ASTFromString得到 AST;若设置了PyCF_ONLY_AST标志,则经_PyCompile_AstPreprocess预处理后用PyAST_mod2obj返回 AST 模块对象而非代码对象——这就是 Python 层compile(..., mode="eval")及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的简化接口,只带代码对象与全局/局部变量,其余参数置NULL;PyEval_EvalCodeEx在特定环境中求值已编译的代码对象。该环境包括:全局变量字典、局部变量映射对象、参数与关键字参数数组、关键字-only 参数的默认值字典、以及单元格组成的闭包元组;PyEval_EvalFrame求值一个执行帧,是PyEval_EvalFrameEx为向后兼容提供的简化接口;PyEval_EvalFrameEx是 Python 求值的核心裸函数:执行与帧f关联的代码对象,解释字节码并按需执行调用。额外的throwflag参数通常可以忽略——为真时立即抛出异常,这是生成器对象throw()方法所用的机制。Python 3.4 起该函数包含一个调试断言,帮助确保不会静默丢弃活动异常;PyEval_MergeCompilerFlags修改当前求值帧的标志位,成功返回 true,失败返回 false。
六、可用 start 符号(start symbols)
| 常量 | 用途 |
|---|---|
Py_eval_input | Python 文法中孤立表达式的起始符号,供Py_CompileString使用 |
Py_file_input | 从文件或其他来源读取的语句序列的起始符号。编译任意长度 Python 源码时应用此符号 |
Py_single_input | 单条语句的起始符号,交互解释器循环所用 |
Py_func_type_input(3.8 加入) | 函数类型的起始符号,用于解析 PEP 484 的"签名类型注释"(signature type comments)。要求设置PyCF_ONLY_AST标志 |
Py_func_type_input对应ast.FunctionTypeAST 节点。从源码看,四个符号的有效性由check_start()统一把关(Python/pythonrun.c),Py_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_version:Python 次版本号,应初始化为PY_MINOR_VERSION(3.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:允许顶层 await;PyCF_TYPE_COMMENTS:处理 PEP 484 类型注释。
7.2 低层标志(实现细节)
以下标志与掩码服务于标准库和交互解释器的狭窄需求,库外代码几乎没有使用理由,属于实现细节,可能随时变更:
PyCF_ALLOW_INCOMPLETE_INPUT(3.11 加入):编译器与codeop模块之间的私有接口,请勿使用,其行为不受支持且可能无警告变更。设置该标志后,当编译因源文本在预期还有更多输入处结束(例如缩进块中部或未终止的字符串字面量)而失败时,抛出的错误是未公开文档的_IncompleteInputError(SyntaxError的子类)。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 cookie"):
PyCompilerFlags flags = { .cf_flags = 0, .cf_feature_version = PY_MINOR_VERSION, }; const char *source = "# coding: latin-1\ns = '\xe9'\n"; /* 尊重 coding cookie:0xE9 字节按 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_MAX,3.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_StringFlags+Py_file_input/Py_eval_input,返回值即结果对象,失败时NULL并保留活动异常,便于 C 层自行处理。 - 执行脚本文件:
PyRun_SimpleFileExFlags;若入口不确定是脚本还是交互输入(等价python < file),用PyRun_AnyFileExFlags,由_Py_FdIsInteractive自动分派。注意 Windows 上以二进制模式打开文件。 - 自定义 REPL:
PyRun_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),仅供参考