PEP 741 – Python 配置 C API
- 作者:
- Victor Stinner <vstinner at python.org>
- 讨论列表:
- Discourse 帖子
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2024年1月18日
- Python 版本:
- 3.14
- 历史记录:
- 2024年1月19日, 2024年2月8日
- 决议:
- Discourse 消息
摘要
添加一个 C API 来配置 Python 初始化,而无需依赖 C 结构,并能够在将来进行 ABI 兼容的更改。
通过添加 PyInitConfig_AddModule()
来完成 PEP 587 API,该 API 可用于添加内置扩展模块;先前称为“inittab”的功能。
添加 PyConfig_Get()
和 PyConfig_Set()
函数来获取和设置当前运行时配置。
PEP 587“Python 初始化配置”统一了所有配置 Python **初始化** 的方式。此 PEP 还将 Python **预初始化** 和 Python **初始化** 的配置统一到单个 API 中。此外,此 PEP 只提供了一种嵌入 Python 的选择,而不是像 PEP 587 中那样提供两种“Python”和“Isolated”选择,从而进一步简化了 API。
较低级别的 PEP 587 PyConfig
API 仍然可用于与 CPython 实现细节有意耦合更高的用例(例如模拟 CPython CLI 的全部功能,包括其配置机制)。
基本原理
获取运行时配置
PEP 587 没有 API 来 **获取** **当前** 运行时配置,只能 **配置** Python **初始化**。
例如,全局配置变量 Py_UnbufferedStdioFlag
在 Python 3.12 中已弃用,建议改为使用 PyConfig.buffered_stdio
。它仅适用于配置 Python,没有公共 API 来获取 PyConfig.buffered_stdio
。
有限 C API 的用户正在请求一个公共 API 来获取当前运行时配置。
Cython 需要获取 optimization_level
配置选项:问题。
当 2022 年全局配置变量被弃用时,Marc-André Lemburg 要求 一个 C API 在运行时访问这些配置变量(不仅仅是在 Python 初始化期间)。
安全修复
为了修复 CVE-2020-10735(将非常大的字符串转换为整数(以 10 为基数)时的拒绝服务),有人讨论过向稳定分支添加一个新的 PyConfig
成员,这会影响 ABI。
Gregory P. Smith 提出了一种使用基于文本的配置文件的不同 API,以不受 PyConfig
成员的限制:FR:允许私有运行时配置以启用扩展而不会破坏 PyConfig ABI(2022 年 8 月)。
最终,决定不向稳定分支添加新的 PyConfig
成员,而仅向开发分支添加新的 PyConfig.int_max_str_digits
成员(这成为了 Python 3.12)。稳定分支中使用一个专用的私有全局变量(与 PyConfig
无关)。
PyPreConfig 和 PyConfig 之间的冗余
Python 预初始化使用 PyPreConfig
结构,Python 初始化使用 PyConfig
结构。这两个结构都有四个重复的成员:dev_mode
、parse_argv
、isolated
和 use_environment
。
冗余是由这两个结构分离的事实造成的,而预初始化需要一些 PyConfig
成员。
嵌入 Python
嵌入 Python 的应用程序
示例
- Blender 3D 图形.
- fontforge 字体编辑器。
- Gimp.
- LibreOffice.
- OBS Studio.
- Tiled.
- vim 文本编辑器。
在 Linux、FreeBSD 和 macOS 上,应用程序通常要么静态链接到 libpython
,要么动态加载 libpython
。 libpython
共享库是带版本号的,例如:在 Linux 上的 Python 3.12 中为 libpython3.12.so
。
vim 项目可以针对稳定的 ABI。通常,使用“系统 Python”版本。目前无法选择使用哪个 Python 版本。用户希望能够根据需要选择更新的 Python。
在 Linux 上,部署嵌入 Python 的应用程序(如 GIMP)的另一种方法是将 Python 包含在 Flatpack、AppImage 或 Snap “容器”中。在这种情况下,应用程序会随容器一起自带 Python 版本的副本。
嵌入 Python 的库
示例
- Apache mod_wsgi (源代码)。
- nimpy:Nim - Python 桥。
- PyO3:Python 解释器的 Rust 绑定。
创建独立应用程序的实用程序
- py2app 用于 macOS。
- py2exe 用于 Windows。
- pyinstaller.
- PyOxidizer:它使用 PEP 587 PyConfig API。
这些实用程序创建独立应用程序,它们没有链接到 libpython。
设置运行时配置
Marc-André Lemburg 要求 一个 C API 来在运行时 **设置** 一些配置选项的值
optimization_level
verbose
parser_debug
inspect
write_bytecode
以前,可以直接设置全局配置变量
Py_OptimizeFlag
Py_VerboseFlag
Py_DebugFlag
Py_InspectFlag
Py_DontWriteBytecodeFlag
但这些配置标志在 Python 3.12 中已弃用,并计划在 Python 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()
.
C API 使用以 null 结尾的 UTF-8 编码字符串来引用配置选项名称。
这些 C API 函数不包含在有限的 C API 中。
PyInitConfig 结构
PyInitConfig
结构通过组合 PyConfig
API 的三个结构来实现,并且还有一个 inittab
成员
PyPreConfig preconfig
PyConfig config
PyStatus status
struct _inittab *inittab
用于PyInitConfig_AddModule()
PyStatus
状态不再单独存在,而是统一 PyInitConfig
结构的一部分,这使得 API 更易于使用。
配置选项
配置选项以 PyPreConfig
和 PyConfig
结构成员命名。请参阅 PyPreConfig 文档 和 PyConfig 文档。
弃用和删除配置选项不在 PEP 的范围内,应根据具体情况进行讨论。
公共配置选项
以下选项可以通过 PyConfig_Get()
获取,并可以通过 PyConfig_Set()
设置。
选项 | 类型 | 注释 |
---|---|---|
argv |
list[str] |
API:sys.argv 。 |
base_exec_prefix |
str |
API:sys.base_exec_prefix 。 |
base_executable |
str |
API:sys._base_executable 。 |
base_prefix |
str |
API:sys.base_prefix 。 |
bytes_warning |
int |
API: sys.flags.bytes_warning 。 |
exec_prefix |
str |
API: sys.exec_prefix 。 |
executable |
str |
API: sys.executable 。 |
inspect |
布尔值 |
API: sys.flags.inspect (int )。 |
int_max_str_digits |
int |
API: sys.flags.int_max_str_digits 、sys.get_int_max_str_digits() 和 sys.set_int_max_str_digits() 。 |
交互式 |
布尔值 |
API: sys.flags.interactive 。 |
模块搜索路径 |
list[str] |
API: sys.path 。 |
optimization_level |
int |
API: sys.flags.optimize 。 |
parser_debug |
布尔值 |
API: sys.flags.debug (int )。 |
platlibdir |
str |
API: sys.platlibdir 。 |
前缀 |
str |
API:sys.base_prefix 。 |
pycache_prefix |
str |
API: sys.pycache_prefix 。 |
静默 |
布尔值 |
API: sys.flags.quiet (int )。 |
stdlib_dir |
str |
API: sys._stdlib_dir 。 |
使用环境变量 |
布尔值 |
API: sys.flags.ignore_environment (int )。 |
verbose |
int |
API: sys.flags.verbose 。 |
warnoptions |
list[str] |
API: sys.warnoptions 。 |
write_bytecode |
布尔值 |
API: sys.flags.dont_write_bytecode (int ) 和 sys.dont_write_bytecode (bool )。 |
xoptions |
dict[str, str] |
API: sys._xoptions 。 |
某些选项名称与 sys
属性不同,例如 optimization_level
选项和 sys.flags.optimize
属性。 PyConfig_Set()
设置相应的 sys
属性。
xoptions
是 PyInitConfig
中的字符串列表,其中每个字符串的格式为 key
(value 隐式为 True
)或 key=value
。在当前运行时配置中,它变成了一个字典 (key: str
→ value: str | True
)。
只读配置选项
以下选项可以通过 PyConfig_Get()
获取,但不能通过 PyConfig_Set()
设置。
选项 | 类型 | 注释 |
---|---|---|
分配器 |
int |
|
缓冲的标准 I/O |
布尔值 |
|
check_hash_pycs_mode |
str |
|
code_debug_ranges |
布尔值 |
|
coerce_c_locale |
布尔值 |
|
coerce_c_locale_warn |
布尔值 |
|
configure_c_stdio |
布尔值 |
|
configure_locale |
布尔值 |
|
cpu_count |
int |
API: os.cpu_count() (int | None )。 |
开发模式 |
布尔值 |
API: sys.flags.dev_mode 。 |
dump_refs |
布尔值 |
|
dump_refs_file |
str |
|
faulthandler |
布尔值 |
API: faulthandler.is_enabled() 。 |
filesystem_encoding |
str |
API: sys.getfilesystemencoding() 。 |
filesystem_errors |
str |
API: sys.getfilesystemencodeerrors() 。 |
hash_seed |
int |
|
home |
str |
|
import_time |
布尔值 |
|
install_signal_handlers |
布尔值 |
|
隔离 |
布尔值 |
API: sys.flags.isolated (int )。 |
legacy_windows_fs_encoding |
布尔值 |
仅限 Windows。 |
legacy_windows_stdio |
布尔值 |
仅限 Windows。 |
malloc_stats |
布尔值 |
|
orig_argv |
list[str] |
API: sys.orig_argv 。 |
parse_argv |
布尔值 |
|
pathconfig_warnings |
布尔值 |
|
perf_profiling |
布尔值 |
API: sys.is_stack_trampoline_active() 。 |
程序名称 |
str |
|
run_command |
str |
|
run_filename |
str |
|
run_module |
str |
|
run_presite |
str |
需要调试版本。 |
safe_path |
布尔值 |
|
show_ref_count |
布尔值 |
|
site_import |
布尔值 |
API: sys.flags.no_site (int )。 |
skip_source_first_line |
布尔值 |
|
stdio_encoding |
str |
API: sys.stdin.encoding 、sys.stdout.encoding 和 sys.stderr.encoding 。 |
stdio_errors |
str |
API: sys.stdin.errors 、sys.stdout.errors 和 sys.stderr.errors 。 |
tracemalloc |
int |
API: tracemalloc.is_tracing() (bool )。 |
use_frozen_modules |
布尔值 |
|
use_hash_seed |
布尔值 |
|
user_site_directory |
布尔值 |
API: sys.flags.no_user_site (int )。 |
utf8_mode |
布尔值 |
|
warn_default_encoding |
布尔值 |
|
_pystats |
布尔值 |
API: sys._stats_on() 、sys._stats_off() 。需要 Py_STATS 构建。 |
创建配置
PyInitConfig
结构- 用于配置 Python 预初始化和 Python 初始化的不透明结构。
PyInitConfig* PyInitConfig_Create(void)
:- 使用 隔离配置 的默认值创建一个新的初始化配置。
必须使用
PyInitConfig_Free()
释放。内存分配失败时返回
NULL
。 void PyInitConfig_Free(PyInitConfig *config)
:- 释放初始化配置的内存。
获取选项
配置选项 name 参数必须是非空、以 null 结尾的 UTF-8 编码字符串。
int PyInitConfig_HasOption(PyInitConfig *config, const char *name)
:- 测试配置中是否包含名为 name 的选项。
如果选项存在,则返回
1
,否则返回0
。 int PyInitConfig_GetInt(PyInitConfig *config, const char *name, int64_t *value)
:- 获取整数配置选项。
- 设置 *value,成功时返回
0
。 - 在 config 中设置错误,错误时返回
-1
。
- 设置 *value,成功时返回
int PyInitConfig_GetStr(PyInitConfig *config, const char *name, char **value)
:- 将字符串配置选项作为以 null 结尾的 UTF-8 编码字符串获取。
- 设置 *value,成功时返回
0
。 - 在 config 中设置错误,错误时返回
-1
。
成功后,必须使用
free(value)
释放字符串。 - 设置 *value,成功时返回
int PyInitConfig_GetStrList(PyInitConfig *config, const char *name, size_t *length, char ***items)
:- 将字符串列表配置选项作为以 null 结尾的 UTF-8 编码字符串数组获取。
- 设置 *length 和 *value,成功时返回
0
。 - 在 config 中设置错误,错误时返回
-1
。
成功后,必须使用
PyInitConfig_FreeStrList(length, items)
释放字符串列表。 - 设置 *length 和 *value,成功时返回
void PyInitConfig_FreeStrList(size_t length, char **items)
:- 释放由
PyInitConfig_GetStrList()
创建的字符串列表的内存。
设置选项
配置选项 name 参数必须是非空、以 null 结尾的 UTF-8 编码字符串。
某些配置选项会对其他选项产生副作用。此逻辑仅在调用 Py_InitializeFromInitConfig()
时实现,而不是由下面的“Set”函数实现。例如,将 dev_mode
设置为 1
不会将 faulthandler
设置为 1
。
int PyInitConfig_SetInt(PyInitConfig *config, const char *name, int64_t value)
:- 设置整数配置选项。
- 成功时返回
0
。 - 在 config 中设置错误,错误时返回
-1
。
- 成功时返回
int PyInitConfig_SetStr(PyInitConfig *config, const char *name, const char *value)
:- 从以 null 结尾的 UTF-8 编码字符串设置字符串配置选项。字符串被复制。
- 成功时返回
0
。 - 在 config 中设置错误,错误时返回
-1
。
- 成功时返回
int PyInitConfig_SetStrList(PyInitConfig *config, const char *name, size_t length, char * const *items)
:- 从以 null 结尾的 UTF-8 编码字符串数组设置字符串列表配置选项。字符串列表被复制。
- 成功时返回
0
。 - 在 config 中设置错误,错误时返回
-1
。
- 成功时返回
int PyInitConfig_AddModule(PyInitConfig *config, const char *name, PyObject* (*initfunc)(void))
:- 将内置扩展模块添加到内置模块表中。
新模块可以通过名称 name 导入,并使用函数 initfunc 作为第一次尝试导入时调用的初始化函数。
- 成功时返回
0
。 - 在 config 中设置错误,错误时返回
-1
。
如果 Python 初始化多次,则必须在每次 Python 初始化时调用
PyInitConfig_AddModule()
。类似于
PyImport_AppendInittab()
函数。 - 成功时返回
初始化 Python
int Py_InitializeFromInitConfig(PyInitConfig *config)
:- 根据初始化配置初始化 Python。
- 成功时返回
0
。 - 在 config 中设置错误,错误时返回
-1
。 - 在config中设置退出代码,如果Python想要退出则返回
-1
。
有关退出代码的情况,请参见
PyInitConfig_GetExitcode()
。 - 成功时返回
错误处理
int PyInitConfig_GetError(PyInitConfig* config, const char **err_msg)
:- 获取config错误消息。
- 如果设置了错误,则设置*err_msg并返回
1
。 - 否则,将*err_msg设置为
NULL
并返回0
。
错误消息是UTF-8编码的字符串。
如果config具有退出代码,则将退出代码格式化为错误消息。
错误消息保持有效,直到使用config调用另一个
PyInitConfig
函数。调用者不必释放错误消息。 - 如果设置了错误,则设置*err_msg并返回
int PyInitConfig_GetExitcode(PyInitConfig* config, int *exitcode)
:- 获取config退出代码。
- 如果Python想要退出,则设置*exitcode并返回
1
。 - 如果config没有设置退出代码,则返回
0
。
只有
Py_InitializeFromInitConfig()
函数可以在parse_argv
选项非零的情况下设置退出代码。解析命令行失败(退出代码2)或命令行选项要求显示命令行帮助(退出代码0)时,可以设置退出代码。
- 如果Python想要退出,则设置*exitcode并返回
获取和设置运行时配置
配置选项 name 参数必须是非空、以 null 结尾的 UTF-8 编码字符串。
PyObject* PyConfig_Get(const char *name)
:- 获取配置选项的当前运行时值作为Python对象。
- 成功时返回一个新的引用。
- 错误时设置异常并返回
NULL
。
对象类型取决于选项:请参见配置选项表。
其他选项来自内部
PyPreConfig
和PyConfig
结构。调用者必须持有GIL。在Python初始化之前或Python结束之后不能调用此函数。
int PyConfig_GetInt(const char *name, int *value)
:- 类似于
PyConfig_Get()
,但将值作为整数获取。- 设置
*value
并返回0
表示成功。 - 错误时设置异常并返回
-1
。
- 设置
PyObject* PyConfig_Names(void)
:- 获取所有配置选项名称作为
frozenset
。错误时设置异常并返回
NULL
。调用者必须持有GIL。
PyObject* PyConfig_Set(const char *name, PyObject *value)
:- 设置配置选项的当前运行时值。
- 如果没有选项name,则引发
ValueError
。 - 如果value是无效值,则引发
ValueError
。 - 如果选项为只读:无法设置,则引发
ValueError
。 - 如果value没有正确的类型,则引发
TypeError
。
只读配置选项无法设置。
调用者必须持有GIL。在Python初始化之前或Python结束之后不能调用此函数。
- 如果没有选项name,则引发
稳定性
选项的行为、默认选项值和Python行为可能会在每个Python版本中发生变化:它们并不“稳定”。
此外,配置选项可以根据通常的PEP 387弃用流程添加、弃用和删除。
与 PyPreConfig 和 PyConfig API 的交互
更低级别的PEP 587 PyPreConfig
和PyConfig
API仍然可用并得到完全支持。如摘要中所述,对于旨在紧密模拟完整CPython CLI行为的嵌入用例,它们仍然是首选方法,而不是仅仅将Python运行时作为更大应用程序的一部分提供。
可以将PyPreConfig
API与此PEP中的初始化API结合使用。在这种情况下,预配置设置的只读与读/写限制也适用于PyInitConfig_SetInt
以及PyConfig_Set
,一旦解释器已预配置(具体来说,只能更新use_environment
,尝试更新任何其他预配置变量将报告错误)。
示例
初始化 Python
初始化Python的示例,设置各种类型的配置选项,错误时返回-1
int init_python(void)
{
PyInitConfig *config = PyInitConfig_Create();
if (config == NULL) {
printf("PYTHON INIT ERROR: memory allocation failed\n");
return -1;
}
// Set an integer (dev mode)
if (PyInitConfig_SetInt(config, "dev_mode", 1) < 0) {
goto error;
}
// Set a list of UTF-8 strings (argv)
char *argv[] = {"my_program", "-c", "pass"};
if (PyInitConfig_SetStrList(config, "argv",
Py_ARRAY_LENGTH(argv), argv) < 0) {
goto error;
}
// Set a UTF-8 string (program name)
if (PyInitConfig_SetStr(config, "program_name", L"my_program") < 0) {
goto error;
}
// Initialize Python with the configuration
if (Py_InitializeFromInitConfig(config) < 0) {
goto error;
}
PyInitConfig_Free(config);
return 0;
error:
// Display the error message
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) {
// Silently ignore the error
PyErr_Clear();
return -1;
}
return verbose;
}
发生错误时,函数会静默忽略错误并返回-1
。实际上,获取verbose
选项不会失败,除非未来的Python版本删除了该选项。
实现
向后兼容性
更改完全向后兼容。仅添加新API。
现有的API(例如PyConfig
C API(PEP 587))保持不变。
被拒绝的想法
配置为文本
有人提议以文本形式提供配置,以使API与稳定的ABI兼容并允许自定义选项。
示例
# integer
bytes_warning = 2
# string
filesystem_encoding = "utf8" # comment
# list of strings
argv = ['python', '-c', 'code']
API将配置作为字符串而不是文件来获取。假设的PyInit_SetConfig()
函数示例
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实际上不值得,而Python可以直接提供一个API来设置配置选项,其中值直接作为字符串或字符串数组传递。它避免了赋予某些字符(如换行符)特殊含义,这些字符需要转义。
使用整数引用选项
使用字符串引用配置选项需要比较字符串,这可能比比较整数慢。
使用整数,类似于类型“插槽”(例如Py_tp_doc
),来引用配置选项。const char *name
参数被int option
替换。
接受自定义选项在使用整数时更容易导致冲突,因为维护整数选项的“命名空间”(范围)更加困难。使用字符串,可以使用带冒号分隔符的简单前缀。
整数还需要维护整数常量列表,从而使C API和Python API更大。
Python 3.13只有大约62个配置选项,因此性能并不是真正的问题。如果以后需要更好的性能,可以使用哈希表根据名称获取选项。
如果在热代码中使用获取配置选项,则可以读取一次值并缓存。顺便说一句,大多数配置选项在运行时无法更改。
多阶段初始化(类似于 PEP 432)
Eric Snow表达了担忧,认为此提议可能会强化嵌入者认为初始化是单一整体步骤的想法。他认为初始化涉及5个不同的阶段,甚至建议API应该明确反映这一点。Eric建议,至少初始化的实现应该反映这些阶段,部分是为了提高代码健康状况。总的来说,他的解释与PEP 432和PEP 587有一些相似之处。
Eric的另一个与本PEP相关的要点是,理想情况下,传递给Py_InitializeFromConfig()
的config应该在调用该函数之前就已完成,而当前初始化实际上会修改config。
虽然Eric不一定建议使用PEP 741的替代方案,但任何围绕阶段添加粒度初始化API的提议实际上与本PEP试图实现的目标相反。此类API更加复杂,它需要添加新的公共结构和新的公共函数。它使Python初始化更加复杂,而不是本PEP试图统一现有API并使其更简单(相反)。具有多个用于类似目的的结构可能会导致成员重复,这与现有PyPreConfig
和PyConfig
结构之间的重复成员类似。
区域设置编码和宽字符串
接受编码为区域设置编码的字符串和在PyInitConfig
API中接受宽字符串(wchar_t*
)被推迟,以保持PyInitConfig
API简单并避免Python预初始化的复杂性。这些功能主要在模拟完整CPython CLI行为时需要,因此最好通过更低级别的PEP 587 API来提供。
讨论
- PEP 741:Python 配置 C API(第二版)(2024年2月)。
- PEP 741:Python 配置 C API(2024年1月)。
- FR:允许私有运行时配置以在不破坏PyConfig ABI的情况下启用扩展(2022年8月)。
版权
本文档放置在公有领域或根据CC0-1.0-Universal许可证,以较宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0741.rst