From ef9d54703f1f21d2fde2fe3ef49d4cf7c8b38f06 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 3 Sep 2024 14:33:49 +0200 Subject: [PATCH] gh-107954, PEP 741: Add PyInitConfig C API (#123502) Add Doc/c-api/config.rst documentation. --- Doc/c-api/init.rst | 2 +- Doc/c-api/init_config.rst | 245 ++++++++- Doc/whatsnew/3.14.rst | 21 +- Include/cpython/initconfig.h | 41 ++ Lib/test/test_embed.py | 46 +- ...-08-30-14-02-17.gh-issue-107954.TPvj4u.rst | 17 + Objects/unicodeobject.c | 4 +- Programs/_testembed.c | 139 +++++ Python/initconfig.c | 493 +++++++++++++++++- 9 files changed, 976 insertions(+), 32 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2024-08-30-14-02-17.gh-issue-107954.TPvj4u.rst diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 0965fd97e09..561c8a1b879 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -7,7 +7,7 @@ Initialization, Finalization, and Threads ***************************************** -See also :ref:`Python Initialization Configuration `. +See also the :ref:`Python Initialization Configuration `. .. _pre-init-safe: diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index 355b8e010e0..b59be98608a 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -6,6 +6,9 @@ Python Initialization Configuration *********************************** +PyConfig C API +============== + .. versionadded:: 3.8 Python can be initialized with :c:func:`Py_InitializeFromConfig` and the @@ -34,7 +37,7 @@ See also :ref:`Initialization, Finalization, and Threads `. Example -======= +------- Example of customized Python always running in isolated mode:: @@ -73,7 +76,7 @@ Example of customized Python always running in isolated mode:: PyWideStringList -================ +---------------- .. c:type:: PyWideStringList @@ -116,7 +119,7 @@ PyWideStringList List items. PyStatus -======== +-------- .. c:type:: PyStatus @@ -210,7 +213,7 @@ Example:: PyPreConfig -=========== +----------- .. c:type:: PyPreConfig @@ -360,7 +363,7 @@ PyPreConfig .. _c-preinit: Preinitialize Python with PyPreConfig -===================================== +------------------------------------- The preinitialization of Python: @@ -440,7 +443,7 @@ the :ref:`Python UTF-8 Mode `:: PyConfig -======== +-------- .. c:type:: PyConfig @@ -1349,7 +1352,7 @@ the :option:`-X` command line option. Initialization with PyConfig -============================ +---------------------------- Function to initialize Python: @@ -1461,7 +1464,7 @@ initialization:: .. _init-isolated-conf: Isolated Configuration -====================== +---------------------- :c:func:`PyPreConfig_InitIsolatedConfig` and :c:func:`PyConfig_InitIsolatedConfig` functions create a configuration to @@ -1481,7 +1484,7 @@ to avoid computing the default path configuration. .. _init-python-config: Python Configuration -==================== +-------------------- :c:func:`PyPreConfig_InitPythonConfig` and :c:func:`PyConfig_InitPythonConfig` functions create a configuration to build a customized Python which behaves as @@ -1499,7 +1502,7 @@ and :ref:`Python UTF-8 Mode ` .. _init-path-config: Python Path Configuration -========================= +------------------------- :c:type:`PyConfig` contains multiple fields for the path configuration: @@ -1585,6 +1588,228 @@ The ``__PYVENV_LAUNCHER__`` environment variable is used to set :c:member:`PyConfig.base_executable`. +PyInitConfig C API +================== + +C API to configure the Python initialization (:pep:`741`). + +.. versionadded:: 3.14 + +Create Config +------------- + +.. c:struct:: PyInitConfig + + Opaque structure to configure the Python initialization. + + +.. c:function:: PyInitConfig* PyInitConfig_Create(void) + + Create a new initialization configuration using :ref:`Isolated Configuration + ` default values. + + It must be freed by :c:func:`PyInitConfig_Free`. + + Return ``NULL`` on memory allocation failure. + + +.. c:function:: void PyInitConfig_Free(PyInitConfig *config) + + Free memory of the initialization configuration *config*. + + +Error Handling +-------------- + +.. 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. + + An error message is an UTF-8 encoded string. + + If *config* has an exit code, format the exit code as an error + message. + + The error message remains valid until another ``PyInitConfig`` + function is called with *config*. The caller doesn't have to free the + error message. + + +.. c:function:: int PyInitConfig_GetExitCode(PyInitConfig* config, int *exitcode) + + Get the *config* exit code. + + * Set *\*exitcode* and return ``1`` if *config* has an exit code set. + * Return ``0`` if *config* has no exit code set. + + Only the ``Py_InitializeFromInitConfig()`` function can set an exit + code if the ``parse_argv`` option is non-zero. + + An exit code can be set when parsing the command line failed (exit + code ``2``) or when a command line option asks to display the command + line help (exit code ``0``). + + +Get Options +----------- + +The configuration option *name* parameter must be a non-NULL +null-terminated UTF-8 encoded string. + +.. c:function:: int PyInitConfig_HasOption(PyInitConfig *config, const char *name) + + Test if the configuration has an option called *name*. + + Return ``1`` if the option exists, or return ``0`` otherwise. + + +.. c:function:: int PyInitConfig_GetInt(PyInitConfig *config, const char *name, int64_t *value) + + Get an integer configuration option. + + * Set *\*value*, and return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + + +.. c:function:: int PyInitConfig_GetStr(PyInitConfig *config, const char *name, char **value) + + Get a string configuration option as a null-terminated UTF-8 + encoded string. + + * Set *\*value*, and return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + + *\*value* can be set to ``NULL`` if the option is an optional string and the + option is unset. + + On success, the string must be released with ``free(value)`` if it's not + ``NULL``. + + +.. c:function:: int PyInitConfig_GetStrList(PyInitConfig *config, const char *name, size_t *length, char ***items) + + Get a string list configuration option as an array of + null-terminated UTF-8 encoded strings. + + * Set *\*length* and *\*value*, and return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + + On success, the string list must be released with + ``PyInitConfig_FreeStrList(length, items)``. + + +.. c:function:: void PyInitConfig_FreeStrList(size_t length, char **items) + + Free memory of a string list created by + ``PyInitConfig_GetStrList()``. + + +Set Options +----------- + +The configuration option *name* parameter must be a non-NULL null-terminated +UTF-8 encoded string. + +Some configuration options have side effects on other options. This logic is +only implemented when ``Py_InitializeFromInitConfig()`` is called, not by the +"Set" functions below. For example, setting ``dev_mode`` to ``1`` does not set +``faulthandler`` to ``1``. + +.. 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 UTF-8 + encoded string. The string is copied. + + * 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 UTF-8 encoded strings. The string list is copied. + + * Return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + + +Initialize Python +----------------- + +.. c:function:: int Py_InitializeFromInitConfig(PyInitConfig *config) + + Initialize Python from the initialization configuration. + + * Return ``0`` on success. + * Set an error in *config* and return ``-1`` on error. + * Set an exit code in *config* and return ``-1`` if Python wants to + exit. + + See ``PyInitConfig_GetExitcode()`` for the exit code case. + + +Example +------- + +Example initializing Python, set configuration options of various types, +return ``-1`` on error: + +.. code-block:: c + + 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; + + // Display the error message + const char *err_msg; + error: + (void)PyInitConfig_GetError(config, &err_msg); + printf("PYTHON INIT ERROR: %s\n", err_msg); + PyInitConfig_Free(config); + + return -1; + } + + Py_RunMain() ============ diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 233d7ce0399..83c13e6fb64 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -500,7 +500,8 @@ New Features * Add :c:func:`Py_HashBuffer` to compute and return the hash value of a buffer. (Contributed by Antoine Pitrou and Victor Stinner in :gh:`122854`.) -* Add functions to get and set the current runtime Python configuration: +* Add functions to get and set the current runtime Python configuration + (:pep:`741`): * :c:func:`PyConfig_Get` * :c:func:`PyConfig_GetInt` @@ -509,6 +510,24 @@ New Features (Contributed by Victor Stinner in :gh:`107954`.) +* Add functions to configure the Python initialization (:pep:`741`): + + * :c:func:`PyInitConfig_Create` + * :c:func:`PyInitConfig_Free` + * :c:func:`PyInitConfig_GetError` + * :c:func:`PyInitConfig_GetExitCode` + * :c:func:`PyInitConfig_HasOption` + * :c:func:`PyInitConfig_GetInt` + * :c:func:`PyInitConfig_GetStr` + * :c:func:`PyInitConfig_GetStrList` + * :c:func:`PyInitConfig_FreeStrList` + * :c:func:`PyInitConfig_SetInt` + * :c:func:`PyInitConfig_SetStr` + * :c:func:`PyInitConfig_SetStrList` + * :c:func:`Py_InitializeFromInitConfig` + + (Contributed by Victor Stinner in :gh:`107954`.) + Porting to Python 3.14 ---------------------- diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index 83faab7e270..328828a9152 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -275,6 +275,47 @@ PyAPI_FUNC(int) PyConfig_Set(const char *name, PyObject *value); See also PyConfig.orig_argv. */ PyAPI_FUNC(void) Py_GetArgcArgv(int *argc, wchar_t ***argv); + +// --- PyInitConfig --------------------------------------------------------- + +typedef struct PyInitConfig PyInitConfig; + +PyAPI_FUNC(PyInitConfig*) PyInitConfig_Create(void); +PyAPI_FUNC(void) PyInitConfig_Free(PyInitConfig *config); + +PyAPI_FUNC(int) PyInitConfig_GetError(PyInitConfig* config, + const char **err_msg); +PyAPI_FUNC(int) PyInitConfig_GetExitCode(PyInitConfig* config, + int *exitcode); + +PyAPI_FUNC(int) PyInitConfig_HasOption(PyInitConfig *config, + const char *name); +PyAPI_FUNC(int) PyInitConfig_GetInt(PyInitConfig *config, + const char *name, + int64_t *value); +PyAPI_FUNC(int) PyInitConfig_GetStr(PyInitConfig *config, + const char *name, + char **value); +PyAPI_FUNC(int) PyInitConfig_GetStrList(PyInitConfig *config, + const char *name, + size_t *length, + char ***items); +PyAPI_FUNC(void) PyInitConfig_FreeStrList(size_t length, char **items); + +PyAPI_FUNC(int) PyInitConfig_SetInt(PyInitConfig *config, + const char *name, + int64_t value); +PyAPI_FUNC(int) PyInitConfig_SetStr(PyInitConfig *config, + const char *name, + const char *value); +PyAPI_FUNC(int) PyInitConfig_SetStrList(PyInitConfig *config, + const char *name, + size_t length, + char * const *items); + +PyAPI_FUNC(int) Py_InitializeFromInitConfig(PyInitConfig *config); + + #ifdef __cplusplus } #endif diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index b1d1f6d06d7..17656d7b3d7 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -494,6 +494,14 @@ class EmbeddingTests(EmbeddingTestsMixin, unittest.TestCase): self.assertEqual(out, '1\n2\n3\n' * INIT_LOOPS) +def config_dev_mode(preconfig, config): + preconfig['allocator'] = PYMEM_ALLOCATOR_DEBUG + preconfig['dev_mode'] = 1 + config['dev_mode'] = 1 + config['warnoptions'] = ['default'] + config['faulthandler'] = 1 + + @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): maxDiff = 4096 @@ -1102,23 +1110,20 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): 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'], @@ -1129,6 +1134,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): 'xoptions': {'dev': True}, 'safe_path': True, } + config_dev_mode(preconfig, config) self.check_all_configs("test_preinit_parse_argv", config, preconfig, api=API_PYTHON) @@ -1728,16 +1734,15 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): '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) @@ -1750,6 +1755,27 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase): self.check_all_configs("test_init_set_config", config, api=API_ISOLATED) + def test_initconfig_api(self): + preconfig = { + 'configure_locale': True, + } + config = { + 'pycache_prefix': 'conf_pycache_prefix', + 'xoptions': {'faulthandler': True}, + 'hash_seed': 10, + 'use_hash_seed': True, + } + config_dev_mode(preconfig, config) + config['faulthandler'] = 0 + self.check_all_configs("test_initconfig_api", config, preconfig, + api=API_ISOLATED) + + def test_initconfig_get_api(self): + self.run_embedded_interpreter("test_initconfig_get_api") + + 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/Misc/NEWS.d/next/C_API/2024-08-30-14-02-17.gh-issue-107954.TPvj4u.rst b/Misc/NEWS.d/next/C_API/2024-08-30-14-02-17.gh-issue-107954.TPvj4u.rst new file mode 100644 index 00000000000..370e2690c45 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2024-08-30-14-02-17.gh-issue-107954.TPvj4u.rst @@ -0,0 +1,17 @@ +Add functions to configure the Python initialization (:pep:`741`): + +* :c:func:`PyInitConfig_Create` +* :c:func:`PyInitConfig_Free` +* :c:func:`PyInitConfig_GetError` +* :c:func:`PyInitConfig_GetExitCode` +* :c:func:`PyInitConfig_HasOption` +* :c:func:`PyInitConfig_GetInt` +* :c:func:`PyInitConfig_GetStr` +* :c:func:`PyInitConfig_GetStrList` +* :c:func:`PyInitConfig_FreeStrList` +* :c:func:`PyInitConfig_SetInt` +* :c:func:`PyInitConfig_SetStr` +* :c:func:`PyInitConfig_SetStrList` +* :c:func:`Py_InitializeFromInitConfig` + +Patch by Victor Stinner. diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index fc1fb78b7ca..2494c989544 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -5393,7 +5393,7 @@ _Py_DecodeUTF8_surrogateescape(const char *arg, Py_ssize_t arglen, } -/* UTF-8 encoder using the surrogateescape error handler . +/* UTF-8 encoder. On success, return 0 and write the newly allocated character string (use PyMem_Free() to free the memory) into *str. @@ -15906,7 +15906,7 @@ encode_wstr_utf8(wchar_t *wstr, char **str, const char *name) int res; res = _Py_EncodeUTF8Ex(wstr, str, NULL, NULL, 1, _Py_ERROR_STRICT); if (res == -2) { - PyErr_Format(PyExc_RuntimeWarning, "cannot decode %s", name); + PyErr_Format(PyExc_RuntimeWarning, "cannot encode %s", name); return -1; } if (res < 0) { diff --git a/Programs/_testembed.c b/Programs/_testembed.c index e341f0c6bfc..13f1db1cc70 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -36,6 +36,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 @@ -1805,6 +1806,141 @@ static int test_init_set_config(void) } +static int test_initconfig_api(void) +{ + PyInitConfig *config = PyInitConfig_Create(); + if (config == NULL) { + printf("Init allocation error\n"); + return 1; + } + + if (PyInitConfig_SetInt(config, "configure_locale", 1) < 0) { + goto error; + } + + if (PyInitConfig_SetInt(config, "dev_mode", 1) < 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 UTF-8 string (pycache_prefix) + if (PyInitConfig_SetStr(config, "pycache_prefix", + "conf_pycache_prefix") < 0) { + goto error; + } + + // Set a list of UTF-8 strings (argv) + 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; + + const char *err_msg; +error: + (void)PyInitConfig_GetError(config, &err_msg); + printf("Python init failed: %s\n", err_msg); + exit(1); +} + + +static int test_initconfig_get_api(void) +{ + PyInitConfig *config = PyInitConfig_Create(); + if (config == NULL) { + printf("Init allocation error\n"); + return 1; + } + + // test PyInitConfig_HasOption() + assert(PyInitConfig_HasOption(config, "verbose") == 1); + assert(PyInitConfig_HasOption(config, "utf8_mode") == 1); + assert(PyInitConfig_HasOption(config, "non-existent") == 0); + + // test PyInitConfig_GetInt() + int64_t value; + assert(PyInitConfig_GetInt(config, "dev_mode", &value) == 0); + assert(value == 0); + assert(PyInitConfig_SetInt(config, "dev_mode", 1) == 0); + assert(PyInitConfig_GetInt(config, "dev_mode", &value) == 0); + assert(value == 1); + + // test PyInitConfig_GetInt() on a PyPreConfig option + assert(PyInitConfig_GetInt(config, "utf8_mode", &value) == 0); + assert(value == 0); + assert(PyInitConfig_SetInt(config, "utf8_mode", 1) == 0); + assert(PyInitConfig_GetInt(config, "utf8_mode", &value) == 0); + assert(value == 1); + + // test PyInitConfig_GetStr() + char *str; + assert(PyInitConfig_SetStr(config, "program_name", PROGRAM_NAME_UTF8) == 0); + assert(PyInitConfig_GetStr(config, "program_name", &str) == 0); + assert(strcmp(str, PROGRAM_NAME_UTF8) == 0); + free(str); + + // test PyInitConfig_GetStrList() and PyInitConfig_FreeStrList() + char* xoptions[] = {"faulthandler"}; + assert(PyInitConfig_SetStrList(config, "xoptions", + Py_ARRAY_LENGTH(xoptions), xoptions) == 0); + size_t length; + char **items; + assert(PyInitConfig_GetStrList(config, "xoptions", &length, &items) == 0); + assert(length == 1); + assert(strcmp(items[0], "faulthandler") == 0); + PyInitConfig_FreeStrList(length, items); + + return 0; +} + + +static int test_initconfig_exit(void) +{ + PyInitConfig *config = PyInitConfig_Create(); + 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(PyInitConfig_SetInt(config, "parse_argv", 1) == 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[] = { @@ -2223,6 +2359,9 @@ 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_get_api", test_initconfig_get_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 1c21486d8f2..d2bb4606220 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -1114,6 +1114,20 @@ PyConfig_SetBytesString(PyConfig *config, wchar_t **config_str, } +static inline void* +config_get_spec_member(const PyConfig *config, const PyConfigSpec *spec) +{ + return (char *)config + spec->offset; +} + + +static inline void* +preconfig_get_spec_member(const PyPreConfig *preconfig, const PyConfigSpec *spec) +{ + return (char *)preconfig + spec->offset; +} + + PyStatus _PyConfig_Copy(PyConfig *config, const PyConfig *config2) { @@ -1122,8 +1136,8 @@ _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; + void *member = config_get_spec_member(config, spec); + const void *member2 = config_get_spec_member((PyConfig*)config2, spec); switch (spec->type) { case PyConfig_MEMBER_INT: case PyConfig_MEMBER_UINT: @@ -3405,15 +3419,478 @@ _Py_DumpPathConfig(PyThreadState *tstate) } -// --- PyConfig_Get() ------------------------------------------------------- +// --- PyInitConfig API --------------------------------------------------- -static void* -config_spec_get_member(const PyConfigSpec *spec, const PyConfig *config) +struct PyInitConfig { + PyPreConfig preconfig; + PyConfig config; + PyStatus status; + char *err_msg; +}; + +static PyInitConfig* +initconfig_alloc(void) { - return (char *)config + spec->offset; + return calloc(1, sizeof(PyInitConfig)); } +PyInitConfig* +PyInitConfig_Create(void) +{ + PyInitConfig *config = initconfig_alloc(); + if (config == NULL) { + return NULL; + } + PyPreConfig_InitIsolatedConfig(&config->preconfig); + PyConfig_InitIsolatedConfig(&config->config); + config->status = _PyStatus_OK(); + return config; +} + + +void +PyInitConfig_Free(PyInitConfig *config) +{ + free(config->err_msg); + free(config); +} + + +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); + + if (config->err_msg != NULL) { + free(config->err_msg); + } + 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; + } +} + + +static void +initconfig_set_error(PyInitConfig *config, const char *err_msg) +{ + config->status = _PyStatus_ERR(err_msg); +} + + +static const PyConfigSpec* +initconfig_find_spec(const PyConfigSpec *spec, const char *name) +{ + for (; spec->name != NULL; spec++) { + if (strcmp(name, spec->name) == 0) { + return spec; + } + } + return NULL; +} + + +int +PyInitConfig_HasOption(PyInitConfig *config, const char *name) +{ + const PyConfigSpec *spec = initconfig_find_spec(PYCONFIG_SPEC, name); + if (spec == NULL) { + spec = initconfig_find_spec(PYPRECONFIG_SPEC, name); + } + return (spec != NULL); +} + + +static const PyConfigSpec* +initconfig_prepare(PyInitConfig *config, const char *name, void **raw_member) +{ + const PyConfigSpec *spec = initconfig_find_spec(PYCONFIG_SPEC, name); + if (spec != NULL) { + *raw_member = config_get_spec_member(&config->config, spec); + return spec; + } + + spec = initconfig_find_spec(PYPRECONFIG_SPEC, name); + if (spec != NULL) { + *raw_member = preconfig_get_spec_member(&config->preconfig, spec); + return spec; + } + + initconfig_set_error(config, "unknown config option name"); + return NULL; +} + + +int +PyInitConfig_GetInt(PyInitConfig *config, const char *name, int64_t *value) +{ + void *raw_member; + const PyConfigSpec *spec = initconfig_prepare(config, name, &raw_member); + if (spec == NULL) { + return -1; + } + + switch (spec->type) { + case PyConfig_MEMBER_INT: + case PyConfig_MEMBER_UINT: + case PyConfig_MEMBER_BOOL: + { + int *member = raw_member; + *value = *member; + break; + } + + case PyConfig_MEMBER_ULONG: + { + unsigned long *member = raw_member; +#if SIZEOF_LONG >= 8 + if ((unsigned long)INT64_MAX < *member) { + initconfig_set_error(config, + "config option value doesn't fit into int64_t"); + return -1; + } +#endif + *value = *member; + break; + } + + default: + initconfig_set_error(config, "config option type is not int"); + return -1; + } + return 0; +} + + +static char* +wstr_to_utf8(PyInitConfig *config, wchar_t *wstr) +{ + char *utf8; + int res = _Py_EncodeUTF8Ex(wstr, &utf8, NULL, NULL, 1, _Py_ERROR_STRICT); + if (res == -2) { + initconfig_set_error(config, "encoding error"); + return NULL; + } + if (res < 0) { + config->status = _PyStatus_NO_MEMORY(); + return NULL; + } + + // Copy to use the malloc() memory allocator + size_t size = strlen(utf8) + 1; + char *str = malloc(size); + if (str == NULL) { + PyMem_RawFree(utf8); + config->status = _PyStatus_NO_MEMORY(); + return NULL; + } + + memcpy(str, utf8, size); + PyMem_RawFree(utf8); + return str; +} + + +int +PyInitConfig_GetStr(PyInitConfig *config, const char *name, char **value) +{ + void *raw_member; + const PyConfigSpec *spec = initconfig_prepare(config, name, &raw_member); + 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; + } + + wchar_t **member = raw_member; + if (*member == NULL) { + *value = NULL; + return 0; + } + + *value = wstr_to_utf8(config, *member); + if (*value == NULL) { + return -1; + } + return 0; +} + + +int +PyInitConfig_GetStrList(PyInitConfig *config, const char *name, size_t *length, char ***items) +{ + void *raw_member; + const PyConfigSpec *spec = initconfig_prepare(config, name, &raw_member); + if (spec == NULL) { + return -1; + } + + if (spec->type != PyConfig_MEMBER_WSTR_LIST) { + initconfig_set_error(config, "config option type is not string list"); + return -1; + } + + PyWideStringList *list = raw_member; + *length = list->length; + + *items = malloc(list->length * sizeof(char*)); + if (*items == NULL) { + config->status = _PyStatus_NO_MEMORY(); + return -1; + } + + for (Py_ssize_t i=0; i < list->length; i++) { + (*items)[i] = wstr_to_utf8(config, list->items[i]); + if ((*items)[i] == NULL) { + PyInitConfig_FreeStrList(i, *items); + return -1; + } + } + return 0; +} + + +void +PyInitConfig_FreeStrList(size_t length, char **items) +{ + for (size_t i=0; i < length; i++) { + free(items[i]); + } + free(items); +} + + +int +PyInitConfig_SetInt(PyInitConfig *config, const char *name, int64_t value) +{ + void *raw_member; + const PyConfigSpec *spec = initconfig_prepare(config, name, &raw_member); + if (spec == NULL) { + return -1; + } + + switch (spec->type) { + case PyConfig_MEMBER_INT: + { + if (value < (int64_t)INT_MIN || (int64_t)INT_MAX < value) { + initconfig_set_error(config, + "config option value is out of int range"); + return -1; + } + int int_value = (int)value; + + int *member = raw_member; + *member = int_value; + break; + } + + case PyConfig_MEMBER_UINT: + case PyConfig_MEMBER_BOOL: + { + if (value < 0 || (uint64_t)UINT_MAX < (uint64_t)value) { + initconfig_set_error(config, + "config option value is out of unsigned int range"); + return -1; + } + int int_value = (int)value; + + int *member = raw_member; + *member = int_value; + break; + } + + case 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 = raw_member; + *member = ulong_value; + break; + } + + default: + initconfig_set_error(config, "config option type is not int"); + return -1; + } + + if (strcmp(name, "hash_seed")) { + config->config.use_hash_seed = 1; + } + + return 0; +} + + +static wchar_t* +utf8_to_wstr(PyInitConfig *config, const char *str) +{ + wchar_t *wstr; + size_t wlen; + int res = _Py_DecodeUTF8Ex(str, strlen(str), &wstr, &wlen, NULL, _Py_ERROR_STRICT); + if (res == -2) { + initconfig_set_error(config, "decoding error"); + return NULL; + } + if (res < 0) { + config->status = _PyStatus_NO_MEMORY(); + return NULL; + } + + // Copy to use the malloc() memory allocator + size_t size = (wlen + 1) * sizeof(wchar_t); + wchar_t *wstr2 = malloc(size); + if (wstr2 == NULL) { + PyMem_RawFree(wstr); + config->status = _PyStatus_NO_MEMORY(); + return NULL; + } + + memcpy(wstr2, wstr, size); + PyMem_RawFree(wstr); + return wstr2; +} + + +int +PyInitConfig_SetStr(PyInitConfig *config, const char *name, const char* value) +{ + void *raw_member; + const PyConfigSpec *spec = initconfig_prepare(config, name, &raw_member); + 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 = raw_member; + + *member = utf8_to_wstr(config, value); + if (*member == NULL) { + return -1; + } + return 0; +} + + +static int +_PyWideStringList_FromUTF8(PyInitConfig *config, PyWideStringList *list, + Py_ssize_t length, char * const *items) +{ + PyWideStringList wlist = _PyWideStringList_INIT; + size_t size = sizeof(wchar_t*) * length; + wlist.items = (wchar_t **)PyMem_RawMalloc(size); + if (wlist.items == NULL) { + config->status = _PyStatus_NO_MEMORY(); + return -1; + } + + for (Py_ssize_t i = 0; i < length; i++) { + wchar_t *arg = utf8_to_wstr(config, items[i]); + if (arg == NULL) { + _PyWideStringList_Clear(&wlist); + return -1; + } + wlist.items[i] = arg; + wlist.length++; + } + + _PyWideStringList_Clear(list); + *list = wlist; + return 0; +} + + +int +PyInitConfig_SetStrList(PyInitConfig *config, const char *name, + size_t length, char * const *items) +{ + void *raw_member; + const PyConfigSpec *spec = initconfig_prepare(config, name, &raw_member); + 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 = raw_member; + return _PyWideStringList_FromUTF8(config, list, length, items); +} + + +int +Py_InitializeFromInitConfig(PyInitConfig *config) +{ + _PyPreConfig_GetConfig(&config->preconfig, &config->config); + + config->status = Py_PreInitializeFromArgs( + &config->preconfig, + config->config.argv.length, + config->config.argv.items); + if (_PyStatus_EXCEPTION(config->status)) { + return -1; + } + + config->status = Py_InitializeFromConfig(&config->config); + if (_PyStatus_EXCEPTION(config->status)) { + return -1; + } + + return 0; +} + + +// --- PyConfig_Get() ------------------------------------------------------- + static const PyConfigSpec* config_generic_find_spec(const PyConfigSpec *spec, const char *name) { @@ -3554,7 +4031,7 @@ config_get(const PyConfig *config, const PyConfigSpec *spec, } } - void *member = config_spec_get_member(spec, config); + void *member = config_get_spec_member(config, spec); switch (spec->type) { case PyConfig_MEMBER_INT: case PyConfig_MEMBER_UINT: @@ -3761,7 +4238,7 @@ config_set_sys_flag(const PyConfigSpec *spec, int int_value) assert(spec->type == PyConfig_MEMBER_INT || spec->type == PyConfig_MEMBER_UINT || spec->type == PyConfig_MEMBER_BOOL); - int *member = config_spec_get_member(spec, config); + int *member = config_get_spec_member(config, spec); *member = int_value; // Set sys.dont_write_bytecode attribute