diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst index 7d9eefb..10f03fc 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:: unsigned long PyInterpreterState_GetID(PyInterpreterState *interp) + + Return the interpreter's unique ID. If there was any error in doing + so then 0 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..e8755b6 100644 --- a/Include/pystate.h +++ b/Include/pystate.h @@ -28,6 +28,8 @@ typedef struct _is { struct _is *next; struct _ts *tstate_head; + unsigned long id; // XXX random hash? UUID? + PyObject *modules; PyObject *modules_by_index; PyObject *sysdict; @@ -157,6 +159,7 @@ typedef struct _ts { PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_New(void); PyAPI_FUNC(void) PyInterpreterState_Clear(PyInterpreterState *); PyAPI_FUNC(void) PyInterpreterState_Delete(PyInterpreterState *); +PyAPI_FUNC(unsigned long) 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..4ac3627 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 ('0' <0x1cf9330>), thread state <0x1cf9700>: id(modules) = 139650431942728 + # interp 1 ('0_1' <0x1d4f690>), thread state <0x1d35350>: id(modules) = 139650431165784 + # interp 2 ('0_2' <0x1d5a690>), thread state <0x1d99ed0>: id(modules) = 139650413140368 + # interp 3 ('0_3' <0x1d4f690>), thread state <0x1dc3340>: id(modules) = 139650412862200 + # interp 0 ('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 = numloops * 4 + 1 + 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..d7b6b0d 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -24,7 +24,9 @@ static void print_subinterp(void) { /* Just output some debug stuff */ PyThreadState *ts = PyThreadState_Get(); - printf("interp %p, thread state %p: ", ts->interp, ts); + unsigned long id = PyInterpreterState_GetID(ts->interp); + printf("interp %lu <%p>, thread state <%p>: ", + id, ts->interp, ts); fflush(stdout); PyRun_SimpleString( "import sys;" @@ -56,6 +58,7 @@ static void test_repeated_init_and_subinterpreters(void) PyThreadState_Swap(NULL); for (j=0; j<3; j++) { + unsigned long mis = PyInterpreterState_GetID(mainstate->interp); substate = Py_NewInterpreter(); print_subinterp(); Py_EndInterpreter(substate); diff --git a/Python/pystate.c b/Python/pystate.c index 65c244e..bce0477 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -66,6 +66,8 @@ static void _PyGILState_NoteThreadState(PyThreadState* tstate); #endif +static unsigned long _next_id = 1; // 0 is reserved for errors. + PyInterpreterState * PyInterpreterState_New(void) { @@ -103,6 +105,8 @@ PyInterpreterState_New(void) HEAD_LOCK(); interp->next = interp_head; interp_head = interp; + interp->id = _next_id; + _next_id += 1; // XXX overflow... HEAD_UNLOCK(); } @@ -170,6 +174,17 @@ PyInterpreterState_Delete(PyInterpreterState *interp) } +unsigned long +PyInterpreterState_GetID(PyInterpreterState *interp) +{ + if (interp == NULL) { + PyErr_SetString(PyExc_RuntimeError, "no interpreter provided"); + return 0; + } + return interp->id; +} + + /* Default implementation for _PyThreadState_GetFrame */ static struct _frame * threadstate_getframe(PyThreadState *self)