Python - PEP 741 – Python 配置 C API
摘要
新增 C API,用于配置 Python 初始化,无需依赖 C 结构体,并支持未来 ABI 兼容性扩展。
完善 PEP 587 API,新增 PyInitConfig_AddModule(),可用于添加内建扩展模块(即原“inittab”功能)。
新增 PyConfig_Get() 和 PyConfig_Set() 函数用于获取和设置当前运行时配置。
PEP 587 统一了所有配置 Python 初始化 的方式。本 PEP 进一步统一 Python 预初始化和初始化的配置接口。此外,本 PEP仅提供一种嵌入 Python 的方式,不再区分“Python”和“隔离”两种模式(PEP 587 提供了两种),从而简化 API。
底层 PEP 587 的 PyConfig API 仍然保留,适用于需要与 CPython 实现高度耦合的场景(如完整模拟 CPython CLI 行为及其配置机制)。
原理
获取运行时配置
PEP 587 仅能设置初始化配置,不能获取当前运行时配置。
例如,Py_UnbufferedStdioFlag 全局配置变量在 Python 3.12 被弃用,推荐用 PyConfig.buffered_stdio,但只能用于初始化,无法在运行时通过公开 API 获取。
limited C API 用户希望有公开 API 可获取当前配置。
Cython 需要获取 optimization_level 配置项:相关 issue。
全局配置变量弃用后,Marc-André Lemburg 要求提供运行时访问这些变量的 C API。
安全修复
为修复 CVE-2020-10735(大整数转字符串拒绝服务),曾讨论向 stable 分支的 PyConfig 结构增加新成员,但这会影响 ABI。
Gregory P. Smith 提出用基于文本的配置文件 API,避免受限于 PyConfig 成员结构:相关讨论。
最终决定不在 stable 分支增加新成员,仅在开发分支(3.12)增加 PyConfig.int_max_str_digits,stable 分支用私有全局变量实现。
PyPreConfig 和 PyConfig 冗余
Python 预初始化用 PyPreConfig,初始化用 PyConfig,两者有四个重复成员:dev_mode、parse_argv、isolated 和 use_environment。
这是因为结构体分离,导致部分 PyConfig 成员在预初始化时就需用到。
嵌入 Python
嵌入 Python 的应用
典型应用:
Linux/FreeBSD/macOS 上,应用通常静态/动态链接 libpython,动态库如 libpython3.12.so。
vim 项目可面向 stable ABI,通常用系统 Python,无法选定具体版本,用户希望可动态指定更高版本。
Linux 上也可将 Python 随 Flatpack、AppImage、Snap 等容器一同分发(如 GIMP),即应用自带 Python 版本。
嵌入 Python 的库
典型库:
- Apache mod_wsgi (源码)
- nimpy:Nim-Python 桥
- PyO3:Rust-Python 绑定
打包工具生成独立应用
- py2app(macOS)
- py2exe(Windows)
- pyinstaller
- PyOxidizer(用 PEP 587 PyConfig API)
这些工具生成独立应用,不直接链接 libpython。
设置运行时配置
Marc-André Lemburg 要求提供运行时设置配置项的 C API:
optimization_levelverboseparser_debuginspectwrite_bytecode
之前可直接设置全局变量:
Py_OptimizeFlagPy_VerboseFlagPy_DebugFlagPy_InspectFlagPy_DontWriteBytecodeFlag
这些变量已在 3.12 弃用,计划 3.14 移除。
规范
新增 C API 函数和结构体用于配置 Python 初始化:
- 创建配置:
PyInitConfig不透明结构PyInitConfig_Create()PyInitConfig_Free(config)
- 获取选项:
PyInitConfig_HasOption(config, name)PyInitConfig_GetInt(config, name, &value)PyInitConfig_GetStr(config, name, &value)PyInitConfig_GetStrList(config, name, &length, &items)PyInitConfig_FreeStrList()
- 设置选项:
PyInitConfig_SetInt(config, name, value)PyInitConfig_SetStr(config, name, value)PyInitConfig_SetStrList(config, name, length, items)PyInitConfig_AddModule(config, name, initfunc)
- 初始化:
Py_InitializeFromInitConfig(config)
- 错误处理:
PyInitConfig_GetError(config, &err_msg)PyInitConfig_GetExitcode(config, &exitcode)
新增运行时获取/设置配置的 C API:
PyConfig_Get(name)PyConfig_GetInt(name, &value)PyConfig_Set(name)PyConfig_Names()
API 使用 UTF-8 的 null 结尾字符串表示配置项名。
这些 API 不包含在 limited C API 中。
PyInitConfig 结构体
PyInitConfig 结合了 PyConfig API 的三部分结构,并包含 inittab 成员:
PyPreConfig preconfigPyConfig configPyStatus statusstruct _inittab *inittab(用于PyInitConfig_AddModule())
PyStatus 不再独立,而并入 PyInitConfig,简化 API。
配置选项
配置项名称与 PyPreConfig 和 PyConfig 成员一致。详见 PyPreConfig 文档 和 PyConfig 文档。
弃用/移除配置项不在本 PEP 范围,应具体讨论。
可读写的配置项
下列选项可用 PyConfig_Get() 获取、PyConfig_Set() 设置:
| 选项 | 类型 | 说明 |
|---|---|---|
argv | list[str] | sys.argv |
base_exec_prefix | str | sys.base_exec_prefix |
base_executable | str | sys._base_executable |
base_prefix | str | sys.base_prefix |
bytes_warning | int | sys.flags.bytes_warning |
exec_prefix | str | sys.exec_prefix |
executable | str | sys.executable |
inspect | bool | sys.flags.inspect (int) |
int_max_str_digits | int | sys.flags.int_max_str_digits, sys.get_int_max_str_digits(), sys.set_int_max_str_digits() |
interactive | bool | sys.flags.interactive |
module_search_paths | list[str] | sys.path |
optimization_level | int | sys.flags.optimize |
parser_debug | bool | sys.flags.debug (int) |
platlibdir | str | sys.platlibdir |
prefix | str | sys.base_prefix |
pycache_prefix | str | sys.pycache_prefix |
quiet | bool | sys.flags.quiet (int) |
stdlib_dir | str | sys._stdlib_dir |
use_environment | bool | sys.flags.ignore_environment (int) |
verbose | int | sys.flags.verbose |
warnoptions | list[str] | sys.warnoptions |
write_bytecode | bool | sys.flags.dont_write_bytecode (int), sys.dont_write_bytecode (bool) |
xoptions | dict[str, str] | sys._xoptions |
部分选项名与 sys 属性不同,如 optimization_level 对应 sys.flags.optimize。PyConfig_Set() 实际设置相关 sys 属性。
xoptions 在 PyInitConfig 中为字符串列表,每项为 key(值隐含为 True)或 key=value。运行时配置则为字典(key: str -> value: str | True)。
只读配置项
下列选项仅可用 PyConfig_Get() 获取,不可用 PyConfig_Set() 设置:
| 选项 | 类型 | 说明 |
|---|---|---|
allocator | int | |
buffered_stdio | bool | |
check_hash_pycs_mode | str | |
code_debug_ranges | bool | |
coerce_c_locale | bool | |
coerce_c_locale_warn | bool | |
configure_c_stdio | bool | |
configure_locale | bool | |
cpu_count | int | os.cpu_count() |
dev_mode | bool | sys.flags.dev_mode |
dump_refs | bool | |
dump_refs_file | str | |
faulthandler | bool | faulthandler.is_enabled() |
filesystem_encoding | str | sys.getfilesystemencoding() |
filesystem_errors | str | sys.getfilesystemencodeerrors() |
hash_seed | int | |
home | str | |
import_time | bool | |
install_signal_handlers | bool | |
isolated | bool | sys.flags.isolated (int) |
legacy_windows_fs_encoding | bool | 仅 Windows |
legacy_windows_stdio | bool | 仅 Windows |
malloc_stats | bool | |
orig_argv | list[str] | sys.orig_argv |
parse_argv | bool | |
pathconfig_warnings | bool | |
perf_profiling | bool | sys.is_stack_trampoline_active() |
program_name | str | |
run_command | str | |
run_filename | str | |
run_module | str | |
run_presite | str | 需 debug 构建 |
safe_path | bool | |
show_ref_count | bool | |
site_import | bool | sys.flags.no_site (int) |
skip_source_first_line | bool | |
stdio_encoding | str | sys.stdin.encoding 等 |
stdio_errors | str | sys.stdin.errors 等 |
tracemalloc | int | tracemalloc.is_tracing() |
use_frozen_modules | bool | |
use_hash_seed | bool | |
user_site_directory | bool | sys.flags.no_user_site (int) |
utf8_mode | bool | |
warn_default_encoding | bool | |
_pystats | bool | sys._stats_on(), 需 Py_STATS 构建 |
创建配置
PyInitConfig* PyInitConfig_Create(void):
创建带 Isolated Configuration 默认值的新配置,需用 PyInitConfig_Free() 释放。分配失败返回 NULL。
void PyInitConfig_Free(PyInitConfig *config):
释放配置内存。
获取选项
配置项名需为非空、null 结尾 UTF-8 字符串。
int PyInitConfig_HasOption(PyInitConfig *config, const char *name):
若有该项返回 1,否则 0。
int PyInitConfig_GetInt(PyInitConfig *config, const char *name, int64_t *value):
获取整数配置项,成功设 *value 并返回 0,失败设错误信息并返回 -1。
int PyInitConfig_GetStr(PyInitConfig *config, const char *name, char **value):
获取字符串配置项,成功设 *value 并返回 0,失败设错误并返回 -1。需用 free(value) 释放。
int PyInitConfig_GetStrList(PyInitConfig *config, const char *name, size_t *length, char ***items):
获取字符串列表配置项,成功设 *length, *items 并返回 0,失败设错误并返回 -1。需用 PyInitConfig_FreeStrList(length, items) 释放。
void PyInitConfig_FreeStrList(size_t length, char **items):
释放字符串列表内存。
设置选项
配置项名同上。
部分项设置会影响其它项,此逻辑仅在 Py_InitializeFromInitConfig() 内实现,非“Set”函数。
int PyInitConfig_SetInt(PyInitConfig *config, const char *name, int64_t value):
设置整数配置项,成功返回 0,失败设错误并返回 -1。
int PyInitConfig_SetStr(PyInitConfig *config, const char *name, const char *value):
设置字符串配置项(UTF-8),复制字符串,成功返回 0,失败设错误并返回 -1。
int PyInitConfig_SetStrList(PyInitConfig *config, const char *name, size_t length, char * const *items):
设置字符串列表配置项,复制列表,成功返回 0,失败设错误并返回 -1。
int PyInitConfig_AddModule(PyInitConfig *config, const char *name, PyObject* (*initfunc)(void)):
添加内建扩展模块到内建模块表。首次 import 时调用 initfunc。多次初始化时,每次都需调用。
类似 PyImport_AppendInittab()。
初始化 Python
int Py_InitializeFromInitConfig(PyInitConfig *config):
用配置初始化 Python,成功返回 0,失败设错误或退出码并返回 -1。
见 PyInitConfig_GetExitcode()。
错误处理
int PyInitConfig_GetError(PyInitConfig* config, const char **err_msg):
获取 config 错误信息,若有错误,设 *err_msg 并返回 1,否则设 *err_msg 为 NULL 并返回 0。错误信息为 UTF-8 字符串,调用其它 PyInitConfig 函数前有效,无需手动释放。
int PyInitConfig_GetExitcode(PyInitConfig* config, int *exitcode):
获取 config 退出码,若需退出,设 *exitcode 并返回 1,否则返回 0。仅 Py_InitializeFromInitConfig() 可设退出码(如命令行解析失败或请求显示帮助)。
运行时获取/设置配置
配置项名同上。
PyObject* PyConfig_Get(const char *name):
获取当前运行时配置项值,类型见上表。需持有 GIL,初始化前后不可用。
int PyConfig_GetInt(const char *name, int *value):
同上,获取整型值,成功设 *value 并返回 0,失败抛异常返回 -1。
PyObject* PyConfig_Names(void):
获取所有配置项名,类型为 frozenset。需持有 GIL。
PyObject* PyConfig_Set(const char *name, PyObject *value):
设置当前运行时配置项。若项不存在、值非法、只读或类型不符,均抛 ValueError/TypeError。只读项不可设置。需持有 GIL,初始化前后不可用。
稳定性
各选项及默认值行为每个 Python 版本可能变化,不保证“稳定”。选项可按 PEP 387 流程增删弃用。
与 PyPreConfig 和 PyConfig API 交互
底层 PEP 587 的 PyPreConfig 和 PyConfig API 保留,适用于高度模拟 CPython CLI 行为的场景。
PyPreConfig API 可与本 PEP 初始化 API 联用。已预初始化解释器后,PyInitConfig_SetInt 和 PyConfig_Set 仅允许更新 use_environment,其他预初始化项不可更改,违者报错。
示例
初始化 Python
初始化并设置多种类型配置项,错误返回 -1:
int init_python(void)
{
PyInitConfig *config = PyInitConfig_Create();
if (config == NULL) {
printf("PYTHON INIT ERROR: memory allocation failed\n");
return -1;
}
// 设置整数(dev mode)
if (PyInitConfig_SetInt(config, "dev_mode", 1) < 0) {
goto error;
}
// 设置字符串列表(argv)
char *argv[] = {"my_program", "-c", "pass"};
if (PyInitConfig_SetStrList(config, "argv", Py_ARRAY_LENGTH(argv), argv) < 0) {
goto error;
}
// 设置字符串(program name)
if (PyInitConfig_SetStr(config, "program_name", L"my_program") < 0) {
goto error;
}
// 用配置初始化 Python
if (Py_InitializeFromInitConfig(config) < 0) {
goto error;
}
PyInitConfig_Free(config);
return 0;
error:
// 显示错误信息
const char *err_msg;
(void)PyInitConfig_GetError(config, &err_msg);
printf("PYTHON INIT ERROR: %s\n", err_msg);
PyInitConfig_Free(config);
return -1;
}
增加初始化 bytes_warning 选项
递增初始化配置的 bytes_warning 选项:
int config_bytes_warning(PyInitConfig *config)
{
int64_t bytes_warning;
if (PyInitConfig_GetInt(config, "bytes_warning", &bytes_warning)) {
return -1;
}
bytes_warning += 1;
if (PyInitConfig_SetInt(config, "bytes_warning", bytes_warning)) {
return -1;
}
return 0;
}
获取运行时 verbose 选项
获取当前运行时配置的 verbose 选项:
int get_verbose(void)
{
int verbose;
if (PyConfig_GetInt("verbose", &verbose) < 0) {
// 忽略错误
PyErr_Clear();
return -1;
}
return verbose;
}
实际中,获取 verbose 基本不会失败,除非未来移除该选项。
实现
- 议题: [C API] PEP 741: Add PyInitConfig C API to customize the Python initialization
- PR: Add PyConfig_Get() function
- PR: Add PyInitConfig C API
向后兼容性
完全向后兼容,仅新增 API,原有如 PyConfig API (PEP 587) 保持不变。
被拒绝方案
文本配置
建议用文本配置兼容 stable ABI 并支持自定义项,如:
bytes_warning = 2
filesystem_encoding = "utf8"
argv = ['python', '-c', 'code']
API 以字符串(非文件)形式传递配置:
void stable_abi_init_demo(int set_path)
{
PyInit_SetConfig(
"isolated = 1\n"
"argv = ['python', '-c', 'code']\n"
"filesystem_encoding = 'utf-8'\n"
);
if (set_path) {
PyInit_SetConfig("pythonpath = '/my/path'");
}
}
但这样需要给字符串加引号并转义,字符串数组格式化也不便。直接传递字符串/数组的 API 更简单,也避免了特殊字符(如换行)的歧义。
用整数引用配置项
用字符串做配置项名查找慢,可用整数(类似 type “slot”),如 Py_tp_doc。但定制选项容易冲突,需维护命名空间。且需要大量常量,增加 API 体积。当前配置项仅 62 个,性能非瓶颈,必要时可用哈希查找。热路径下可缓存配置值,大部分项运行时不能变。
多阶段初始化(类似 PEP 432)
Eric Snow 认为初始化应分为 5 个阶段,API 也应体现。PEP 432/587 类似。理想状态下,Py_InitializeFromConfig() 前配置应完整,实际初始化过程中配置会被修改。
但分阶段 API 更复杂,需新增结构和函数,反而加剧复杂性。多结构易致成员重复。本 PEP目标是统一简化现有 API。
本地编码与宽字符串
本地编码字符串和宽字符(wchar_t*)支持被延后,保持 PyInitConfig API 简洁。此类特性主要用于完整模拟 CPython CLI,适合用 PEP 587 的低层 API。
讨论记录
- PEP 741: Python Configuration C API (second version)
- PEP 741: Python Configuration C API
- FR: Allow private runtime config to enable extending without breaking the PyConfig ABI
版权
本文档可置于公有领域或 CC0-1.0-Universal 许可下,以更宽松者为准。
原文地址:https://peps.python.org/pep-0741/
1049

被折叠的 条评论
为什么被折叠?



