diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 7d9eefb..bffee2d 100644 --- a/Doc/c-api/init.rst +++ b/Doc/c-api/init.rst @@ -821,6 +821,14 @@ been created. :c:func:`PyThreadState_Clear`. +.. c:function:: PY_INT64_T PyInterpreterState_GetID(PyInterpreterState *interp) + + Return the interpreter's unique ID. If there was any error in doing + so then -1 is returned and an error is set. + + .. versionadded:: 3.7 + + .. c:function:: PyObject* PyThreadState_GetDict() Return a dictionary in which extensions can store thread-specific state diff --git a/Include/pystate.h b/Include/pystate.h index afc3c0c..bc390610 100644 --- a/Include/pystate.h +++ b/Include/pystate.h @@ -28,6 +28,8 @@ typedef struct _is { struct _is *next; struct _ts *tstate_head; + PY_INT64_T id; + PyObject *modules; PyObject *modules_by_index; PyObject *sysdict; @@ -154,9 +156,13 @@ typedef struct _ts { #endif +#ifndef Py_LIMITED_API +PyAPI_FUNC(void) _PyInterpreterState_Init(void); +#endif /* !Py_LIMITED_API */ PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_New(void); PyAPI_FUNC(void) PyInterpreterState_Clear(PyInterpreterState *); PyAPI_FUNC(void) PyInterpreterState_Delete(PyInterpreterState *); +PyAPI_FUNC(PY_INT64_T) PyInterpreterState_GetID(PyInterpreterState *); #ifndef Py_LIMITED_API PyAPI_FUNC(int) _PyState_AddModule(PyObject*, struct PyModuleDef*); #endif /* !Py_LIMITED_API */ diff --git a/Lib/test/test_capi.py b/Lib/test/test_capi.py index 6c3625d..62ebb0a 100644 --- a/Lib/test/test_capi.py +++ b/Lib/test/test_capi.py @@ -1,6 +1,7 @@ # Run the _testcapi module tests (tests for the Python/C API): by defn, # these are all functions _testcapi exports whose name begins with 'test_'. +from collections import namedtuple import os import pickle import random @@ -384,12 +385,85 @@ class EmbeddingTests(unittest.TestCase): return out, err def test_subinterps(self): - # This is just a "don't crash" test out, err = self.run_embedded_interpreter() - if support.verbose: - print() - print(out) - print(err) + self.assertEqual(err, "") + + # The output from _testembed looks like this: + # --- Pass 0 --- + # interp 0 <0x1cf9330>, thread state <0x1cf9700>: id(modules) = 139650431942728 + # interp 1 <0x1d4f690>, thread state <0x1d35350>: id(modules) = 139650431165784 + # interp 2 <0x1d5a690>, thread state <0x1d99ed0>: id(modules) = 139650413140368 + # interp 3 <0x1d4f690>, thread state <0x1dc3340>: id(modules) = 139650412862200 + # interp 0 <0x1cf9330>, thread state <0x1cf9700>: id(modules) = 139650431942728 + # --- Pass 1 --- + # ... + + interp_pat = (r"^interp (\d+) <(0x[\da-f]+)>, " + r"thread state <(0x[\da-f]+)>: " + r"id\(modules\) = ([\d]+)$") + Interp = namedtuple("Interp", "id interp tstate modules") + + main = None + lastmain = None + numinner = None + numloops = 0 + for line in out.splitlines(): + if line == "--- Pass {} ---".format(numloops): + if numinner is not None: + self.assertEqual(numinner, 5) + if support.verbose: + print(line) + lastmain = main + main = None + mainid = 0 + numloops += 1 + numinner = 0 + continue + numinner += 1 + + self.assertLessEqual(numinner, 5) + match = re.match(interp_pat, line) + if match is None: + self.assertRegex(line, interp_pat) + + # The last line in the loop should be the same as the first. + if numinner == 5: + self.assertEqual(match.groups(), main) + continue + + # Parse the line from the loop. The first line is the main + # interpreter and the 3 afterward are subinterpreters. + interp = Interp(*match.groups()) + if support.verbose: + print(interp) + if numinner == 1: + main = interp + id = str(mainid) + else: + subid = mainid + numinner - 1 + id = str(subid) + + # Validate the loop line for each interpreter. + self.assertEqual(interp.id, id) + self.assertTrue(interp.interp) + self.assertTrue(interp.tstate) + self.assertTrue(interp.modules) + if interp is main: + if lastmain is not None: + # A new main interpreter may have the same interp + # and/or tstate pointer as an earlier finalized/ + # destroyed one. So we do not check interp or + # tstate here. + self.assertNotEqual(interp.modules, lastmain.modules) + else: + # A new subinterpreter may have the same + # PyInterpreterState pointer as a previous one if + # the earlier one has already been destroyed. So + # we compare with the main interpreter. The same + # applies to tstate. + self.assertNotEqual(interp.interp, main.interp) + self.assertNotEqual(interp.tstate, main.tstate) + self.assertNotEqual(interp.modules, main.modules) @staticmethod def _get_default_pipe_encoding(): diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 3968399..9b8d7b8 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -22,9 +22,12 @@ static void _testembed_Py_Initialize(void) static void print_subinterp(void) { - /* Just output some debug stuff */ + /* Output information about the interpreter in the format + expected in Lib/test/test_capi.py (test_subinterps). */ PyThreadState *ts = PyThreadState_Get(); - printf("interp %p, thread state %p: ", ts->interp, ts); + PY_INT64_T id = PyInterpreterState_GetID(ts->interp); + printf("interp %lu <%p>, thread state <%p>: ", + id, ts->interp, ts); fflush(stdout); PyRun_SimpleString( "import sys;" diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 06030c3..b21dc5e 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -344,6 +344,7 @@ _Py_InitializeEx_Private(int install_sigs, int install_importlib) _PyRandom_Init(); + _PyInterpreterState_Init(); interp = PyInterpreterState_New(); if (interp == NULL) Py_FatalError("Py_Initialize: can't make first interpreter"); diff --git a/Python/pystate.c b/Python/pystate.c index 65c244e..11bb5fb 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -65,6 +65,23 @@ PyThreadFrameGetter _PyThreadState_GetFrame = NULL; static void _PyGILState_NoteThreadState(PyThreadState* tstate); #endif +/* _next_interp_id is an auto-numbered sequence of small integers. + It gets initialized in _PyInterpreterState_Init(), which is called + in Py_Initialize(), and used in PyInterpreterState_New(). A negative + interpreter ID indicates an error occurred. The main interpreter + will always have an ID of 0. Overflow results in a RuntimeError. + If that becomes a problem later then we can adjust, e.g. by using + a Python int. + + We initialize this to -1 so that the pre-Py_Initialize() value + results in an error. */ +static PY_INT64_T _next_interp_id = -1; + +void +_PyInterpreterState_Init(void) +{ + _next_interp_id = 0; +} PyInterpreterState * PyInterpreterState_New(void) @@ -103,6 +120,15 @@ PyInterpreterState_New(void) HEAD_LOCK(); interp->next = interp_head; interp_head = interp; + if (_next_interp_id < 0) { + /* overflow or Py_Initialize() not called! */ + PyErr_SetString(PyExc_RuntimeError, + "failed to get an interpreter ID"); + interp = NULL; + } else { + interp->id = _next_interp_id; + _next_interp_id += 1; + } HEAD_UNLOCK(); } @@ -170,6 +196,17 @@ PyInterpreterState_Delete(PyInterpreterState *interp) } +PY_INT64_T +PyInterpreterState_GetID(PyInterpreterState *interp) +{ + if (interp == NULL) { + PyErr_SetString(PyExc_RuntimeError, "no interpreter provided"); + return -1; + } + return interp->id; +} + + /* Default implementation for _PyThreadState_GetFrame */ static struct _frame * threadstate_getframe(PyThreadState *self)