From 2022d23aab8d3892f9544508e3d5af9d9a5c86b2 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sun, 1 Oct 2023 13:25:07 +0200 Subject: [PATCH] gh-107954: Add PyInitConfig C API Add PyInitConfig functions: * PyInitConfig_Python_New() * PyInitConfig_Isolated_New() * PyInitConfig_Free(config) * PyInitConfig_SetInt(config, key, value) * PyInitConfig_SetStr(config, key, value) * PyInitConfig_SetWStr(config, key, value) * PyInitConfig_SetStrList(config, key, length, items) * PyInitConfig_SetWStrList(config, key, length, items) * PyInitConfig_Exception(config) * PyInitConfig_GetError(config, &err_msg) * PyInitConfig_GetExitCode(config, &exitcode) Add also functions using it: * Py_InitializeFromInitConfig(config) * Py_ExitWithInitConfig(config) Add these functions to the limited C API. Changes: * Add Doc/c-api/config.rst. * Add Include/initconfig.h header. --- Doc/c-api/config.rst | 188 +++++++++++ Doc/c-api/index.rst | 1 + Doc/c-api/init.rst | 3 +- Doc/c-api/init_config.rst | 3 +- Doc/data/stable_abi.dat | 13 + Doc/whatsnew/3.13.rst | 15 + Include/Python.h | 2 +- Include/cpython/initconfig.h | 81 ++++- Include/initconfig.h | 16 + Include/internal/pycore_initconfig.h | 4 + Lib/test/test_embed.py | 42 ++- Lib/test/test_stable_abi_ctypes.py | 13 + ...-11-30-23-19-59.gh-issue-107954.7rFtEo.rst | 17 + PC/python3dll.c | 13 + Programs/_testembed.c | 87 +++++ Python/initconfig.c | 303 +++++++++++++++++- Python/preconfig.c | 52 +-- 17 files changed, 802 insertions(+), 51 deletions(-) create mode 100644 Doc/c-api/config.rst create mode 100644 Include/initconfig.h create mode 100644 Misc/NEWS.d/next/C API/2023-11-30-23-19-59.gh-issue-107954.7rFtEo.rst diff --git a/Doc/c-api/config.rst b/Doc/c-api/config.rst new file mode 100644 index 00000000000000..8fb7e9486c6b3b --- /dev/null +++ b/Doc/c-api/config.rst @@ -0,0 +1,188 @@ +.. highlight:: c + +.. _config-c-api: + +******************** +Python Configuration +******************** + +.. versionadded:: 3.13 + +API part of the limited C API version 3.13 to configure the Python +initialization and get the Python runtime configuration. + +See also :ref:`Python Initialization Configuration `. + + +Initialize Python +================= + +Configuration to customize Python initialization. Configuration option names +are names of a :c:type:`PyConfig` members. + +.. c:struct:: PyInitConfig + + Opaque structure to configure the Python initialization. + + +.. c:function:: PyInitConfig* PyInitConfig_Python_New(void) + + Create a new initialization configuration using :ref:`Python Configuration + ` default values. + + It must be freed by :c:func:`PyInitConfig_Free`. + + Return ``NULL`` on memory allocation failure. + + +.. c:function:: PyInitConfig* PyInitConfig_Isolated_New(void) + + Similar to :c:func:`PyInitConfig_Python_New`, but use :ref:`Isolated + Configuration ` default values. + + +.. c:function:: void PyInitConfig_Free(PyInitConfig *config) + + Free memory of an initialization configuration. + + +.. c:function:: int PyInitConfig_SetInt(PyInitConfig *config, const char *name, int64_t value) + + Set an integer configuration option. + + * Return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + + +.. c:function:: int PyInitConfig_SetStr(PyInitConfig *config, const char *name, const char *value) + + Set a string configuration option from a null-terminated bytes string. + + The bytes string is decoded by :c:func:`Py_DecodeLocale`. If Python is not + yet preinitialized, this function preinitializes it to ensure that encodings + are properly configured. + + * Return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + + +.. c:function:: int PyInitConfig_SetWStr(PyInitConfig *config, const char *name, const wchar_t *value) + + Set a string configuration option from a null-terminated wide string. + + If Python is not yet preinitialized, this function preinitializes it. + + * Return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + + +.. c:function:: int PyInitConfig_SetStrList(PyInitConfig *config, const char *name, size_t length, char * const *items) + + Set a string list configuration option from an array of null-terminated + bytes strings. + + The bytes string is decoded by :c:func:`Py_DecodeLocale`. If Python is not + yet preinitialized, this function preinitializes it to ensure that encodings + are properly configured. + + * Return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + + +.. c:function:: int PyInitConfig_SetWStrList(PyInitConfig *config, const char *name, size_t length, wchar_t * const *items) + + Set a string list configuration option from a null-terminated wide strings. + + If Python is not yet preinitialized, this function preinitializes it. + + * Return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + + +.. c:function:: int Py_InitializeFromInitConfig(PyInitConfig *config) + + Initialize Python from the initialization configuration. + + * Return ``0`` on success. + * Return ``-1`` if an error was set or if an exit code was set. + + +Error handling +============== + +.. c:function:: int PyInitConfig_Exception(PyInitConfig* config) + + Check if an exception is set in *config*: + + * Return non-zero if an error was set or if an exit code was set. + * Return zero otherwise. + + +.. c:function:: int PyInitConfig_GetError(PyInitConfig* config, const char **err_msg) + + Get the *config* error message. + + * Set *\*err_msg* and return ``1`` if an error is set. + * Set *\*err_msg* to ``NULL`` and return ``0`` otherwise. + + +.. c:function:: int PyInitConfig_GetExitCode(PyInitConfig* config, int *exitcode) + + Get the *config* exit code. + + * Set *\*exitcode* and return ``1`` if an exit code is set. + * Return ``0`` otherwise. + + +.. c:function:: void Py_ExitWithInitConfig(PyInitConfig *config) + + Exit Python and free memory of a initialization configuration. + + If an error message is set, display the error message. + + If an exit code is set, use it to exit the process. + + The function does not return. + + +Example +------- + +Code:: + + void init_python(void) + { + PyInitConfig *config = PyInitConfig_Python_New(); + if (config == NULL) { + printf("Init allocation error\n"); + return; + } + + if (PyInitConfig_SetInt(config, "dev_mode", 1) < 0) { + goto error; + } + + // Set a list of wide strings (argv) + wchar_t *argv[] = {L"my_program"", L"-c", L"pass"}; + if (PyInitConfig_SetWStrList(config, "argv", + Py_ARRAY_LENGTH(argv), argv) < 0) { + goto error; + } + + // Set a wide string (program_name) + if (PyInitConfig_SetWStr(config, "program_name", L"my_program") < 0) { + goto error; + } + + // Set a list of bytes strings (xoptions) + char* xoptions[] = {"faulthandler"}; + if (PyInitConfig_SetStrList(config, "xoptions", + Py_ARRAY_LENGTH(xoptions), xoptions) < 0) { + goto error; + } + + if (Py_InitializeFromInitConfig(config) < 0) { + Py_ExitWithInitConfig(config); + } + PyInitConfig_Free(config); + } diff --git a/Doc/c-api/index.rst b/Doc/c-api/index.rst index ba56b03c6ac8e7..fa2ada706072aa 100644 --- a/Doc/c-api/index.rst +++ b/Doc/c-api/index.rst @@ -22,6 +22,7 @@ document the API functions in detail. concrete.rst init.rst init_config.rst + config.rst memory.rst objimpl.rst apiabiversion.rst diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 9e118d4f36145f..ec45c1270b92e5 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -7,7 +7,8 @@ Initialization, Finalization, and Threads ***************************************** -See also :ref:`Python Initialization Configuration `. +See also the :ref:`Python Initialization Configuration ` +and the :ref:`Python Configuration ` .. _pre-init-safe: diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index 5195f6cccfe9df..1e11b64be44949 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -27,7 +27,8 @@ There are two kinds of configuration: The :c:func:`Py_RunMain` function can be used to write a customized Python program. -See also :ref:`Initialization, Finalization, and Threads `. +See also :ref:`Initialization, Finalization, and Threads ` +and the :ref:`Python Configuration `. .. seealso:: :pep:`587` "Python Initialization Configuration". diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 76a035f194d911..62b2df79224147 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -327,6 +327,17 @@ function,PyImport_ImportModuleLevelObject,3.7,, function,PyImport_ImportModuleNoBlock,3.2,, function,PyImport_ReloadModule,3.2,, function,PyIndex_Check,3.8,, +function,PyInitConfig_Exception,3.13,, +function,PyInitConfig_Free,3.13,, +function,PyInitConfig_GetError,3.13,, +function,PyInitConfig_GetExitCode,3.13,, +function,PyInitConfig_Isolated_New,3.13,, +function,PyInitConfig_Python_New,3.13,, +function,PyInitConfig_SetInt,3.13,, +function,PyInitConfig_SetStr,3.13,, +function,PyInitConfig_SetStrList,3.13,, +function,PyInitConfig_SetWStr,3.13,, +function,PyInitConfig_SetWStrList,3.13,, type,PyInterpreterState,3.2,,opaque function,PyInterpreterState_Clear,3.2,, function,PyInterpreterState_Delete,3.2,, @@ -836,6 +847,7 @@ function,Py_EncodeLocale,3.7,, function,Py_EndInterpreter,3.2,, function,Py_EnterRecursiveCall,3.9,, function,Py_Exit,3.2,, +function,Py_ExitWithInitConfig,3.13,, function,Py_FatalError,3.2,, var,Py_FileSystemDefaultEncodeErrors,3.10,, var,Py_FileSystemDefaultEncoding,3.2,, @@ -861,6 +873,7 @@ var,Py_HasFileSystemDefaultEncoding,3.2,, function,Py_IncRef,3.2,, function,Py_Initialize,3.2,, function,Py_InitializeEx,3.2,, +function,Py_InitializeFromInitConfig,3.13,, function,Py_Is,3.10,, function,Py_IsFalse,3.10,, function,Py_IsFinalizing,3.13,, diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 7edfdd4f8167a0..9e033f9f8da9fc 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -2128,6 +2128,21 @@ New Features destruction the same way the :mod:`tracemalloc` module does. (Contributed by Pablo Galindo in :gh:`93502`.) +* Add functions to the limited C API to configure the Python initialization: + + * :c:func:`PyInitConfig_Python_New` + * :c:func:`PyInitConfig_Isolated_New` + * :c:func:`PyInitConfig_Free` + * :c:func:`PyInitConfig_SetInt` + * :c:func:`PyInitConfig_SetStr` + * :c:func:`PyInitConfig_SetStrList` + * :c:func:`PyInitConfig_Exception` + * :c:func:`PyInitConfig_GetError` + * :c:func:`PyInitConfig_GetExitCode` + * :c:func:`Py_InitializeFromInitConfig` + * :c:func:`Py_ExitWithInitConfig` + + (Contributed by Victor Stinner in :gh:`107954`.) Build Changes ============= diff --git a/Include/Python.h b/Include/Python.h index e05901b9e52b5a..78bdc8bbc0b586 100644 --- a/Include/Python.h +++ b/Include/Python.h @@ -95,7 +95,7 @@ #include "sliceobject.h" #include "cpython/cellobject.h" #include "iterobject.h" -#include "cpython/initconfig.h" +#include "initconfig.h" #include "pystate.h" #include "cpython/genobject.h" #include "descrobject.h" diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index 5da5ef9e5431b1..cfa1fb1c3363be 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -1,8 +1,5 @@ -#ifndef Py_PYCORECONFIG_H -#define Py_PYCORECONFIG_H -#ifndef Py_LIMITED_API -#ifdef __cplusplus -extern "C" { +#ifndef Py_CPYTHON_INITCONFIG_H +# error "this header file must not be included directly" #endif /* --- PyStatus ----------------------------------------------- */ @@ -267,8 +264,72 @@ PyAPI_FUNC(PyStatus) PyConfig_SetWideStringList(PyConfig *config, See also PyConfig.orig_argv. */ PyAPI_FUNC(void) Py_GetArgcArgv(int *argc, wchar_t ***argv); -#ifdef __cplusplus -} -#endif -#endif /* !Py_LIMITED_API */ -#endif /* !Py_PYCORECONFIG_H */ + +// --- PyInitConfig --------------------------------------------------------- + +typedef struct PyInitConfig PyInitConfig; + +// Create a new initialization configuration. +// It must be freed by PyInitConfig_Free(). +// Return NULL on memory allocation failure. +// +// PyInitConfig_Python_New() has a Python configuration by default. +// PyInitConfig_Isolated_New() has an Isolated configuration by default. +PyAPI_FUNC(PyInitConfig*) PyInitConfig_Python_New(void); +PyAPI_FUNC(PyInitConfig*) PyInitConfig_Isolated_New(void); + +// Free memory of a initialization configuration. +PyAPI_FUNC(void) PyInitConfig_Free(PyInitConfig *config); + +// Set an integer configuration option. +// Return 0 on success, or return -1 on error. +PyAPI_FUNC(int) PyInitConfig_SetInt( + PyInitConfig *config, + const char *name, + int64_t value); + +// Set a string configuration option from a bytes string. +// +// The bytes string is decoded by Py_DecodeLocale(). Preinitialize Python if +// needed to ensure that encodings are properly configured. +// +// Return 0 on success, or return -1 on error. +PyAPI_FUNC(int) PyInitConfig_SetStr( + PyInitConfig *config, + const char *name, + const char *value); + +// Set a string list configuration option from bytes strings. +// +// The bytes strings are decoded by Py_DecodeLocale(). Preinitialize Python if +// needed to ensure that encodings are properly configured. +// +// Return 0 on success, or return -1 on error. +PyAPI_FUNC(int) PyInitConfig_SetStrList( + PyInitConfig *config, + const char *name, + size_t length, + char * const *items); + +// Initialize Python from the initialization configuration. +// Return 0 on success. +// Return -1 if Python wants to exit and on error +PyAPI_FUNC(int) Py_InitializeFromInitConfig(PyInitConfig *config); + +// Return non-zero if an error was set or if an exit code was set. +// Return zero otherwise. +PyAPI_FUNC(int) PyInitConfig_Exception(PyInitConfig* config); + +// Get the error message. +// Set *err_msg and return 1 if an error is set. +// Set *err_msg to NULL and return 0 otherwise. +PyAPI_FUNC(int) PyInitConfig_GetError(PyInitConfig* config, const char **err_msg); + +// Get the exit code. +// Set '*exitcode' and return 1 if an exit code is set. +// Return 0 otherwise. +PyAPI_FUNC(int) PyInitConfig_GetExitCode(PyInitConfig* config, int *exitcode); + +// Exit Python and free memory of a initialization configuration. +// The function does not return. +PyAPI_FUNC(void) _Py_NO_RETURN Py_ExitWithInitConfig(PyInitConfig *config); diff --git a/Include/initconfig.h b/Include/initconfig.h new file mode 100644 index 00000000000000..c47aaeaf2897ee --- /dev/null +++ b/Include/initconfig.h @@ -0,0 +1,16 @@ +#ifndef Py_INITCONFIG_H +#define Py_INITCONFIG_H +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef Py_LIMITED_API +# define Py_CPYTHON_INITCONFIG_H +# include "cpython/initconfig.h" +# undef Py_CPYTHON_INITCONFIG_H +#endif + +#ifdef __cplusplus +} +#endif +#endif // !Py_INITCONFIG_H diff --git a/Include/internal/pycore_initconfig.h b/Include/internal/pycore_initconfig.h index 1c68161341860a..e64d0c8a8907f0 100644 --- a/Include/internal/pycore_initconfig.h +++ b/Include/internal/pycore_initconfig.h @@ -62,6 +62,10 @@ extern int _PyWideStringList_Copy(PyWideStringList *list, extern PyStatus _PyWideStringList_Extend(PyWideStringList *list, const PyWideStringList *list2); extern PyObject* _PyWideStringList_AsList(const PyWideStringList *list); +extern PyStatus _PyWideStringList_FromBytes( + PyWideStringList *list, + Py_ssize_t length, + char * const *items); /* --- _PyArgv ---------------------------------------------------- */ diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index d94c63a13b8ea4..715650240b5256 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -405,6 +405,13 @@ def test_ucnhash_capi_reset(self): self.assertEqual(out, '9\n' * INIT_LOOPS) +def config_dev_mode(preconfig, config): + preconfig['allocator'] = PYMEM_ALLOCATOR_DEBUG + config['dev_mode'] = 1 + config['warnoptions'] = ['default'] + config['faulthandler'] = 1 + + @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): maxDiff = 4096 @@ -1013,23 +1020,20 @@ def test_init_env_dev_mode_alloc(self): api=API_COMPAT) def test_init_dev_mode(self): - preconfig = { - 'allocator': PYMEM_ALLOCATOR_DEBUG, - } + preconfig = {} config = { 'faulthandler': True, 'dev_mode': True, 'warnoptions': ['default'], } + config_dev_mode(preconfig, config) self.check_all_configs("test_init_dev_mode", config, preconfig, api=API_PYTHON) def test_preinit_parse_argv(self): # Pre-initialize implicitly using argv: make sure that -X dev # is used to configure the allocation in preinitialization - preconfig = { - 'allocator': PYMEM_ALLOCATOR_DEBUG, - } + preconfig = {} config = { 'argv': ['script.py'], 'orig_argv': ['python3', '-X', 'dev', '-P', 'script.py'], @@ -1040,6 +1044,7 @@ def test_preinit_parse_argv(self): 'xoptions': ['dev'], 'safe_path': True, } + config_dev_mode(preconfig, config) self.check_all_configs("test_preinit_parse_argv", config, preconfig, api=API_PYTHON) @@ -1639,16 +1644,15 @@ def test_init_warnoptions(self): 'ignore:::PySys_AddWarnOption2', # PySys_AddWarnOption() 'ignore:::PyConfig_BeforeRead', # PyConfig.warnoptions 'ignore:::PyConfig_AfterRead'] # PyWideStringList_Append() - preconfig = dict(allocator=PYMEM_ALLOCATOR_DEBUG) + preconfig = {} config = { - 'dev_mode': 1, - 'faulthandler': 1, 'bytes_warning': 1, - 'warnoptions': warnoptions, 'orig_argv': ['python3', '-Wignore:::cmdline1', '-Wignore:::cmdline2'], } + config_dev_mode(preconfig, config) + config['warnoptions'] = warnoptions self.check_all_configs("test_init_warnoptions", config, preconfig, api=API_PYTHON) @@ -1661,6 +1665,24 @@ def test_init_set_config(self): self.check_all_configs("test_init_set_config", config, api=API_ISOLATED) + def test_initconfig_api(self): + preconfig = {} + config = { + 'dev_mode': 1, + 'pycache_prefix': 'conf_pycache_prefix', + 'argv': ['-c'], + 'orig_argv': ['./_testembed', '-c', 'pass'], + 'run_command': 'pass\n', + 'xoptions': ['faulthandler'], + 'faulthandler': 1, + } + config_dev_mode(preconfig, config) + self.check_all_configs("test_initconfig_api", config, preconfig, + api=API_PYTHON) + + def test_initconfig_exit(self): + self.run_embedded_interpreter("test_initconfig_exit") + def test_get_argc_argv(self): self.run_embedded_interpreter("test_get_argc_argv") # ignore output diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index c06c285c5013a6..ec944efde37516 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -364,6 +364,17 @@ def test_windows_feature_macros(self): "PyImport_ImportModuleNoBlock", "PyImport_ReloadModule", "PyIndex_Check", + "PyInitConfig_Exception", + "PyInitConfig_Free", + "PyInitConfig_GetError", + "PyInitConfig_GetExitCode", + "PyInitConfig_Isolated_New", + "PyInitConfig_Python_New", + "PyInitConfig_SetInt", + "PyInitConfig_SetStr", + "PyInitConfig_SetStrList", + "PyInitConfig_SetWStr", + "PyInitConfig_SetWStrList", "PyInterpreterState_Clear", "PyInterpreterState_Delete", "PyInterpreterState_Get", @@ -853,6 +864,7 @@ def test_windows_feature_macros(self): "Py_EndInterpreter", "Py_EnterRecursiveCall", "Py_Exit", + "Py_ExitWithInitConfig", "Py_FatalError", "Py_FileSystemDefaultEncodeErrors", "Py_FileSystemDefaultEncoding", @@ -879,6 +891,7 @@ def test_windows_feature_macros(self): "Py_IncRef", "Py_Initialize", "Py_InitializeEx", + "Py_InitializeFromInitConfig", "Py_Is", "Py_IsFalse", "Py_IsFinalizing", diff --git a/Misc/NEWS.d/next/C API/2023-11-30-23-19-59.gh-issue-107954.7rFtEo.rst b/Misc/NEWS.d/next/C API/2023-11-30-23-19-59.gh-issue-107954.7rFtEo.rst new file mode 100644 index 00000000000000..4437dd13a23730 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2023-11-30-23-19-59.gh-issue-107954.7rFtEo.rst @@ -0,0 +1,17 @@ +Add functions to the limited C API to configure the Python initialization: + +* :c:func:`PyInitConfig_Python_New` +* :c:func:`PyInitConfig_Isolated_New` +* :c:func:`PyInitConfig_Free` +* :c:func:`PyInitConfig_SetInt` +* :c:func:`PyInitConfig_SetStr` +* :c:func:`PyInitConfig_SetWStr` +* :c:func:`PyInitConfig_SetStrList` +* :c:func:`PyInitConfig_SetWStrList` +* :c:func:`PyInitConfig_Exception` +* :c:func:`PyInitConfig_GetError` +* :c:func:`PyInitConfig_GetExitCode` +* :c:func:`Py_InitializeFromInitConfig` +* :c:func:`Py_ExitWithInitConfig` + +Patch by Victor Stinner. diff --git a/PC/python3dll.c b/PC/python3dll.c index 86c888430891c9..c786b868f4565c 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -48,6 +48,7 @@ EXPORT_FUNC(Py_EncodeLocale) EXPORT_FUNC(Py_EndInterpreter) EXPORT_FUNC(Py_EnterRecursiveCall) EXPORT_FUNC(Py_Exit) +EXPORT_FUNC(Py_ExitWithInitConfig) EXPORT_FUNC(Py_FatalError) EXPORT_FUNC(Py_Finalize) EXPORT_FUNC(Py_FinalizeEx) @@ -70,6 +71,7 @@ EXPORT_FUNC(Py_GetVersion) EXPORT_FUNC(Py_IncRef) EXPORT_FUNC(Py_Initialize) EXPORT_FUNC(Py_InitializeEx) +EXPORT_FUNC(Py_InitializeFromInitConfig) EXPORT_FUNC(Py_Is) EXPORT_FUNC(Py_IsFalse) EXPORT_FUNC(Py_IsFinalizing) @@ -317,6 +319,17 @@ EXPORT_FUNC(PyImport_ImportModuleLevelObject) EXPORT_FUNC(PyImport_ImportModuleNoBlock) EXPORT_FUNC(PyImport_ReloadModule) EXPORT_FUNC(PyIndex_Check) +EXPORT_FUNC(PyInitConfig_Exception) +EXPORT_FUNC(PyInitConfig_Free) +EXPORT_FUNC(PyInitConfig_GetError) +EXPORT_FUNC(PyInitConfig_GetExitCode) +EXPORT_FUNC(PyInitConfig_Isolated_New) +EXPORT_FUNC(PyInitConfig_Python_New) +EXPORT_FUNC(PyInitConfig_SetInt) +EXPORT_FUNC(PyInitConfig_SetStr) +EXPORT_FUNC(PyInitConfig_SetStrList) +EXPORT_FUNC(PyInitConfig_SetWStr) +EXPORT_FUNC(PyInitConfig_SetWStrList) EXPORT_FUNC(PyInterpreterState_Clear) EXPORT_FUNC(PyInterpreterState_Delete) EXPORT_FUNC(PyInterpreterState_Get) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index d149b6a0c5cd21..914861d6b4c0ad 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -35,6 +35,7 @@ char **main_argv; /* Use path starting with "./" avoids a search along the PATH */ #define PROGRAM_NAME L"./_testembed" +#define PROGRAM_NAME_UTF8 "./_testembed" #define INIT_LOOPS 4 @@ -1796,6 +1797,90 @@ static int test_init_set_config(void) } +static int test_initconfig_api(void) +{ + PyInitConfig *config = PyInitConfig_Python_New(); + if (config == NULL) { + printf("Init allocation error\n"); + return 1; + } + + if (PyInitConfig_SetInt(config, "dev_mode", 1) < 0) { + goto error; + } + + // Set a list of wide strings (argv) + char *argv[] = {PROGRAM_NAME_UTF8, "-c", "pass"}; + if (PyInitConfig_SetStrList(config, "argv", + Py_ARRAY_LENGTH(argv), argv) < 0) { + goto error; + } + + if (PyInitConfig_SetInt(config, "hash_seed", 10) < 0) { + goto error; + } + + // Set a UTF-8 string (program_name) + if (PyInitConfig_SetStr(config, "program_name", PROGRAM_NAME_UTF8) < 0) { + goto error; + } + + // Set a bytes string (pycache_prefix) + if (PyInitConfig_SetStr(config, "pycache_prefix", + "conf_pycache_prefix") < 0) { + goto error; + } + + // Set a list of bytes strings (xoptions) + char* xoptions[] = {"faulthandler"}; + if (PyInitConfig_SetStrList(config, "xoptions", + Py_ARRAY_LENGTH(xoptions), xoptions) < 0) { + goto error; + } + + + if (Py_InitializeFromInitConfig(config) < 0) { + goto error; + } + PyInitConfig_Free(config); + + dump_config(); + Py_Finalize(); + return 0; + +error: + printf("Init failed:\n"); + Py_ExitWithInitConfig(config); +} + + +static int test_initconfig_exit(void) +{ + PyInitConfig *config = PyInitConfig_Python_New(); + if (config == NULL) { + printf("Init allocation error\n"); + return 1; + } + + char *argv[] = {PROGRAM_NAME_UTF8, "--help"}; + assert(PyInitConfig_SetStrList(config, "argv", + Py_ARRAY_LENGTH(argv), argv) == 0); + + assert(Py_InitializeFromInitConfig(config) < 0); + + int exitcode; + assert(PyInitConfig_GetExitCode(config, &exitcode) == 1); + assert(exitcode == 0); + + const char *err_msg; + assert(PyInitConfig_GetError(config, &err_msg) == 1); + assert(strcmp(err_msg, "exit code 0") == 0); + + PyInitConfig_Free(config); + return 0; +} + + static void configure_init_main(PyConfig *config) { wchar_t* argv[] = { @@ -2198,6 +2283,8 @@ static struct TestCase TestCases[] = { {"test_init_is_python_build", test_init_is_python_build}, {"test_init_warnoptions", test_init_warnoptions}, {"test_init_set_config", test_init_set_config}, + {"test_initconfig_api", test_initconfig_api}, + {"test_initconfig_exit", test_initconfig_exit}, {"test_run_main", test_run_main}, {"test_run_main_loop", test_run_main_loop}, {"test_get_argc_argv", test_get_argc_argv}, diff --git a/Python/initconfig.c b/Python/initconfig.c index a28c08c5318ddc..ff7ae075ee4d41 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -26,6 +26,8 @@ #include "config_common.h" +// Forward declarations +typedef struct PyConfigSpec PyConfigSpec; /* --- PyConfig spec ---------------------------------------------- */ @@ -40,11 +42,11 @@ typedef enum { PyConfig_MEMBER_WSTR_LIST = 12, } PyConfigMemberType; -typedef struct { +struct PyConfigSpec { const char *name; size_t offset; PyConfigMemberType type; -} PyConfigSpec; +}; #define SPEC(MEMBER, TYPE) \ {#MEMBER, offsetof(PyConfig, MEMBER), PyConfig_MEMBER_##TYPE} @@ -973,7 +975,7 @@ config_set_bytes_string(PyConfig *config, wchar_t **config_str, /* Decode str using Py_DecodeLocale() and set the result into *config_str. - Pre-initialize Python if needed to ensure that encodings are properly + Preinitialize Python if needed to ensure that encodings are properly configured. */ PyStatus PyConfig_SetBytesString(PyConfig *config, wchar_t **config_str, @@ -983,6 +985,13 @@ PyConfig_SetBytesString(PyConfig *config, wchar_t **config_str, } +static inline void* +config_spec_get_member(const PyConfigSpec *spec, const PyConfig *config) +{ + return (char *)config + spec->offset; +} + + PyStatus _PyConfig_Copy(PyConfig *config, const PyConfig *config2) { @@ -991,19 +1000,19 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2) PyStatus status; const PyConfigSpec *spec = PYCONFIG_SPEC; for (; spec->name != NULL; spec++) { - char *member = (char *)config + spec->offset; - char *member2 = (char *)config2 + spec->offset; + char *member = config_spec_get_member(spec, config); + const char *member2 = config_spec_get_member(spec, (PyConfig*)config2); switch (spec->type) { case PyConfig_MEMBER_INT: case PyConfig_MEMBER_UINT: case PyConfig_MEMBER_BOOL: { - *(int*)member = *(int*)member2; + *(int*)member = *(const int*)member2; break; } case PyConfig_MEMBER_ULONG: { - *(unsigned long*)member = *(unsigned long*)member2; + *(unsigned long*)member = *(const unsigned long*)member2; break; } case PyConfig_MEMBER_WSTR: @@ -1042,7 +1051,8 @@ _PyConfig_AsDict(const PyConfig *config) const PyConfigSpec *spec = PYCONFIG_SPEC; for (; spec->name != NULL; spec++) { - char *member = (char *)config + spec->offset; + char *member = config_spec_get_member(spec, config); + PyObject *obj; switch (spec->type) { case PyConfig_MEMBER_INT: @@ -1090,6 +1100,7 @@ _PyConfig_AsDict(const PyConfig *config) Py_DECREF(dict); return NULL; } + int res = PyDict_SetItemString(dict, spec->name, obj); Py_DECREF(obj); if (res < 0) { @@ -1256,7 +1267,7 @@ _PyConfig_FromDict(PyConfig *config, PyObject *dict) const PyConfigSpec *spec = PYCONFIG_SPEC; for (; spec->name != NULL; spec++) { - char *member = (char *)config + spec->offset; + char *member = config_spec_get_member(spec, config); switch (spec->type) { case PyConfig_MEMBER_INT: case PyConfig_MEMBER_UINT: @@ -3244,3 +3255,277 @@ _Py_DumpPathConfig(PyThreadState *tstate) _PyErr_SetRaisedException(tstate, exc); } + + +// --- PyInitConfig API --------------------------------------------------- + +struct PyInitConfig { + PyConfig config; + PyStatus status; + char *err_msg; +}; + +static PyInitConfig* +initconfig_alloc(void) +{ + return calloc(1, sizeof(PyInitConfig)); +} + + +PyInitConfig* +PyInitConfig_Python_New(void) +{ + PyInitConfig *config = initconfig_alloc(); + if (config == NULL) { + return NULL; + } + PyConfig_InitPythonConfig(&config->config); + return config; +} + + +PyInitConfig* +PyInitConfig_Isolated_New(void) +{ + PyInitConfig *config = initconfig_alloc(); + if (config == NULL) { + return NULL; + } + PyConfig_InitIsolatedConfig(&config->config); + return config; +} + + +void +PyInitConfig_Free(PyInitConfig *config) +{ + free(config->err_msg); + free(config); +} + + +static void +initconfig_set_error(PyInitConfig *config, const char *err_msg) +{ + config->status = _PyStatus_ERR(err_msg); +} + + +static const PyConfigSpec* +config_get_spec(const char *name) +{ + const PyConfigSpec *spec = PYCONFIG_SPEC; + for (; spec->name != NULL; spec++) { + if (strcmp(name, spec->name) == 0) { + return spec; + } + } + return NULL; +} + + +static const PyConfigSpec* +initconfig_prepare_set(PyInitConfig *config, const char *name, int preconfig) +{ + if (config == NULL) { + initconfig_set_error(config, "config argument is NULL"); + return NULL; + } + if (name == NULL) { + initconfig_set_error(config, "config option name is NULL"); + return NULL; + } + + const PyConfigSpec *spec = config_get_spec(name); + if (spec == NULL) { + initconfig_set_error(config, "unknown config option name"); + return NULL; + } + + if (preconfig) { + config->status = _Py_PreInitializeFromConfig(&config->config, NULL); + if (_PyStatus_EXCEPTION(config->status)) { + return NULL; + } + } + + return spec; +} + + +int +PyInitConfig_SetInt(PyInitConfig *config, const char *name, int64_t value) +{ + const PyConfigSpec *spec = initconfig_prepare_set(config, name, 0); + if (spec == NULL) { + return -1; + } + + if (spec->type == PyConfig_MEMBER_INT) { + if (value < (int64_t)INT_MIN || INT_MAX < (int64_t)value) { + initconfig_set_error(config, + "config option value is out of int range"); + return -1; + } + int int_value = (int)value; + + int *member = config_spec_get_member(spec, &config->config); + *member = int_value; + } + else if (spec->type == PyConfig_MEMBER_UINT || spec->type == PyConfig_MEMBER_BOOL) { + if (value < 0 || UINT_MAX < (int64_t)value) { + initconfig_set_error(config, + "config option value is out of unsigned int range"); + return -1; + } + int int_value = (int)value; + + int *member = config_spec_get_member(spec, &config->config); + *member = int_value; + } + else if (spec->type == PyConfig_MEMBER_ULONG) { + if (value < 0 || (uint64_t)ULONG_MAX < (uint64_t)value) { + initconfig_set_error(config, + "config option value is out of unsigned long range"); + return -1; + } + unsigned long ulong_value = (unsigned long)value; + + unsigned long *member = config_spec_get_member(spec, &config->config); + *member = ulong_value; + } + else { + initconfig_set_error(config, "config option type is not int"); + return -1; + } + return 0; +} + + +int +PyInitConfig_SetStr(PyInitConfig *config, const char *name, + const char* value) +{ + const PyConfigSpec *spec = initconfig_prepare_set(config, name, 1); + if (spec == NULL) { + return -1; + } + + if (spec->type != PyConfig_MEMBER_WSTR + && spec->type != PyConfig_MEMBER_WSTR_OPT) { + initconfig_set_error(config, "config option type is not string"); + return -1; + } + + if (value == NULL && spec->type != PyConfig_MEMBER_WSTR_OPT) { + initconfig_set_error(config, "config option string cannot be NULL"); + } + + wchar_t **member = config_spec_get_member(spec, &config->config); + config->status = PyConfig_SetBytesString(&config->config, member, value); + + if (_PyStatus_EXCEPTION(config->status)) { + return -1; + } + return 0; +} + + +int +PyInitConfig_SetStrList(PyInitConfig *config, const char *name, + size_t length, char * const *items) +{ + const PyConfigSpec *spec = initconfig_prepare_set(config, name, 1); + if (spec == NULL) { + return -1; + } + + if (spec->type != PyConfig_MEMBER_WSTR_LIST) { + initconfig_set_error(config, "config option type is not strings list"); + return -1; + } + PyWideStringList *list = config_spec_get_member(spec, &config->config); + + config->status = _PyWideStringList_FromBytes(list, length, items); + if (_PyStatus_EXCEPTION(config->status)) { + return -1; + } + return 0; +} + + +int +Py_InitializeFromInitConfig(PyInitConfig *config) +{ + config->status = Py_InitializeFromConfig(&config->config); + if (_PyStatus_EXCEPTION(config->status)) { + return -1; + } + return 0; +} + + +void +Py_ExitWithInitConfig(PyInitConfig *config) +{ + if (_PyStatus_IS_ERROR(config->status)) { + printf("Python initialization error: %s\n", + config->status.err_msg); + } + + PyInitConfig_Free(config); + + if (_PyStatus_EXCEPTION(config->status)) { + Py_ExitStatusException(config->status); + } + else { + exit(1); + } +} + + +int +PyInitConfig_Exception(PyInitConfig* config) +{ + return _PyStatus_EXCEPTION(config->status); +} + + +int +PyInitConfig_GetError(PyInitConfig* config, const char **perr_msg) +{ + if (_PyStatus_IS_EXIT(config->status)) { + char buffer[22]; // len("exit code -2147483648\0") + PyOS_snprintf(buffer, sizeof(buffer), + "exit code %i", + config->status.exitcode); + config->err_msg = strdup(buffer); + if (config->err_msg != NULL) { + *perr_msg = config->err_msg; + return 1; + } + config->status = _PyStatus_NO_MEMORY(); + } + + if (_PyStatus_IS_ERROR(config->status) && config->status.err_msg != NULL) { + *perr_msg = config->status.err_msg; + return 1; + } + else { + *perr_msg = NULL; + return 0; + } +} + + +int +PyInitConfig_GetExitCode(PyInitConfig* config, int *exitcode) +{ + if (_PyStatus_IS_EXIT(config->status)) { + *exitcode = config->status.exitcode; + return 1; + } + else { + return 0; + } +} diff --git a/Python/preconfig.c b/Python/preconfig.c index 5b26c75de8b3a0..ebcfa28323f74b 100644 --- a/Python/preconfig.c +++ b/Python/preconfig.c @@ -74,33 +74,47 @@ _Py_COMP_DIAG_POP /* --- _PyArgv ---------------------------------------------------- */ -/* Decode bytes_argv using Py_DecodeLocale() */ PyStatus -_PyArgv_AsWstrList(const _PyArgv *args, PyWideStringList *list) +_PyWideStringList_FromBytes(PyWideStringList *list, + Py_ssize_t length, char * const *items) { PyWideStringList wargv = _PyWideStringList_INIT; - if (args->use_bytes_argv) { - size_t size = sizeof(wchar_t*) * args->argc; - wargv.items = (wchar_t **)PyMem_RawMalloc(size); - if (wargv.items == NULL) { - return _PyStatus_NO_MEMORY(); - } + size_t size = sizeof(wchar_t*) * length; + wargv.items = (wchar_t **)PyMem_RawMalloc(size); + if (wargv.items == NULL) { + return _PyStatus_NO_MEMORY(); + } - for (Py_ssize_t i = 0; i < args->argc; i++) { - size_t len; - wchar_t *arg = Py_DecodeLocale(args->bytes_argv[i], &len); - if (arg == NULL) { - _PyWideStringList_Clear(&wargv); - return DECODE_LOCALE_ERR("command line arguments", len); - } - wargv.items[i] = arg; - wargv.length++; + for (Py_ssize_t i = 0; i < length; i++) { + size_t len; + wchar_t *arg = Py_DecodeLocale(items[i], &len); + if (arg == NULL) { + _PyWideStringList_Clear(&wargv); + return DECODE_LOCALE_ERR("command line arguments", len); } + wargv.items[i] = arg; + wargv.length++; + } - _PyWideStringList_Clear(list); - *list = wargv; + _PyWideStringList_Clear(list); + *list = wargv; + return _PyStatus_OK(); +} + +/* Decode bytes_argv using Py_DecodeLocale() */ +PyStatus +_PyArgv_AsWstrList(const _PyArgv *args, PyWideStringList *list) +{ + if (args->use_bytes_argv) { + PyStatus status; + status = _PyWideStringList_FromBytes(list, + args->argc, args->bytes_argv); + if (_PyStatus_EXCEPTION(status)) { + return status; + } } else { + PyWideStringList wargv = _PyWideStringList_INIT; wargv.length = args->argc; wargv.items = (wchar_t **)args->wchar_argv; if (_PyWideStringList_Copy(list, &wargv) < 0) {