diff --git a/Include/datetime.h b/Include/datetime.h index b78cc0e8e2e5ac..54d80303a281ce 100644 --- a/Include/datetime.h +++ b/Include/datetime.h @@ -186,18 +186,31 @@ typedef struct { } PyDateTime_CAPI; #define PyDateTime_CAPSULE_NAME "datetime.datetime_CAPI" - +#define PyDateTime_INTERNAL_CAPSULE_NAME "datetime.datetime_CAPI_INTERNAL" /* This block is only used as part of the public API and should not be * included in _datetimemodule.c, which does not use the C API capsule. * See bpo-35081 for more details. * */ #ifndef _PY_DATETIME_IMPL -/* Define global variable for the C API and a macro for setting it. */ -static PyDateTime_CAPI *PyDateTimeAPI = NULL; +static PyDateTime_CAPI * +_PyDateTimeAPI_not_ready(void) +{ + return NULL; +} +static PyDateTime_CAPI *(*_PyDateTimeAPI_Get)(void) = _PyDateTimeAPI_not_ready; + +static inline void +_PyDateTimeAPI_Import(void) +{ + void *(*func)(void) = PyCapsule_Import(PyDateTime_INTERNAL_CAPSULE_NAME, 0); + if (func) { + _PyDateTimeAPI_Get = func(); + } +} -#define PyDateTime_IMPORT \ - PyDateTimeAPI = (PyDateTime_CAPI *)PyCapsule_Import(PyDateTime_CAPSULE_NAME, 0) +#define PyDateTimeAPI _PyDateTimeAPI_Get() +#define PyDateTime_IMPORT _PyDateTimeAPI_Import() /* Macro for access to the UTC singleton */ #define PyDateTime_TimeZone_UTC PyDateTimeAPI->TimeZone_UTC diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 570110893629cf..6946b394e68a82 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -91,7 +91,7 @@ def test_name_cleanup(self): if not name.startswith('__') and not name.endswith('__')) allowed = set(['MAXYEAR', 'MINYEAR', 'date', 'datetime', 'datetime_CAPI', 'time', 'timedelta', 'timezone', - 'tzinfo', 'UTC', 'sys']) + 'tzinfo', 'UTC', 'sys', 'datetime_CAPI_INTERNAL']) self.assertEqual(names - allowed, set([])) def test_divide_and_round(self): diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 020e8493e57c0c..2cbaac346e915c 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -2282,6 +2282,23 @@ def test_module_state_shared_in_global(self): subinterp_attr_id = os.read(r, 100) self.assertEqual(main_attr_id, subinterp_attr_id) + @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") + def test_datetime_capi_client(self): + script = textwrap.dedent(""" + import importlib.machinery + import importlib.util + fullname = '_test_datetime_capi_client' + origin = importlib.util.find_spec('_testmultiphase').origin + loader = importlib.machinery.ExtensionFileLoader(fullname, origin) + spec = importlib.util.spec_from_loader(fullname, loader) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + """) + exec(script) # run main interp first + exec(script) # run main interp twice + ret = support.run_in_subinterp(script) + self.assertEqual(ret, 0) + @requires_subinterpreters class InterpreterConfigTests(unittest.TestCase): diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 00015c5d8c23cb..06bfd76f6275c3 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -63,6 +63,51 @@ static datetime_state _datetime_global_state; #define STATIC_STATE() (&_datetime_global_state) +typedef struct { + PyInterpreterState *interp; + PyDateTime_CAPI *capi; +} CAPI_Cache; + +static CAPI_Cache apicache[2]; + +static inline void +set_datetime_capi_by_interp(PyDateTime_CAPI *capi) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + int i = interp == PyInterpreterState_Main() ? 0 : 1; + apicache[i].interp = interp; + apicache[i].capi = capi; +} + +static PyDateTime_CAPI * +_PyDateTimeAPI_Get(void) +{ + PyInterpreterState *interp = PyInterpreterState_Get(); + for (int i = 0; i < 2; i++) { + if (apicache[i].interp == interp) { + return apicache[i].capi; + } + } + PyDateTime_CAPI *capi = PyCapsule_Import(PyDateTime_CAPSULE_NAME, 0); + if (capi) { + set_datetime_capi_by_interp(capi); + } + return capi; +} + +static void * +_PyDateTimeAPI_Import(void) +{ + PyDateTime_CAPI *capi = PyCapsule_Import(PyDateTime_CAPSULE_NAME, 0); + if (capi) { + // PyInit__datetime() is not called when the module is already loaded + // with single-phase init. + set_datetime_capi_by_interp(capi); + return _PyDateTimeAPI_Get; + } + return NULL; +} + /* We require that C int be at least 32 bits, and use int virtually * everywhere. In just a few cases we use a temp long, where a Python * API returns a C long. In such cases, we have to ensure that the @@ -6944,6 +6989,20 @@ _datetime_exec(PyObject *module) goto error; } + capsule = PyCapsule_New(_PyDateTimeAPI_Import, + PyDateTime_INTERNAL_CAPSULE_NAME, NULL); + if (capsule == NULL) { + PyMem_Free(capi); + goto error; + } + if (PyModule_Add(module, "datetime_CAPI_INTERNAL", capsule) < 0) { + PyMem_Free(capi); + goto error; + } + + /* Ensure that the newest capi is used on multi-phase init */ + set_datetime_capi_by_interp(capi); + /* A 4-year cycle has an extra leap day over what we'd get from * pasting together 4 single years. */ diff --git a/Modules/_testmultiphase.c b/Modules/_testmultiphase.c index ca3d83233c5dd0..4bb372058076aa 100644 --- a/Modules/_testmultiphase.c +++ b/Modules/_testmultiphase.c @@ -969,3 +969,81 @@ PyInit__test_shared_gil_only(void) { return PyModuleDef_Init(&shared_gil_only_def); } + + +#include "datetime.h" + +static int +datetime_capi_import_with_error(void) +{ + static int is_datetime_multiphase = -1; + int ismain = PyInterpreterState_Get() == PyInterpreterState_Main(); + if (ismain && is_datetime_multiphase < 0) { + PyObject *module = PyImport_ImportModule("_datetime"); + if (module == NULL) { + return -1; + } + PyModuleDef *def = PyModule_GetDef(module); + Py_DECREF(module); + if (def && def->m_size >= 0) { + is_datetime_multiphase = 1; + } + else { + is_datetime_multiphase = 0; + } + } + if (is_datetime_multiphase < 0) { + PyErr_SetString(PyExc_AssertionError, + "Main interpreter must be loaded first."); + return -1; + } + + _PyDateTimeAPI_Import(); + if (!PyErr_Occurred()) { + return 0; + } +#ifdef Py_GIL_DISABLED + if (!ismain && !is_datetime_multiphase) { + // _datetime module and Capsule are not imported + PyErr_WriteUnraisable(NULL); + return 0; + } +#endif + return -1; +} + +static int +datetime_capi_client_exec(PyObject *m) +{ + _PyDateTimeAPI_Get = _PyDateTimeAPI_not_ready; + if (_PyDateTimeAPI_Get() != NULL) { + PyErr_SetString(PyExc_AssertionError, + "DateTime API is expected to remain NULL."); + return -1; + } + if (datetime_capi_import_with_error() < 0) { + return -1; + } + if (PyDateTimeAPI != PyCapsule_Import(PyDateTime_CAPSULE_NAME, 0)) { + PyErr_SetString(PyExc_AssertionError, + "DateTime API does not match Capsule CAPI."); + return -1; + } + PyErr_Clear(); + return 0; +} + +static PyModuleDef_Slot datetime_capi_client_slots[] = { + {Py_mod_exec, datetime_capi_client_exec}, + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, + {0, NULL}, +}; + +static PyModuleDef datetime_capi_client_def = TEST_MODULE_DEF( + "_testmultiphase_datetime_capi_client", datetime_capi_client_slots, NULL); + +PyMODINIT_FUNC +PyInit__test_datetime_capi_client(void) +{ + return PyModuleDef_Init(&datetime_capi_client_def); +} diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index 1b8cccf80872c8..ca143d98032ea1 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -450,7 +450,7 @@ Modules/_tkinter.c - trbInCmd - ## initialized once ## other -Include/datetime.h - PyDateTimeAPI - +Include/datetime.h - _PyDateTimeAPI_Get - Modules/_ctypes/cfield.c _ctypes_get_fielddesc initialized - Modules/_ctypes/malloc_closure.c - _pagesize - Modules/_cursesmodule.c - initialised - @@ -468,6 +468,7 @@ Modules/readline.c - libedit_history_start - Modules/_ctypes/cfield.c - formattable - Modules/_ctypes/malloc_closure.c - free_list - Modules/_curses_panel.c - lop - +Modules/_datetimemodule.c - apicache - Modules/_ssl/debughelpers.c _PySSL_keylog_callback lock - Modules/_tkinter.c - quitMainLoop - Modules/_tkinter.c - errorInCmd - diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index c4dbdcfe80bb4a..f21755e486e14e 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -591,6 +591,7 @@ Modules/_testmultiphase.c - slots_exec_unreported_exception - Modules/_testmultiphase.c - slots_nonmodule_with_exec_slots - Modules/_testmultiphase.c - testexport_methods - Modules/_testmultiphase.c - uninitialized_def - +Modules/_testmultiphase.c datetime_capi_import_with_error is_datetime_multiphase - Modules/_testsinglephase.c - global_state - Modules/_xxtestfuzz/_xxtestfuzz.c - _fuzzmodule - Modules/_xxtestfuzz/_xxtestfuzz.c - module_methods -