From 67a88c264257f0404aa7bb2d275b95253de1d0d5 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 24 Oct 2023 12:45:38 -0600 Subject: [PATCH 01/22] Factor out _Py_excinfo. --- Include/internal/pycore_pyerrors.h | 24 ++++ Modules/_xxsubinterpretersmodule.c | 211 +++++++++++------------------ Python/errors.c | 175 ++++++++++++++++++++++++ 3 files changed, 280 insertions(+), 130 deletions(-) diff --git a/Include/internal/pycore_pyerrors.h b/Include/internal/pycore_pyerrors.h index 184eb35e52b47b..a05a626a5cdcf7 100644 --- a/Include/internal/pycore_pyerrors.h +++ b/Include/internal/pycore_pyerrors.h @@ -68,6 +68,30 @@ extern PyStatus _PyErr_InitTypes(PyInterpreterState *); extern void _PyErr_FiniTypes(PyInterpreterState *); +/* exception snapshots */ + +// Ultimately we'd like to preserve enough information about the +// exception and traceback that we could re-constitute (or at least +// simulate, a la traceback.TracebackException), and even chain, a copy +// of the exception in the calling interpreter. + +typedef struct _excinfo { + const char *type; + const char *msg; +} _Py_excinfo; + +PyAPI_FUNC(void) _Py_excinfo_Clear(_Py_excinfo *info); +PyAPI_FUNC(int) _Py_excinfo_Copy(_Py_excinfo *dest, _Py_excinfo *src); +PyAPI_FUNC(const char *) _Py_excinfo_InitFromException( + _Py_excinfo *info, + PyObject *exc); +PyAPI_FUNC(void) _Py_excinfo_Apply(_Py_excinfo *info, PyObject *exctype); +PyAPI_FUNC(const char *) _Py_excinfo_AsUTF8( + _Py_excinfo *info, + char *buf, + size_t bufsize); + + /* other API */ static inline PyObject* _PyErr_Occurred(PyThreadState *tstate) diff --git a/Modules/_xxsubinterpretersmodule.c b/Modules/_xxsubinterpretersmodule.c index 640fd69061d929..a281ae12ab3ed1 100644 --- a/Modules/_xxsubinterpretersmodule.c +++ b/Modules/_xxsubinterpretersmodule.c @@ -7,6 +7,7 @@ #include "Python.h" #include "pycore_crossinterp.h" // struct _xid +#include "pycore_pyerrors.h" // _Py_excinfo #include "pycore_initconfig.h" // _PyErr_SetFromPyStatus() #include "pycore_modsupport.h" // _PyArg_BadArgument() #include "pycore_pyerrors.h" // _PyErr_ChainExceptions1() @@ -113,6 +114,85 @@ clear_module_state(module_state *state) } +/* exception info ***********************************************************/ + +#define ERR_NOT_SET 0 +#define ERR_UNCAUGHT_EXCEPTION 1 +#define ERR_NO_MEMORY 2 +#define ERR_ALREADY_RUNNING 3 + +static const char * +_excinfo_bind(PyObject *exc, _Py_excinfo *info, int *p_code) +{ + assert(exc != NULL); + + const char *failure = _Py_excinfo_InitFromException(info, exc); + if (failure != NULL) { + PyErr_Clear(); + *p_code = ERR_NO_MEMORY; + return failure; + } + + assert(!PyErr_Occurred()); + *p_code = ERR_UNCAUGHT_EXCEPTION; + return NULL; +} + +typedef struct _sharedexception { + PyInterpreterState *interp; + int code; + _Py_excinfo uncaught; +} _sharedexception; + +static const char * +_sharedexception_bind(PyObject *exc, int code, _sharedexception *sharedexc) +{ + if (sharedexc->interp == NULL) { + sharedexc->interp = PyInterpreterState_Get(); + } + + const char *failure = NULL; + if (code == ERR_NOT_SET) { + failure = _excinfo_bind(exc, &sharedexc->uncaught, &sharedexc->code); + assert(sharedexc->code != ERR_NOT_SET); + } + else { + assert(exc == NULL); + assert(code != ERR_UNCAUGHT_EXCEPTION); + sharedexc->code = code; + _Py_excinfo_Clear(&sharedexc->uncaught); + } + return failure; +} + +static void +_sharedexception_apply(_sharedexception *exc, PyObject *wrapperclass) +{ + if (exc->code == ERR_UNCAUGHT_EXCEPTION) { + _Py_excinfo_Apply(&exc->uncaught, wrapperclass); + } + else { + assert(exc->code != ERR_NOT_SET); + if (exc->code == ERR_NO_MEMORY) { + PyErr_NoMemory(); + } + else if (exc->code == ERR_ALREADY_RUNNING) { + assert(exc->interp != NULL); + assert(_PyInterpreterState_IsRunningMain(exc->interp)); + _PyInterpreterState_FailIfRunningMain(exc->interp); + } + else { +#ifdef Py_DEBUG + Py_UNREACHABLE(); +#else + PyErr_Format(PyExc_RuntimeError, "unsupported error code %d", code); +#endif + } + assert(PyErr_Occurred()); + } +} + + /* data-sharing-specific code ***********************************************/ struct _sharednsitem { @@ -240,135 +320,6 @@ _sharedns_apply(_sharedns *shared, PyObject *ns) return 0; } -// Ultimately we'd like to preserve enough information about the -// exception and traceback that we could re-constitute (or at least -// simulate, a la traceback.TracebackException), and even chain, a copy -// of the exception in the calling interpreter. - -typedef struct _sharedexception { - PyInterpreterState *interp; -#define ERR_NOT_SET 0 -#define ERR_NO_MEMORY 1 -#define ERR_ALREADY_RUNNING 2 - int code; - const char *name; - const char *msg; -} _sharedexception; - -static const struct _sharedexception no_exception = { - .name = NULL, - .msg = NULL, -}; - -static void -_sharedexception_clear(_sharedexception *exc) -{ - if (exc->name != NULL) { - PyMem_RawFree((void *)exc->name); - } - if (exc->msg != NULL) { - PyMem_RawFree((void *)exc->msg); - } -} - -static const char * -_sharedexception_bind(PyObject *exc, int code, _sharedexception *sharedexc) -{ - if (sharedexc->interp == NULL) { - sharedexc->interp = PyInterpreterState_Get(); - } - - if (code != ERR_NOT_SET) { - assert(exc == NULL); - assert(code > 0); - sharedexc->code = code; - return NULL; - } - - assert(exc != NULL); - const char *failure = NULL; - - PyObject *nameobj = PyUnicode_FromString(Py_TYPE(exc)->tp_name); - if (nameobj == NULL) { - failure = "unable to format exception type name"; - code = ERR_NO_MEMORY; - goto error; - } - sharedexc->name = _copy_raw_string(nameobj); - Py_DECREF(nameobj); - if (sharedexc->name == NULL) { - if (PyErr_ExceptionMatches(PyExc_MemoryError)) { - failure = "out of memory copying exception type name"; - } else { - failure = "unable to encode and copy exception type name"; - } - code = ERR_NO_MEMORY; - goto error; - } - - if (exc != NULL) { - PyObject *msgobj = PyUnicode_FromFormat("%S", exc); - if (msgobj == NULL) { - failure = "unable to format exception message"; - code = ERR_NO_MEMORY; - goto error; - } - sharedexc->msg = _copy_raw_string(msgobj); - Py_DECREF(msgobj); - if (sharedexc->msg == NULL) { - if (PyErr_ExceptionMatches(PyExc_MemoryError)) { - failure = "out of memory copying exception message"; - } else { - failure = "unable to encode and copy exception message"; - } - code = ERR_NO_MEMORY; - goto error; - } - } - - return NULL; - -error: - assert(failure != NULL); - PyErr_Clear(); - _sharedexception_clear(sharedexc); - *sharedexc = (_sharedexception){ - .interp = sharedexc->interp, - .code = code, - }; - return failure; -} - -static void -_sharedexception_apply(_sharedexception *exc, PyObject *wrapperclass) -{ - if (exc->name != NULL) { - assert(exc->code == ERR_NOT_SET); - if (exc->msg != NULL) { - PyErr_Format(wrapperclass, "%s: %s", exc->name, exc->msg); - } - else { - PyErr_SetString(wrapperclass, exc->name); - } - } - else if (exc->msg != NULL) { - assert(exc->code == ERR_NOT_SET); - PyErr_SetString(wrapperclass, exc->msg); - } - else if (exc->code == ERR_NO_MEMORY) { - PyErr_NoMemory(); - } - else if (exc->code == ERR_ALREADY_RUNNING) { - assert(exc->interp != NULL); - assert(_PyInterpreterState_IsRunningMain(exc->interp)); - _PyInterpreterState_FailIfRunningMain(exc->interp); - } - else { - assert(exc->code == ERR_NOT_SET); - PyErr_SetNone(wrapperclass); - } -} - /* Python code **************************************************************/ @@ -549,7 +500,7 @@ _run_script(PyInterpreterState *interp, } _PyInterpreterState_SetNotRunningMain(interp); - *sharedexc = no_exception; + *sharedexc = (_sharedexception){0}; return 0; error: diff --git a/Python/errors.c b/Python/errors.c index 15af39b10dc07e..75b55f558a524d 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1913,3 +1913,178 @@ PyErr_ProgramTextObject(PyObject *filename, int lineno) { return _PyErr_ProgramDecodedTextObject(filename, lineno, NULL); } + + +/***********************/ +/* exception snapshots */ +/***********************/ + +static const char * +_copy_raw_string(const char *str) +{ + char *copied = PyMem_RawMalloc(strlen(str)+1); + if (copied == NULL) { + return NULL; + } + strcpy(copied, str); + return copied; +} + +static int +_exc_type_name_as_utf8(PyObject *exc, const char **p_typename) +{ + // XXX Use PyObject_GetAttrString(Py_TYPE(exc), '__name__')? + PyObject *nameobj = PyUnicode_FromString(Py_TYPE(exc)->tp_name); + if (nameobj == NULL) { + assert(PyErr_Occurred()); + *p_typename = "unable to format exception type name"; + return -1; + } + const char *name = PyUnicode_AsUTF8(nameobj); + if (name == NULL) { + assert(PyErr_Occurred()); + Py_DECREF(nameobj); + *p_typename = "unable to encode exception type name"; + return -1; + } + name = _copy_raw_string(name); + Py_DECREF(nameobj); + if (name == NULL) { + *p_typename = "out of memory copying exception type name"; + return -1; + } + *p_typename = name; + return 0; +} + +static int +_exc_msg_as_utf8(PyObject *exc, const char **p_msg) +{ + PyObject *msgobj = PyUnicode_FromFormat("%S", exc); + if (msgobj == NULL) { + assert(PyErr_Occurred()); + *p_msg = "unable to format exception message"; + return -1; + } + const char *msg = PyUnicode_AsUTF8(msgobj); + if (msg == NULL) { + assert(PyErr_Occurred()); + Py_DECREF(msgobj); + *p_msg = "unable to encode exception message"; + return -1; + } + msg = _copy_raw_string(msg); + Py_DECREF(msgobj); + if (msg == NULL) { + assert(PyErr_ExceptionMatches(PyExc_MemoryError)); + *p_msg = "out of memory copying exception message"; + return -1; + } + *p_msg = msg; + return 0; +} + +void +_Py_excinfo_Clear(_Py_excinfo *info) +{ + if (info->type != NULL) { + PyMem_RawFree((void *)info->type); + } + if (info->msg != NULL) { + PyMem_RawFree((void *)info->msg); + } + *info = (_Py_excinfo){ NULL }; +} + +int +_Py_excinfo_Copy(_Py_excinfo *dest, _Py_excinfo *src) +{ + // XXX Clear dest first? + + if (src->type == NULL) { + dest->type = NULL; + } + else { + dest->type = _copy_raw_string(src->type); + if (dest->type == NULL) { + return -1; + } + } + + if (src->msg == NULL) { + dest->msg = NULL; + } + else { + dest->msg = _copy_raw_string(src->msg); + if (dest->msg == NULL) { + return -1; + } + } + + return 0; +} + +const char * +_Py_excinfo_InitFromException(_Py_excinfo *info, PyObject *exc) +{ + assert(exc != NULL); + + // Extract the exception type name. + const char *typename = NULL; + if (_exc_type_name_as_utf8(exc, &typename) < 0) { + assert(typename != NULL); + return typename; + } + + // Extract the exception message. + const char *msg = NULL; + if (_exc_msg_as_utf8(exc, &msg) < 0) { + assert(msg != NULL); + return msg; + } + + info->type = typename; + info->msg = msg; + return NULL; +} + +void +_Py_excinfo_Apply(_Py_excinfo *info, PyObject *exctype) +{ + if (info->type != NULL) { + if (info->msg != NULL) { + PyErr_Format(exctype, "%s: %s", info->type, info->msg); + } + else { + PyErr_SetString(exctype, info->type); + } + } + else if (info->msg != NULL) { + PyErr_SetString(exctype, info->msg); + } + else { + PyErr_SetNone(exctype); + } +} + +const char * +_Py_excinfo_AsUTF8(_Py_excinfo *info, char *buf, size_t bufsize) +{ + // XXX Dynamically allocate if no buf provided? + assert(buf != NULL); + if (info->type != NULL) { + if (info->msg != NULL) { + snprintf(buf, bufsize, "%s: %s", info->type, info->msg); + return buf; + } + else { + return info->type; + } + } + else if (info->msg != NULL) { + return info->msg; + } + else { + return NULL; + } +} From a9ea9ac49bb10995edf63bdfb95812a2d59b1f71 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 25 Oct 2023 15:45:41 -0600 Subject: [PATCH 02/22] Extract _PyXI_errcode. --- Include/internal/pycore_crossinterp.h | 17 ++++++ Modules/_xxsubinterpretersmodule.c | 74 +++++++++++++-------------- Python/crossinterp.c | 42 +++++++++++++++ 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 59e4cd9ece780d..d89d4cd9f40148 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -133,6 +133,23 @@ PyAPI_FUNC(int) _PyCrossInterpreterData_UnregisterClass(PyTypeObject *); PyAPI_FUNC(crossinterpdatafunc) _PyCrossInterpreterData_Lookup(PyObject *); +/***************************/ +/* short-term data sharing */ +/***************************/ + +typedef enum error_code { + _PyXI_ERR_NO_ERROR = 0, + _PyXI_ERR_UNCAUGHT_EXCEPTION = -1, + _PyXI_ERR_OTHER = -2, + _PyXI_ERR_NO_MEMORY = -3, + _PyXI_ERR_ALREADY_RUNNING = -4, +} _PyXI_errcode; + +PyAPI_FUNC(int) _PyXI_ApplyErrorCode( + _PyXI_errcode code, + PyInterpreterState *interp); + + #ifdef __cplusplus } #endif diff --git a/Modules/_xxsubinterpretersmodule.c b/Modules/_xxsubinterpretersmodule.c index a281ae12ab3ed1..8dc74db1c4612e 100644 --- a/Modules/_xxsubinterpretersmodule.c +++ b/Modules/_xxsubinterpretersmodule.c @@ -116,49 +116,52 @@ clear_module_state(module_state *state) /* exception info ***********************************************************/ -#define ERR_NOT_SET 0 -#define ERR_UNCAUGHT_EXCEPTION 1 -#define ERR_NO_MEMORY 2 -#define ERR_ALREADY_RUNNING 3 - static const char * -_excinfo_bind(PyObject *exc, _Py_excinfo *info, int *p_code) +_excinfo_bind(PyObject *exc, _Py_excinfo *info, _PyXI_errcode *p_code) { assert(exc != NULL); const char *failure = _Py_excinfo_InitFromException(info, exc); if (failure != NULL) { + // We failed to initialize info->uncaught. + // XXX Print the excobj/traceback? Emit a warning? + // XXX Print the current exception/traceback? + if (PyErr_ExceptionMatches(PyExc_MemoryError)) { + *p_code = _PyXI_ERR_NO_MEMORY; + } + else { + *p_code = _PyXI_ERR_OTHER; + } PyErr_Clear(); - *p_code = ERR_NO_MEMORY; - return failure; } - - assert(!PyErr_Occurred()); - *p_code = ERR_UNCAUGHT_EXCEPTION; - return NULL; + else { + assert(!PyErr_Occurred()); + *p_code = _PyXI_ERR_UNCAUGHT_EXCEPTION; + } + return failure; } typedef struct _sharedexception { PyInterpreterState *interp; - int code; + _PyXI_errcode code; _Py_excinfo uncaught; } _sharedexception; static const char * -_sharedexception_bind(PyObject *exc, int code, _sharedexception *sharedexc) +_sharedexception_bind(PyObject *exc, _PyXI_errcode code, _sharedexception *sharedexc) { if (sharedexc->interp == NULL) { sharedexc->interp = PyInterpreterState_Get(); } const char *failure = NULL; - if (code == ERR_NOT_SET) { + if (code == _PyXI_ERR_UNCAUGHT_EXCEPTION) { failure = _excinfo_bind(exc, &sharedexc->uncaught, &sharedexc->code); - assert(sharedexc->code != ERR_NOT_SET); + assert(sharedexc->code != _PyXI_ERR_NO_ERROR); } else { assert(exc == NULL); - assert(code != ERR_UNCAUGHT_EXCEPTION); + assert(code != _PyXI_ERR_NO_ERROR); sharedexc->code = code; _Py_excinfo_Clear(&sharedexc->uncaught); } @@ -168,26 +171,12 @@ _sharedexception_bind(PyObject *exc, int code, _sharedexception *sharedexc) static void _sharedexception_apply(_sharedexception *exc, PyObject *wrapperclass) { - if (exc->code == ERR_UNCAUGHT_EXCEPTION) { + if (exc->code == _PyXI_ERR_UNCAUGHT_EXCEPTION) { _Py_excinfo_Apply(&exc->uncaught, wrapperclass); } else { - assert(exc->code != ERR_NOT_SET); - if (exc->code == ERR_NO_MEMORY) { - PyErr_NoMemory(); - } - else if (exc->code == ERR_ALREADY_RUNNING) { - assert(exc->interp != NULL); - assert(_PyInterpreterState_IsRunningMain(exc->interp)); - _PyInterpreterState_FailIfRunningMain(exc->interp); - } - else { -#ifdef Py_DEBUG - Py_UNREACHABLE(); -#else - PyErr_Format(PyExc_RuntimeError, "unsupported error code %d", code); -#endif - } + assert(exc->code != _PyXI_ERR_NO_ERROR); + (void)_PyXI_ApplyErrorCode(exc->code, exc->interp); assert(PyErr_Occurred()); } } @@ -444,7 +433,8 @@ _run_script(PyInterpreterState *interp, const char *codestr, Py_ssize_t codestrlen, _sharedns *shared, _sharedexception *sharedexc, int flags) { - int errcode = ERR_NOT_SET; + PyObject *excval = NULL; + _PyXI_errcode errcode = _PyXI_ERR_UNCAUGHT_EXCEPTION; if (_PyInterpreterState_SetRunningMain(interp) < 0) { assert(PyErr_Occurred()); @@ -452,11 +442,10 @@ _run_script(PyInterpreterState *interp, // be more efficient to leave the exception in place and return // immediately. However, life is simpler if we don't. PyErr_Clear(); - errcode = ERR_ALREADY_RUNNING; + errcode = _PyXI_ERR_ALREADY_RUNNING; goto error; } - PyObject *excval = NULL; PyObject *main_mod = PyUnstable_InterpreterState_GetMainModule(interp); if (main_mod == NULL) { goto error; @@ -504,7 +493,14 @@ _run_script(PyInterpreterState *interp, return 0; error: - excval = PyErr_GetRaisedException(); + assert(errcode != _PyXI_ERR_NO_ERROR); + if (errcode == _PyXI_ERR_UNCAUGHT_EXCEPTION) { + assert(PyErr_Occurred()); + excval = PyErr_GetRaisedException(); + } + else { + assert(!PyErr_Occurred()); + } const char *failure = _sharedexception_bind(excval, errcode, sharedexc); if (failure != NULL) { fprintf(stderr, @@ -518,7 +514,7 @@ _run_script(PyInterpreterState *interp, PyErr_Display(NULL, excval, NULL); Py_DECREF(excval); } - if (errcode != ERR_ALREADY_RUNNING) { + if (errcode != _PyXI_ERR_ALREADY_RUNNING) { _PyInterpreterState_SetNotRunningMain(interp); } assert(!PyErr_Occurred()); diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 74f1d6ecef1329..70c3fdedc2f228 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -625,3 +625,45 @@ _register_builtins_for_crossinterpreter_data(struct _xidregistry *xidregistry) Py_FatalError("could not register str for cross-interpreter sharing"); } } + + +/***************************/ +/* short-term data sharing */ +/***************************/ + +/* error codes */ + +int +_PyXI_ApplyErrorCode(_PyXI_errcode code, PyInterpreterState *interp) +{ + assert(!PyErr_Occurred()); + switch (code) { + case _PyXI_ERR_NO_ERROR: // fall through + case _PyXI_ERR_UNCAUGHT_EXCEPTION: + // There is nothing to apply. +#ifdef Py_DEBUG + Py_UNREACHABLE(); +#endif + return 0; + case _PyXI_ERR_OTHER: + // XXX msg? + PyErr_SetNone(PyExc_RuntimeError); + break; + case _PyXI_ERR_NO_MEMORY: + PyErr_NoMemory(); + break; + case _PyXI_ERR_ALREADY_RUNNING: + assert(interp != NULL); + assert(_PyInterpreterState_IsRunningMain(interp)); + _PyInterpreterState_FailIfRunningMain(interp); + break; + default: +#ifdef Py_DEBUG + Py_UNREACHABLE(); +#else + PyErr_Format(PyExc_RuntimeError, "unsupported error code %d", code); +#endif + } + assert(PyErr_Occurred()); + return -1; +} From 33d91af4256394822840c9330ca234659579593e Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 25 Oct 2023 11:56:31 -0600 Subject: [PATCH 03/22] Extract _PyXI_exception_info. --- Include/internal/pycore_crossinterp.h | 25 ++++++++ Modules/_xxsubinterpretersmodule.c | 85 +++------------------------ Python/crossinterp.c | 56 ++++++++++++++++++ 3 files changed, 90 insertions(+), 76 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index d89d4cd9f40148..4fe924ea3e2caa 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -8,6 +8,12 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif +#include "pycore_pyerrors.h" + + +/**************************/ +/* cross-interpreter data */ +/**************************/ /***************************/ /* cross-interpreter calls */ @@ -150,6 +156,25 @@ PyAPI_FUNC(int) _PyXI_ApplyErrorCode( PyInterpreterState *interp); +typedef struct _sharedexception { + // The originating interpreter. + PyInterpreterState *interp; + // The kind of error to propagate. + _PyXI_errcode code; + // The exception information to propagate, if applicable. + // This is populated only for _PyXI_ERR_UNCAUGHT_EXCEPTION. + _Py_excinfo uncaught; +} _PyXI_exception_info; + +PyAPI_FUNC(const char *) _PyXI_InitExceptionInfo( + _PyXI_exception_info *info, + PyObject *exc, + _PyXI_errcode code); +PyAPI_FUNC(void) _PyXI_ApplyExceptionInfo( + _PyXI_exception_info *info, + PyObject *exctype); + + #ifdef __cplusplus } #endif diff --git a/Modules/_xxsubinterpretersmodule.c b/Modules/_xxsubinterpretersmodule.c index 8dc74db1c4612e..deb436543fbd02 100644 --- a/Modules/_xxsubinterpretersmodule.c +++ b/Modules/_xxsubinterpretersmodule.c @@ -114,74 +114,6 @@ clear_module_state(module_state *state) } -/* exception info ***********************************************************/ - -static const char * -_excinfo_bind(PyObject *exc, _Py_excinfo *info, _PyXI_errcode *p_code) -{ - assert(exc != NULL); - - const char *failure = _Py_excinfo_InitFromException(info, exc); - if (failure != NULL) { - // We failed to initialize info->uncaught. - // XXX Print the excobj/traceback? Emit a warning? - // XXX Print the current exception/traceback? - if (PyErr_ExceptionMatches(PyExc_MemoryError)) { - *p_code = _PyXI_ERR_NO_MEMORY; - } - else { - *p_code = _PyXI_ERR_OTHER; - } - PyErr_Clear(); - } - else { - assert(!PyErr_Occurred()); - *p_code = _PyXI_ERR_UNCAUGHT_EXCEPTION; - } - return failure; -} - -typedef struct _sharedexception { - PyInterpreterState *interp; - _PyXI_errcode code; - _Py_excinfo uncaught; -} _sharedexception; - -static const char * -_sharedexception_bind(PyObject *exc, _PyXI_errcode code, _sharedexception *sharedexc) -{ - if (sharedexc->interp == NULL) { - sharedexc->interp = PyInterpreterState_Get(); - } - - const char *failure = NULL; - if (code == _PyXI_ERR_UNCAUGHT_EXCEPTION) { - failure = _excinfo_bind(exc, &sharedexc->uncaught, &sharedexc->code); - assert(sharedexc->code != _PyXI_ERR_NO_ERROR); - } - else { - assert(exc == NULL); - assert(code != _PyXI_ERR_NO_ERROR); - sharedexc->code = code; - _Py_excinfo_Clear(&sharedexc->uncaught); - } - return failure; -} - -static void -_sharedexception_apply(_sharedexception *exc, PyObject *wrapperclass) -{ - if (exc->code == _PyXI_ERR_UNCAUGHT_EXCEPTION) { - _Py_excinfo_Apply(&exc->uncaught, wrapperclass); - } - else { - assert(exc->code != _PyXI_ERR_NO_ERROR); - (void)_PyXI_ApplyErrorCode(exc->code, exc->interp); - assert(PyErr_Occurred()); - } -} - - /* data-sharing-specific code ***********************************************/ struct _sharednsitem { @@ -431,7 +363,7 @@ exceptions_init(PyObject *mod) static int _run_script(PyInterpreterState *interp, const char *codestr, Py_ssize_t codestrlen, - _sharedns *shared, _sharedexception *sharedexc, int flags) + _sharedns *shared, _PyXI_exception_info *sharedexc, int flags) { PyObject *excval = NULL; _PyXI_errcode errcode = _PyXI_ERR_UNCAUGHT_EXCEPTION; @@ -489,7 +421,7 @@ _run_script(PyInterpreterState *interp, } _PyInterpreterState_SetNotRunningMain(interp); - *sharedexc = (_sharedexception){0}; + *sharedexc = (_PyXI_exception_info){ NULL }; return 0; error: @@ -501,20 +433,21 @@ _run_script(PyInterpreterState *interp, else { assert(!PyErr_Occurred()); } - const char *failure = _sharedexception_bind(excval, errcode, sharedexc); + const char *failure = _PyXI_InitExceptionInfo(sharedexc, excval, errcode); if (failure != NULL) { fprintf(stderr, "RunFailedError: script raised an uncaught exception (%s)", failure); } - if (excval != NULL) { + if (sharedexc->code == _PyXI_ERR_UNCAUGHT_EXCEPTION) { + assert(excval != NULL); // XXX Instead, store the rendered traceback on sharedexc, // attach it to the exception when applied, // and teach PyErr_Display() to print it. PyErr_Display(NULL, excval, NULL); - Py_DECREF(excval); } - if (errcode != _PyXI_ERR_ALREADY_RUNNING) { + Py_XDECREF(excval); + if (sharedexc->code != _PyXI_ERR_ALREADY_RUNNING) { _PyInterpreterState_SetNotRunningMain(interp); } assert(!PyErr_Occurred()); @@ -545,7 +478,7 @@ _run_in_interpreter(PyObject *mod, PyInterpreterState *interp, } // Run the script. - _sharedexception exc = (_sharedexception){ .interp = interp }; + _PyXI_exception_info exc = (_PyXI_exception_info){ .interp = interp }; int result = _run_script(interp, codestr, codestrlen, shared, &exc, flags); // Switch back. @@ -558,7 +491,7 @@ _run_in_interpreter(PyObject *mod, PyInterpreterState *interp, // Propagate any exception out to the caller. if (result < 0) { assert(!PyErr_Occurred()); - _sharedexception_apply(&exc, state->RunFailedError); + _PyXI_ApplyExceptionInfo(&exc, state->RunFailedError); assert(PyErr_Occurred()); } diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 70c3fdedc2f228..8fd225c1a54d9c 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -667,3 +667,59 @@ _PyXI_ApplyErrorCode(_PyXI_errcode code, PyInterpreterState *interp) assert(PyErr_Occurred()); return -1; } + +/* shared exceptions */ + +const char * +_PyXI_InitExceptionInfo(_PyXI_exception_info *info, + PyObject *excobj, _PyXI_errcode code) +{ + if (info->interp == NULL) { + info->interp = PyInterpreterState_Get(); + } + + const char *failure = NULL; + if (code == _PyXI_ERR_UNCAUGHT_EXCEPTION) { + // There is an unhandled exception we need to propagate. + failure = _Py_excinfo_InitFromException(&info->uncaught, excobj); + if (failure != NULL) { + // We failed to initialize info->uncaught. + // XXX Print the excobj/traceback? Emit a warning? + // XXX Print the current exception/traceback? + if (PyErr_ExceptionMatches(PyExc_MemoryError)) { + info->code = _PyXI_ERR_NO_MEMORY; + } + else { + info->code = _PyXI_ERR_OTHER; + } + PyErr_Clear(); + } + else { + info->code = code; + } + assert(info->code != _PyXI_ERR_NO_ERROR); + } + else { + // There is an error code we need to propagate. + assert(excobj == NULL); + assert(code != _PyXI_ERR_NO_ERROR); + info->code = code; + _Py_excinfo_Clear(&info->uncaught); + } + return failure; +} + +void +_PyXI_ApplyExceptionInfo(_PyXI_exception_info *info, PyObject *exctype) +{ + if (info->code == _PyXI_ERR_UNCAUGHT_EXCEPTION) { + // Raise an exception that proxies the propagated exception. + _Py_excinfo_Apply(&info->uncaught, exctype); + } + else { + // Raise an exception corresponding to the code. + assert(info->code != _PyXI_ERR_NO_ERROR); + (void)_PyXI_ApplyErrorCode(info->code, info->interp); + } + assert(PyErr_Occurred()); +} From 2424e33c71038cc1712dfff5bf2b8d860d6c96b7 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 25 Oct 2023 12:22:44 -0600 Subject: [PATCH 04/22] Extract _PyXI_namespace. --- Include/internal/pycore_crossinterp.h | 7 ++ Modules/_xxsubinterpretersmodule.c | 167 +------------------------- Python/crossinterp.c | 163 +++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 163 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 4fe924ea3e2caa..9cb753b76b1b72 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -175,6 +175,13 @@ PyAPI_FUNC(void) _PyXI_ApplyExceptionInfo( PyObject *exctype); +typedef struct _sharedns _PyXI_namespace; + +PyAPI_FUNC(void) _PyXI_FreeNamespace(_PyXI_namespace *ns); +PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromDict(PyObject *nsobj); +PyAPI_FUNC(int) _PyXI_ApplyNamespace(_PyXI_namespace *ns, PyObject *nsobj); + + #ifdef __cplusplus } #endif diff --git a/Modules/_xxsubinterpretersmodule.c b/Modules/_xxsubinterpretersmodule.c index deb436543fbd02..1bef4164ebe741 100644 --- a/Modules/_xxsubinterpretersmodule.c +++ b/Modules/_xxsubinterpretersmodule.c @@ -20,22 +20,6 @@ #define MODULE_NAME "_xxsubinterpreters" -static const char * -_copy_raw_string(PyObject *strobj) -{ - const char *str = PyUnicode_AsUTF8(strobj); - if (str == NULL) { - return NULL; - } - char *copied = PyMem_RawMalloc(strlen(str)+1); - if (copied == NULL) { - PyErr_NoMemory(); - return NULL; - } - strcpy(copied, str); - return copied; -} - static PyInterpreterState * _get_current_interp(void) { @@ -63,21 +47,6 @@ add_new_exception(PyObject *mod, const char *name, PyObject *base) #define ADD_NEW_EXCEPTION(MOD, NAME, BASE) \ add_new_exception(MOD, MODULE_NAME "." Py_STRINGIFY(NAME), BASE) -static int -_release_xid_data(_PyCrossInterpreterData *data) -{ - PyObject *exc = PyErr_GetRaisedException(); - int res = _PyCrossInterpreterData_Release(data); - if (res < 0) { - /* The owning interpreter is already destroyed. */ - _PyCrossInterpreterData_Clear(NULL, data); - // XXX Emit a warning? - PyErr_Clear(); - } - PyErr_SetRaisedException(exc); - return res; -} - /* module state *************************************************************/ @@ -114,134 +83,6 @@ clear_module_state(module_state *state) } -/* data-sharing-specific code ***********************************************/ - -struct _sharednsitem { - const char *name; - _PyCrossInterpreterData data; -}; - -static void _sharednsitem_clear(struct _sharednsitem *); // forward - -static int -_sharednsitem_init(struct _sharednsitem *item, PyObject *key, PyObject *value) -{ - item->name = _copy_raw_string(key); - if (item->name == NULL) { - return -1; - } - if (_PyObject_GetCrossInterpreterData(value, &item->data) != 0) { - _sharednsitem_clear(item); - return -1; - } - return 0; -} - -static void -_sharednsitem_clear(struct _sharednsitem *item) -{ - if (item->name != NULL) { - PyMem_RawFree((void *)item->name); - item->name = NULL; - } - (void)_release_xid_data(&item->data); -} - -static int -_sharednsitem_apply(struct _sharednsitem *item, PyObject *ns) -{ - PyObject *name = PyUnicode_FromString(item->name); - if (name == NULL) { - return -1; - } - PyObject *value = _PyCrossInterpreterData_NewObject(&item->data); - if (value == NULL) { - Py_DECREF(name); - return -1; - } - int res = PyDict_SetItem(ns, name, value); - Py_DECREF(name); - Py_DECREF(value); - return res; -} - -typedef struct _sharedns { - Py_ssize_t len; - struct _sharednsitem* items; -} _sharedns; - -static _sharedns * -_sharedns_new(Py_ssize_t len) -{ - _sharedns *shared = PyMem_RawCalloc(sizeof(_sharedns), 1); - if (shared == NULL) { - PyErr_NoMemory(); - return NULL; - } - shared->len = len; - shared->items = PyMem_RawCalloc(sizeof(struct _sharednsitem), len); - if (shared->items == NULL) { - PyErr_NoMemory(); - PyMem_RawFree(shared); - return NULL; - } - return shared; -} - -static void -_sharedns_free(_sharedns *shared) -{ - for (Py_ssize_t i=0; i < shared->len; i++) { - _sharednsitem_clear(&shared->items[i]); - } - PyMem_RawFree(shared->items); - PyMem_RawFree(shared); -} - -static _sharedns * -_get_shared_ns(PyObject *shareable) -{ - if (shareable == NULL || shareable == Py_None) { - return NULL; - } - Py_ssize_t len = PyDict_Size(shareable); - if (len == 0) { - return NULL; - } - - _sharedns *shared = _sharedns_new(len); - if (shared == NULL) { - return NULL; - } - Py_ssize_t pos = 0; - for (Py_ssize_t i=0; i < len; i++) { - PyObject *key, *value; - if (PyDict_Next(shareable, &pos, &key, &value) == 0) { - break; - } - if (_sharednsitem_init(&shared->items[i], key, value) != 0) { - break; - } - } - if (PyErr_Occurred()) { - _sharedns_free(shared); - return NULL; - } - return shared; -} - -static int -_sharedns_apply(_sharedns *shared, PyObject *ns) -{ - for (Py_ssize_t i=0; i < shared->len; i++) { - if (_sharednsitem_apply(&shared->items[i], ns) != 0) { - return -1; - } - } - return 0; -} - - /* Python code **************************************************************/ static const char * @@ -363,7 +204,7 @@ exceptions_init(PyObject *mod) static int _run_script(PyInterpreterState *interp, const char *codestr, Py_ssize_t codestrlen, - _sharedns *shared, _PyXI_exception_info *sharedexc, int flags) + _PyXI_namespace *shared, _PyXI_exception_info *sharedexc, int flags) { PyObject *excval = NULL; _PyXI_errcode errcode = _PyXI_ERR_UNCAUGHT_EXCEPTION; @@ -391,7 +232,7 @@ _run_script(PyInterpreterState *interp, // Apply the cross-interpreter data. if (shared != NULL) { - if (_sharedns_apply(shared, ns) != 0) { + if (_PyXI_ApplyNamespace(shared, ns) != 0) { Py_DECREF(ns); goto error; } @@ -462,7 +303,7 @@ _run_in_interpreter(PyObject *mod, PyInterpreterState *interp, module_state *state = get_module_state(mod); assert(state != NULL); - _sharedns *shared = _get_shared_ns(shareables); + _PyXI_namespace *shared = _PyXI_NamespaceFromDict(shareables); if (shared == NULL && PyErr_Occurred()) { return -1; } @@ -496,7 +337,7 @@ _run_in_interpreter(PyObject *mod, PyInterpreterState *interp, } if (shared != NULL) { - _sharedns_free(shared); + _PyXI_FreeNamespace(shared); } return result; diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 8fd225c1a54d9c..bf6e2fa5cff1ef 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -626,6 +626,42 @@ _register_builtins_for_crossinterpreter_data(struct _xidregistry *xidregistry) } } +/*************************/ +/* convenience utilities */ +/*************************/ + +static const char * +_copy_string_obj_raw(PyObject *strobj) +{ + const char *str = PyUnicode_AsUTF8(strobj); + if (str == NULL) { + return NULL; + } + + char *copied = PyMem_RawMalloc(strlen(str)+1); + if (copied == NULL) { + PyErr_NoMemory(); + return NULL; + } + strcpy(copied, str); + return copied; +} + +static int +_release_xid_data(_PyCrossInterpreterData *data) +{ + PyObject *exc = PyErr_GetRaisedException(); + int res = _PyCrossInterpreterData_Release(data); + if (res < 0) { + /* The owning interpreter is already destroyed. */ + _PyCrossInterpreterData_Clear(NULL, data); + // XXX Emit a warning? + PyErr_Clear(); + } + PyErr_SetRaisedException(exc); + return res; +} + /***************************/ /* short-term data sharing */ @@ -723,3 +759,130 @@ _PyXI_ApplyExceptionInfo(_PyXI_exception_info *info, PyObject *exctype) } assert(PyErr_Occurred()); } + +/* shared namespaces */ + +typedef struct _sharednsitem { + const char *name; + _PyCrossInterpreterData data; +} _PyXI_namespace_item; + +static void _sharednsitem_clear(_PyXI_namespace_item *); // forward + +static int +_sharednsitem_init(_PyXI_namespace_item *item, PyObject *key, PyObject *value) +{ + item->name = _copy_string_obj_raw(key); + if (item->name == NULL) { + return -1; + } + if (_PyObject_GetCrossInterpreterData(value, &item->data) != 0) { + _sharednsitem_clear(item); + return -1; + } + return 0; +} + +static void +_sharednsitem_clear(_PyXI_namespace_item *item) +{ + if (item->name != NULL) { + PyMem_RawFree((void *)item->name); + item->name = NULL; + } + (void)_release_xid_data(&item->data); +} + +static int +_sharednsitem_apply(_PyXI_namespace_item *item, PyObject *ns) +{ + PyObject *name = PyUnicode_FromString(item->name); + if (name == NULL) { + return -1; + } + PyObject *value = _PyCrossInterpreterData_NewObject(&item->data); + if (value == NULL) { + Py_DECREF(name); + return -1; + } + int res = PyDict_SetItem(ns, name, value); + Py_DECREF(name); + Py_DECREF(value); + return res; +} + +struct _sharedns { + Py_ssize_t len; + _PyXI_namespace_item *items; +}; + +static _PyXI_namespace * +_sharedns_new(Py_ssize_t len) +{ + _PyXI_namespace *shared = PyMem_RawCalloc(sizeof(_PyXI_namespace), 1); + if (shared == NULL) { + PyErr_NoMemory(); + return NULL; + } + shared->len = len; + shared->items = PyMem_RawCalloc(sizeof(struct _sharednsitem), len); + if (shared->items == NULL) { + PyErr_NoMemory(); + PyMem_RawFree(shared); + return NULL; + } + return shared; +} + +void +_PyXI_FreeNamespace(_PyXI_namespace *ns) +{ + for (Py_ssize_t i=0; i < ns->len; i++) { + _sharednsitem_clear(&ns->items[i]); + } + PyMem_RawFree(ns->items); + PyMem_RawFree(ns); +} + +_PyXI_namespace * +_PyXI_NamespaceFromDict(PyObject *nsobj) +{ + if (nsobj == NULL || nsobj == Py_None) { + return NULL; + } + Py_ssize_t len = PyDict_Size(nsobj); + if (len == 0) { + return NULL; + } + + _PyXI_namespace *ns = _sharedns_new(len); + if (ns == NULL) { + return NULL; + } + Py_ssize_t pos = 0; + for (Py_ssize_t i=0; i < len; i++) { + PyObject *key, *value; + if (PyDict_Next(nsobj, &pos, &key, &value) == 0) { + break; + } + if (_sharednsitem_init(&ns->items[i], key, value) != 0) { + break; + } + } + if (PyErr_Occurred()) { + _PyXI_FreeNamespace(ns); + return NULL; + } + return ns; +} + +int +_PyXI_ApplyNamespace(_PyXI_namespace *ns, PyObject *nsobj) +{ + for (Py_ssize_t i=0; i < ns->len; i++) { + if (_sharednsitem_apply(&ns->items[i], nsobj) != 0) { + return -1; + } + } + return 0; +} From 23d695944a924b5227aa225e735775f333603360 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 23 Oct 2023 13:01:08 -0600 Subject: [PATCH 05/22] Factor out _enter_interpreter(), _exit_interpreter(), etc. --- Lib/test/support/interpreters.py | 2 +- Modules/_xxsubinterpretersmodule.c | 200 +++++++++++++++++------------ 2 files changed, 121 insertions(+), 81 deletions(-) diff --git a/Lib/test/support/interpreters.py b/Lib/test/support/interpreters.py index 182f47b19f1dd4..ab9342b767dfae 100644 --- a/Lib/test/support/interpreters.py +++ b/Lib/test/support/interpreters.py @@ -92,7 +92,7 @@ def close(self): return _interpreters.destroy(self._id) # XXX Rename "run" to "exec"? - def run(self, src_str, /, *, channels=None): + def run(self, src_str, /, channels=None): """Run the given source code in the interpreter. This is essentially the same as calling the builtin "exec" diff --git a/Modules/_xxsubinterpretersmodule.c b/Modules/_xxsubinterpretersmodule.c index 1bef4164ebe741..07367b2e69f1ab 100644 --- a/Modules/_xxsubinterpretersmodule.c +++ b/Modules/_xxsubinterpretersmodule.c @@ -201,13 +201,64 @@ exceptions_init(PyObject *mod) return 0; } +static PyThreadState * +_enter_interpreter(PyInterpreterState *interp) +{ + // Switch to interpreter. + PyThreadState *save_tstate = NULL; + PyThreadState *tstate = NULL; + if (interp != PyInterpreterState_Get()) { + tstate = PyThreadState_New(interp); + tstate->_whence = _PyThreadState_WHENCE_EXEC; + // XXX Possible GILState issues? + save_tstate = PyThreadState_Swap(tstate); + } + return save_tstate; +} + +static int +_exit_interpreter(PyInterpreterState *interp, PyThreadState *save_tstate, + int errcode, _PyXI_exception_info *exc) +{ + int res = 0; + if (errcode != _PyXI_ERR_NO_ERROR) { + assert(exc != NULL); + PyObject *excval = PyErr_GetRaisedException(); + *exc = (_PyXI_exception_info){ .interp = interp }; + const char *failure = _PyXI_InitExceptionInfo(exc, excval, errcode); + if (failure != NULL) { + fprintf(stderr, + "RunFailedError: script raised an uncaught exception (%s)", + failure); + } + else { + res = -1; + } + if (excval != NULL) { + // XXX Instead, store the rendered traceback on sharedexc, + // attach it to the exception when applied, + // and teach PyErr_Display() to print it. + PyErr_Display(NULL, excval, NULL); + Py_DECREF(excval); + } + assert(!PyErr_Occurred()); + } + + // Switch back. + if (save_tstate != NULL) { + PyThreadState *tstate = PyThreadState_Get(); + PyThreadState_Clear(tstate); + PyThreadState_Swap(save_tstate); + PyThreadState_Delete(tstate); + } + + return res; +} + static int -_run_script(PyInterpreterState *interp, - const char *codestr, Py_ssize_t codestrlen, - _PyXI_namespace *shared, _PyXI_exception_info *sharedexc, int flags) +_enter_interpreter_main(PyInterpreterState *interp, PyObject **p_ns) { - PyObject *excval = NULL; - _PyXI_errcode errcode = _PyXI_ERR_UNCAUGHT_EXCEPTION; + assert(PyInterpreterState_Get() == interp); if (_PyInterpreterState_SetRunningMain(interp) < 0) { assert(PyErr_Occurred()); @@ -215,30 +266,33 @@ _run_script(PyInterpreterState *interp, // be more efficient to leave the exception in place and return // immediately. However, life is simpler if we don't. PyErr_Clear(); - errcode = _PyXI_ERR_ALREADY_RUNNING; - goto error; + return _PyXI_ERR_ALREADY_RUNNING; } PyObject *main_mod = PyUnstable_InterpreterState_GetMainModule(interp); if (main_mod == NULL) { - goto error; + return _PyXI_ERR_UNCAUGHT_EXCEPTION; } PyObject *ns = PyModule_GetDict(main_mod); // borrowed Py_DECREF(main_mod); if (ns == NULL) { - goto error; + return _PyXI_ERR_UNCAUGHT_EXCEPTION; } - Py_INCREF(ns); - // Apply the cross-interpreter data. - if (shared != NULL) { - if (_PyXI_ApplyNamespace(shared, ns) != 0) { - Py_DECREF(ns); - goto error; - } - } + *p_ns = Py_NewRef(ns); + return _PyXI_ERR_NO_ERROR; +} + +static void +_exit_interpreter_main(PyInterpreterState *interp, PyObject *ns) +{ + Py_XDECREF(ns); + _PyInterpreterState_SetNotRunningMain(interp); +} - // Run the script/code/etc. +static int +_run_script(PyObject *ns, const char *codestr, Py_ssize_t codestrlen, int flags) +{ PyObject *result = NULL; if (flags & RUN_TEXT) { result = PyRun_StringFlags(codestr, Py_file_input, ns, ns, NULL); @@ -253,46 +307,11 @@ _run_script(PyInterpreterState *interp, else { Py_UNREACHABLE(); } - Py_DECREF(ns); if (result == NULL) { - goto error; - } - else { - Py_DECREF(result); // We throw away the result. + return -1; } - _PyInterpreterState_SetNotRunningMain(interp); - - *sharedexc = (_PyXI_exception_info){ NULL }; + Py_DECREF(result); // We throw away the result. return 0; - -error: - assert(errcode != _PyXI_ERR_NO_ERROR); - if (errcode == _PyXI_ERR_UNCAUGHT_EXCEPTION) { - assert(PyErr_Occurred()); - excval = PyErr_GetRaisedException(); - } - else { - assert(!PyErr_Occurred()); - } - const char *failure = _PyXI_InitExceptionInfo(sharedexc, excval, errcode); - if (failure != NULL) { - fprintf(stderr, - "RunFailedError: script raised an uncaught exception (%s)", - failure); - } - if (sharedexc->code == _PyXI_ERR_UNCAUGHT_EXCEPTION) { - assert(excval != NULL); - // XXX Instead, store the rendered traceback on sharedexc, - // attach it to the exception when applied, - // and teach PyErr_Display() to print it. - PyErr_Display(NULL, excval, NULL); - } - Py_XDECREF(excval); - if (sharedexc->code != _PyXI_ERR_ALREADY_RUNNING) { - _PyInterpreterState_SetNotRunningMain(interp); - } - assert(!PyErr_Occurred()); - return -1; } static int @@ -300,47 +319,69 @@ _run_in_interpreter(PyObject *mod, PyInterpreterState *interp, const char *codestr, Py_ssize_t codestrlen, PyObject *shareables, int flags) { - module_state *state = get_module_state(mod); - assert(state != NULL); - + // Convert the attrs for cross-interpreter use. _PyXI_namespace *shared = _PyXI_NamespaceFromDict(shareables); if (shared == NULL && PyErr_Occurred()) { return -1; } + _PyXI_exception_info exc; // Switch to interpreter. - PyThreadState *save_tstate = NULL; - PyThreadState *tstate = NULL; - if (interp != PyInterpreterState_Get()) { - tstate = PyThreadState_New(interp); - tstate->_whence = _PyThreadState_WHENCE_EXEC; - // XXX Possible GILState issues? - save_tstate = PyThreadState_Swap(tstate); + PyThreadState *save_tstate = _enter_interpreter(interp); + PyObject *ns = NULL; + _PyXI_errcode errcode = _enter_interpreter_main(interp, &ns); + if (errcode != _PyXI_ERR_NO_ERROR) { + goto error; + } + errcode = _PyXI_ERR_UNCAUGHT_EXCEPTION; + + // Apply the cross-interpreter data. + if (shared != NULL) { + if (_PyXI_ApplyNamespace(shared, ns) < 0) { + goto error; + } } // Run the script. - _PyXI_exception_info exc = (_PyXI_exception_info){ .interp = interp }; - int result = _run_script(interp, codestr, codestrlen, shared, &exc, flags); + errcode = _run_script(ns, codestr, codestrlen, flags); + if (errcode != _PyXI_ERR_NO_ERROR) { + goto error; + } // Switch back. - if (save_tstate != NULL) { - PyThreadState_Clear(tstate); - PyThreadState_Swap(save_tstate); - PyThreadState_Delete(tstate); + assert(!PyErr_Occurred()); + assert(errcode == _PyXI_ERR_NO_ERROR); + _exit_interpreter_main(interp, ns); + (void)_exit_interpreter(interp, save_tstate, 0, NULL); + + goto finally; + +error: + // Switch back. + assert(errcode != _PyXI_ERR_NO_ERROR); + if (errcode != _PyXI_ERR_ALREADY_RUNNING) { + _exit_interpreter_main(interp, ns); + } + else { + assert(ns == NULL); } + int res = _exit_interpreter(interp, save_tstate, errcode, &exc); + assert(res < 0); // Propagate any exception out to the caller. - if (result < 0) { - assert(!PyErr_Occurred()); - _PyXI_ApplyExceptionInfo(&exc, state->RunFailedError); - assert(PyErr_Occurred()); - } + assert(!PyErr_Occurred()); + module_state *state = get_module_state(mod); + assert(state != NULL); + _PyXI_ApplyExceptionInfo(&exc, state->RunFailedError); + assert(PyErr_Occurred()); +finally: + // Clear the cross-interpreter attrs. if (shared != NULL) { _PyXI_FreeNamespace(shared); } - return result; + return (int)errcode; } @@ -526,7 +567,6 @@ PyDoc_STRVAR(get_main_doc, \n\ Return the ID of main interpreter."); - static PyUnicodeObject * convert_script_arg(PyObject *arg, const char *fname, const char *displayname, const char *expected) @@ -627,7 +667,7 @@ _interp_exec(PyObject *self, int res = _run_in_interpreter(self, interp, codestr, codestrlen, shared_arg, flags); Py_XDECREF(bytes_obj); - if (res != 0) { + if (res < 0) { return -1; } @@ -702,7 +742,7 @@ interp_run_string(PyObject *self, PyObject *args, PyObject *kwds) return NULL; } - int res = _interp_exec(self, id, (PyObject *)script, shared); + int res = _interp_exec(self, id, script, shared); Py_DECREF(script); if (res < 0) { return NULL; From b08249fc38fc1f5b66409df4fdd96e4e05f77bd2 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 26 Oct 2023 20:08:50 -0600 Subject: [PATCH 06/22] Move enter/exit to crossinterp.c. --- Include/internal/pycore_crossinterp.h | 55 ++++++ Modules/_xxsubinterpretersmodule.c | 170 +++-------------- Python/crossinterp.c | 264 +++++++++++++++++++++++++- 3 files changed, 341 insertions(+), 148 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 9cb753b76b1b72..a16ec4e93d10e2 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -182,6 +182,61 @@ PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromDict(PyObject *nsobj); PyAPI_FUNC(int) _PyXI_ApplyNamespace(_PyXI_namespace *ns, PyObject *nsobj); +// A cross-interpreter session involves entering an interpreter +// (_PyXI_Enter()), doing some work with it, and finally exiting +// that interpreter (_PyXI_Exit()). +// +// At the boundaries of the session, both entering and exiting, +// data may be exchanged between the previous interpreter and the +// target one in a thread-safe way that does not violate the +// isolation between interpreters. This includes setting objects +// in the target's __main__ module on the way in, and capturing +// uncaught exceptions on the way out. +typedef struct xi_session { + // Once a session has been entered, this is the tstate that was + // current before the session. If it is different from cur_tstate + // then we must have switched interpreters. Either way, this will + // be the current tstate once we exit the session. + PyThreadState *prev_tstate; + // Once a session has been entered, this is the current tstate. + // It must be current when the session exits. + PyThreadState *init_tstate; + // This is true if init_tstate needs cleanup during exit. + int own_init_tstate; + + // This is true if, while entering the session, init_thread took + // "ownership" of the interpreter's __main__ module. This means + // it is the only thread that is allowed to run code there. + // (Caveat: for now, users may still run exec() against the + // __main__ module's dict, though that isn't advisable.) + int running; + // This is a cached reference to the __dict__ of the entered + // interpreter's __main__ module. It is looked up when at the + // beginning of the session as a convenience. + PyObject *main_ns; + + // This is set if the interpreter is entered and raised an exception + // that needs to be handled in some special way during exit. + _PyXI_errcode *exc_override; + // This is set if exit captured an exception to propagate. + _PyXI_exception_info *exc; + + // -- pre-allocated memory -- + _PyXI_exception_info _exc; +} _PyXI_session; + +PyAPI_FUNC(int) _PyXI_Enter( + PyInterpreterState *interp, + PyObject *nsupdates, + _PyXI_session *session); +PyAPI_FUNC(void) _PyXI_Exit(_PyXI_session *session); + +PyAPI_FUNC(void) _PyXI_ApplyCapturedException( + _PyXI_session *session, + PyObject *excwrapper); +PyAPI_FUNC(int) _PyXI_HasCapturedException(_PyXI_session *session); + + #ifdef __cplusplus } #endif diff --git a/Modules/_xxsubinterpretersmodule.c b/Modules/_xxsubinterpretersmodule.c index 07367b2e69f1ab..ee8586cb117c1a 100644 --- a/Modules/_xxsubinterpretersmodule.c +++ b/Modules/_xxsubinterpretersmodule.c @@ -201,95 +201,6 @@ exceptions_init(PyObject *mod) return 0; } -static PyThreadState * -_enter_interpreter(PyInterpreterState *interp) -{ - // Switch to interpreter. - PyThreadState *save_tstate = NULL; - PyThreadState *tstate = NULL; - if (interp != PyInterpreterState_Get()) { - tstate = PyThreadState_New(interp); - tstate->_whence = _PyThreadState_WHENCE_EXEC; - // XXX Possible GILState issues? - save_tstate = PyThreadState_Swap(tstate); - } - return save_tstate; -} - -static int -_exit_interpreter(PyInterpreterState *interp, PyThreadState *save_tstate, - int errcode, _PyXI_exception_info *exc) -{ - int res = 0; - if (errcode != _PyXI_ERR_NO_ERROR) { - assert(exc != NULL); - PyObject *excval = PyErr_GetRaisedException(); - *exc = (_PyXI_exception_info){ .interp = interp }; - const char *failure = _PyXI_InitExceptionInfo(exc, excval, errcode); - if (failure != NULL) { - fprintf(stderr, - "RunFailedError: script raised an uncaught exception (%s)", - failure); - } - else { - res = -1; - } - if (excval != NULL) { - // XXX Instead, store the rendered traceback on sharedexc, - // attach it to the exception when applied, - // and teach PyErr_Display() to print it. - PyErr_Display(NULL, excval, NULL); - Py_DECREF(excval); - } - assert(!PyErr_Occurred()); - } - - // Switch back. - if (save_tstate != NULL) { - PyThreadState *tstate = PyThreadState_Get(); - PyThreadState_Clear(tstate); - PyThreadState_Swap(save_tstate); - PyThreadState_Delete(tstate); - } - - return res; -} - -static int -_enter_interpreter_main(PyInterpreterState *interp, PyObject **p_ns) -{ - assert(PyInterpreterState_Get() == interp); - - if (_PyInterpreterState_SetRunningMain(interp) < 0) { - assert(PyErr_Occurred()); - // In the case where we didn't switch interpreters, it would - // be more efficient to leave the exception in place and return - // immediately. However, life is simpler if we don't. - PyErr_Clear(); - return _PyXI_ERR_ALREADY_RUNNING; - } - - PyObject *main_mod = PyUnstable_InterpreterState_GetMainModule(interp); - if (main_mod == NULL) { - return _PyXI_ERR_UNCAUGHT_EXCEPTION; - } - PyObject *ns = PyModule_GetDict(main_mod); // borrowed - Py_DECREF(main_mod); - if (ns == NULL) { - return _PyXI_ERR_UNCAUGHT_EXCEPTION; - } - - *p_ns = Py_NewRef(ns); - return _PyXI_ERR_NO_ERROR; -} - -static void -_exit_interpreter_main(PyInterpreterState *interp, PyObject *ns) -{ - Py_XDECREF(ns); - _PyInterpreterState_SetNotRunningMain(interp); -} - static int _run_script(PyObject *ns, const char *codestr, Py_ssize_t codestrlen, int flags) { @@ -315,73 +226,38 @@ _run_script(PyObject *ns, const char *codestr, Py_ssize_t codestrlen, int flags) } static int -_run_in_interpreter(PyObject *mod, PyInterpreterState *interp, +_run_in_interpreter(PyInterpreterState *interp, const char *codestr, Py_ssize_t codestrlen, - PyObject *shareables, int flags) + PyObject *shareables, int flags, + PyObject *excwrapper) { - // Convert the attrs for cross-interpreter use. - _PyXI_namespace *shared = _PyXI_NamespaceFromDict(shareables); - if (shared == NULL && PyErr_Occurred()) { - return -1; - } - _PyXI_exception_info exc; - - // Switch to interpreter. - PyThreadState *save_tstate = _enter_interpreter(interp); - PyObject *ns = NULL; - _PyXI_errcode errcode = _enter_interpreter_main(interp, &ns); - if (errcode != _PyXI_ERR_NO_ERROR) { - goto error; - } - errcode = _PyXI_ERR_UNCAUGHT_EXCEPTION; + assert(!PyErr_Occurred()); + _PyXI_session session = {0}; - // Apply the cross-interpreter data. - if (shared != NULL) { - if (_PyXI_ApplyNamespace(shared, ns) < 0) { - goto error; - } + // Prep and switch interpreters. + if (_PyXI_Enter(interp, shareables, &session) < 0) { + assert(!PyErr_Occurred()); + _PyXI_ApplyExceptionInfo(session.exc, excwrapper); + assert(PyErr_Occurred()); + return -1; } // Run the script. - errcode = _run_script(ns, codestr, codestrlen, flags); - if (errcode != _PyXI_ERR_NO_ERROR) { - goto error; - } + int res = _run_script(session.main_ns, codestr, codestrlen, flags); - // Switch back. - assert(!PyErr_Occurred()); - assert(errcode == _PyXI_ERR_NO_ERROR); - _exit_interpreter_main(interp, ns); - (void)_exit_interpreter(interp, save_tstate, 0, NULL); - - goto finally; - -error: - // Switch back. - assert(errcode != _PyXI_ERR_NO_ERROR); - if (errcode != _PyXI_ERR_ALREADY_RUNNING) { - _exit_interpreter_main(interp, ns); - } - else { - assert(ns == NULL); - } - int res = _exit_interpreter(interp, save_tstate, errcode, &exc); - assert(res < 0); + // Clean up and switch back. + _PyXI_Exit(&session); // Propagate any exception out to the caller. assert(!PyErr_Occurred()); - module_state *state = get_module_state(mod); - assert(state != NULL); - _PyXI_ApplyExceptionInfo(&exc, state->RunFailedError); - assert(PyErr_Occurred()); - -finally: - // Clear the cross-interpreter attrs. - if (shared != NULL) { - _PyXI_FreeNamespace(shared); + if (res < 0) { + _PyXI_ApplyCapturedException(&session, excwrapper); + } + else { + assert(!_PyXI_HasCapturedException(&session)); } - return (int)errcode; + return res; } @@ -664,8 +540,10 @@ _interp_exec(PyObject *self, } // Run the code in the interpreter. - int res = _run_in_interpreter(self, interp, codestr, codestrlen, - shared_arg, flags); + module_state *state = get_module_state(self); + assert(state != NULL); + int res = _run_in_interpreter(interp, codestr, codestrlen, + shared_arg, flags, state->RunFailedError); Py_XDECREF(bytes_obj); if (res < 0) { return -1; diff --git a/Python/crossinterp.c b/Python/crossinterp.c index bf6e2fa5cff1ef..4639ce0791ba76 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -812,6 +812,7 @@ _sharednsitem_apply(_PyXI_namespace_item *item, PyObject *ns) } struct _sharedns { + PyInterpreterState *interp; Py_ssize_t len; _PyXI_namespace_item *items; }; @@ -834,8 +835,8 @@ _sharedns_new(Py_ssize_t len) return shared; } -void -_PyXI_FreeNamespace(_PyXI_namespace *ns) +static void +_free_xi_namespace(_PyXI_namespace *ns) { for (Py_ssize_t i=0; i < ns->len; i++) { _sharednsitem_clear(&ns->items[i]); @@ -844,6 +845,43 @@ _PyXI_FreeNamespace(_PyXI_namespace *ns) PyMem_RawFree(ns); } +static int +_pending_free_xi_namespace(void *arg) +{ + _PyXI_namespace *ns = (_PyXI_namespace *)arg; + _free_xi_namespace(ns); + return 0; +} + +void +_PyXI_FreeNamespace(_PyXI_namespace *ns) +{ + if (ns->len == 0) { + return; + } + PyInterpreterState *interp = ns->interp; + if (interp == NULL) { + assert(ns->items[0].name == NULL); + // No data was actually set, so we can free the items + // without clearing each item's XI data. + PyMem_RawFree(ns->items); + PyMem_RawFree(ns); + } + else { + // We can assume the first item represents all items. + assert(ns->items[0].data.interpid == interp->id); + if (interp == PyInterpreterState_Get()) { + // We can avoid pending calls. + _free_xi_namespace(ns); + } + else { + // We have to use a pending call due to data in another interpreter. + // XXX Make sure the pending call was added? + _PyEval_AddPendingCall(interp, _pending_free_xi_namespace, ns, 0); + } + } +} + _PyXI_namespace * _PyXI_NamespaceFromDict(PyObject *nsobj) { @@ -859,6 +897,8 @@ _PyXI_NamespaceFromDict(PyObject *nsobj) if (ns == NULL) { return NULL; } + ns->interp = PyInterpreterState_Get(); + Py_ssize_t pos = 0; for (Py_ssize_t i=0; i < len; i++) { PyObject *key, *value; @@ -886,3 +926,223 @@ _PyXI_ApplyNamespace(_PyXI_namespace *ns, PyObject *nsobj) } return 0; } + + +/**********************/ +/* high-level helpers */ +/**********************/ + +/* enter/exit a cross-interpreter session */ + +static void +_enter_session(_PyXI_session *session, PyInterpreterState *interp) +{ + // Set here and cleared in _exit_session(). + assert(!session->own_init_tstate); + assert(session->init_tstate == NULL); + assert(session->prev_tstate == NULL); + // Set elsewhere and cleared in _exit_session(). + assert(!session->running); + assert(session->main_ns == NULL); + // Set elsewhere and cleared in _capture_current_exception(). + assert(session->exc_override == NULL); + // Set elsewhere and cleared in _PyXI_ApplyCapturedException(). + assert(session->exc == NULL); + + // Switch to interpreter. + PyThreadState *tstate = PyThreadState_Get(); + PyThreadState *prev = tstate; + if (interp != tstate->interp) { + tstate = PyThreadState_New(interp); + tstate->_whence = _PyThreadState_WHENCE_EXEC; + // XXX Possible GILState issues? + session->prev_tstate = PyThreadState_Swap(tstate); + assert(session->prev_tstate == prev); + session->own_init_tstate = 1; + } + session->init_tstate = tstate; + session->prev_tstate = prev; +} + +static void +_exit_session(_PyXI_session *session) +{ + PyThreadState *tstate = session->init_tstate; + assert(tstate != NULL); + assert(PyThreadState_Get() == tstate); + + // Release any of the entered interpreters resources. + if (session->main_ns != NULL) { + Py_CLEAR(session->main_ns); + } + + // Ensure this thread no longer owns __main__. + if (session->running) { + _PyInterpreterState_SetNotRunningMain(tstate->interp); + assert(!PyErr_Occurred()); + session->running = 0; + } + + // Switch back. + assert(session->prev_tstate != NULL); + if (session->prev_tstate != session->init_tstate) { + assert(session->own_init_tstate); + session->own_init_tstate = 0; + PyThreadState_Clear(tstate); + PyThreadState_Swap(session->prev_tstate); + PyThreadState_Delete(tstate); + } + else { + assert(!session->own_init_tstate); + } + session->prev_tstate = NULL; + session->init_tstate = NULL; +} + +static void +_capture_current_exception(_PyXI_session *session) +{ + assert(session->exc == NULL); + if (!PyErr_Occurred()) { + assert(session->exc_override == NULL); + return; + } + + // Handle the exception override. + _PyXI_errcode errcode = session->exc_override != NULL + ? *session->exc_override + : _PyXI_ERR_UNCAUGHT_EXCEPTION; + session->exc_override = NULL; + + // Pop the exception object. + PyObject *excval = NULL; + if (errcode == _PyXI_ERR_UNCAUGHT_EXCEPTION) { + // We want to actually capture the current exception. + excval = PyErr_GetRaisedException(); + } + else { + // We could do a variety of things here, depending on errcode. + // However, for now we simply ignore the exception and rely + // strictly on errcode. + PyErr_Clear(); + } + + // Capture the exception. + _PyXI_exception_info *exc = &session->_exc; + *exc = (_PyXI_exception_info){ + .interp = session->init_tstate->interp, + }; + const char *failure = _PyXI_InitExceptionInfo(exc, excval, errcode); + + // Handle capture failure. + if (failure != NULL) { + // XXX Make this error message more generic. + fprintf(stderr, + "RunFailedError: script raised an uncaught exception (%s)", + failure); + exc = NULL; + } + + // a temporary hack (famous last words) + if (excval != NULL) { + // XXX Store the traceback info (or rendered traceback) on + // _PyXI_excinfo, attach it to the exception when applied, + // and teach PyErr_Display() to print it. +#ifdef Py_DEBUG + // XXX Drop this once _Py_excinfo picks up the slack. + PyErr_Display(NULL, excval, NULL); +#endif + Py_DECREF(excval); + } + + // Finished! + assert(!PyErr_Occurred()); + session->exc = exc; +} + +void +_PyXI_ApplyCapturedException(_PyXI_session *session, PyObject *excwrapper) +{ + assert(!PyErr_Occurred()); + assert(session->exc != NULL); + _PyXI_ApplyExceptionInfo(session->exc, excwrapper); + assert(PyErr_Occurred()); + session->exc = NULL; +} + +int +_PyXI_HasCapturedException(_PyXI_session *session) +{ + return session->exc != NULL; +} + +int +_PyXI_Enter(PyInterpreterState *interp, PyObject *nsupdates, + _PyXI_session *session) +{ + // Convert the attrs for cross-interpreter use. + _PyXI_namespace *sharedns = NULL; + if (nsupdates != NULL) { + sharedns = _PyXI_NamespaceFromDict(nsupdates); + if (sharedns == NULL && PyErr_Occurred()) { + assert(session->exc == NULL); + return -1; + } + } + + // Switch to the requested interpreter (if necessary). + _enter_session(session, interp); + _PyXI_errcode errcode = _PyXI_ERR_UNCAUGHT_EXCEPTION; + + // Ensure this thread owns __main__. + if (_PyInterpreterState_SetRunningMain(interp) < 0) { + // In the case where we didn't switch interpreters, it would + // be more efficient to leave the exception in place and return + // immediately. However, life is simpler if we don't. + errcode = _PyXI_ERR_ALREADY_RUNNING; + goto error; + } + session->running = 1; + + // Cache __main__.__dict__. + PyObject *main_mod = PyUnstable_InterpreterState_GetMainModule(interp); + if (main_mod == NULL) { + goto error; + } + PyObject *ns = PyModule_GetDict(main_mod); // borrowed + Py_DECREF(main_mod); + if (ns == NULL) { + goto error; + } + session->main_ns = Py_NewRef(ns); + + // Apply the cross-interpreter data. + if (sharedns != NULL) { + if (_PyXI_ApplyNamespace(sharedns, ns) < 0) { + goto error; + } + _PyXI_FreeNamespace(sharedns); + } + + errcode = _PyXI_ERR_NO_ERROR; + return 0; + +error: + assert(PyErr_Occurred()); + if (errcode != _PyXI_ERR_UNCAUGHT_EXCEPTION) { + session->exc_override = &errcode; + } + _capture_current_exception(session); + _exit_session(session); + if (sharedns != NULL) { + _PyXI_FreeNamespace(sharedns); + } + return -1; +} + +void +_PyXI_Exit(_PyXI_session *session) +{ + _capture_current_exception(session); + _exit_session(session); +} From 773f5ab1091ea8e3a4f145a936ffd2e9ff939b90 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 23 Oct 2023 13:17:13 -0600 Subject: [PATCH 07/22] Factor out _sharednsitem_set_value(). --- Python/crossinterp.c | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 4639ce0791ba76..89a005e21f5249 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -764,20 +764,31 @@ _PyXI_ApplyExceptionInfo(_PyXI_exception_info *info, PyObject *exctype) typedef struct _sharednsitem { const char *name; + int hasdata; _PyCrossInterpreterData data; } _PyXI_namespace_item; static void _sharednsitem_clear(_PyXI_namespace_item *); // forward static int -_sharednsitem_init(_PyXI_namespace_item *item, PyObject *key, PyObject *value) +_sharednsitem_init(_PyXI_namespace_item *item, PyObject *key) { item->name = _copy_string_obj_raw(key); if (item->name == NULL) { return -1; } + item->hasdata = 0; + return 0; +} + +static int +_sharednsitem_set_value(_PyXI_namespace_item *item, PyObject *value) +{ + assert(item->name != NULL); + assert(!item->hasdata); + item->hasdata = 1; if (_PyObject_GetCrossInterpreterData(value, &item->data) != 0) { - _sharednsitem_clear(item); + item->hasdata = 0; return -1; } return 0; @@ -790,7 +801,10 @@ _sharednsitem_clear(_PyXI_namespace_item *item) PyMem_RawFree((void *)item->name); item->name = NULL; } - (void)_release_xid_data(&item->data); + if (item->hasdata) { + item->hasdata = 0; + (void)_release_xid_data(&item->data); + } } static int @@ -800,6 +814,7 @@ _sharednsitem_apply(_PyXI_namespace_item *item, PyObject *ns) if (name == NULL) { return -1; } + assert(item->hasdata); PyObject *value = _PyCrossInterpreterData_NewObject(&item->data); if (value == NULL) { Py_DECREF(name); @@ -888,6 +903,11 @@ _PyXI_NamespaceFromDict(PyObject *nsobj) if (nsobj == NULL || nsobj == Py_None) { return NULL; } + if (!PyDict_CheckExact(nsobj)) { + PyErr_SetString(PyExc_TypeError, "expected a dict"); + return NULL; + } + Py_ssize_t len = PyDict_Size(nsobj); if (len == 0) { return NULL; @@ -902,10 +922,15 @@ _PyXI_NamespaceFromDict(PyObject *nsobj) Py_ssize_t pos = 0; for (Py_ssize_t i=0; i < len; i++) { PyObject *key, *value; - if (PyDict_Next(nsobj, &pos, &key, &value) == 0) { + if (!PyDict_Next(nsobj, &pos, &key, &value)) { + break; + } + _PyXI_namespace_item *item = &ns->items[i]; + if (_sharednsitem_init(item, key) != 0) { break; } - if (_sharednsitem_init(&ns->items[i], key, value) != 0) { + if (_sharednsitem_set_value(item, value) < 0) { + _sharednsitem_clear(item); break; } } From cf7354e41bdc1b4bf5ac93850042d29d0273142f Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 30 Oct 2023 18:17:02 -0600 Subject: [PATCH 08/22] Add _PyXI_NamespaceFromNames(). --- Include/internal/pycore_crossinterp.h | 1 + Python/crossinterp.c | 35 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index a16ec4e93d10e2..2274cfdf412d9f 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -178,6 +178,7 @@ PyAPI_FUNC(void) _PyXI_ApplyExceptionInfo( typedef struct _sharedns _PyXI_namespace; PyAPI_FUNC(void) _PyXI_FreeNamespace(_PyXI_namespace *ns); +PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromNames(PyObject *names); PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromDict(PyObject *nsobj); PyAPI_FUNC(int) _PyXI_ApplyNamespace(_PyXI_namespace *ns, PyObject *nsobj); diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 89a005e21f5249..f3429c1a87e3c7 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -897,6 +897,41 @@ _PyXI_FreeNamespace(_PyXI_namespace *ns) } } +_PyXI_namespace * +_PyXI_NamespaceFromNames(PyObject *names) +{ + if (names == NULL || names == Py_None) { + return NULL; + } + + Py_ssize_t len = PySequence_Size(names); + if (len <= 0) { + return NULL; + } + + _PyXI_namespace *ns = _sharedns_new(len); + if (ns == NULL) { + return NULL; + } + for (Py_ssize_t i=0; i < len; i++) { + PyObject *key = PySequence_GetItem(names, i); + if (key == NULL) { + break; + } + struct _sharednsitem *item = &ns->items[i]; + int res = _sharednsitem_init(item, key); + Py_DECREF(key); + if (res < 0) { + break; + } + } + if (PyErr_Occurred()) { + _PyXI_FreeNamespace(ns); + return NULL; + } + return ns; +} + _PyXI_namespace * _PyXI_NamespaceFromDict(PyObject *nsobj) { From 6b43620017d9a2985c430b1ae84f2eb382bc7c2b Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 30 Oct 2023 18:24:14 -0600 Subject: [PATCH 09/22] Add a default arg to _PyXI_ApplyNamespace(). --- Include/internal/pycore_crossinterp.h | 5 ++++- Python/crossinterp.c | 23 ++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 2274cfdf412d9f..4c5a4dfcf77c66 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -180,7 +180,10 @@ typedef struct _sharedns _PyXI_namespace; PyAPI_FUNC(void) _PyXI_FreeNamespace(_PyXI_namespace *ns); PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromNames(PyObject *names); PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromDict(PyObject *nsobj); -PyAPI_FUNC(int) _PyXI_ApplyNamespace(_PyXI_namespace *ns, PyObject *nsobj); +PyAPI_FUNC(int) _PyXI_ApplyNamespace( + _PyXI_namespace *ns, + PyObject *nsobj, + PyObject *dflt); // A cross-interpreter session involves entering an interpreter diff --git a/Python/crossinterp.c b/Python/crossinterp.c index f3429c1a87e3c7..1e2340978aa5bb 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -808,17 +808,22 @@ _sharednsitem_clear(_PyXI_namespace_item *item) } static int -_sharednsitem_apply(_PyXI_namespace_item *item, PyObject *ns) +_sharednsitem_apply(_PyXI_namespace_item *item, PyObject *ns, PyObject *dflt) { PyObject *name = PyUnicode_FromString(item->name); if (name == NULL) { return -1; } - assert(item->hasdata); - PyObject *value = _PyCrossInterpreterData_NewObject(&item->data); - if (value == NULL) { - Py_DECREF(name); - return -1; + PyObject *value; + if (item->hasdata) { + value = _PyCrossInterpreterData_NewObject(&item->data); + if (value == NULL) { + Py_DECREF(name); + return -1; + } + } + else { + value = Py_NewRef(dflt); } int res = PyDict_SetItem(ns, name, value); Py_DECREF(name); @@ -977,10 +982,10 @@ _PyXI_NamespaceFromDict(PyObject *nsobj) } int -_PyXI_ApplyNamespace(_PyXI_namespace *ns, PyObject *nsobj) +_PyXI_ApplyNamespace(_PyXI_namespace *ns, PyObject *nsobj, PyObject *dflt) { for (Py_ssize_t i=0; i < ns->len; i++) { - if (_sharednsitem_apply(&ns->items[i], nsobj) != 0) { + if (_sharednsitem_apply(&ns->items[i], nsobj, dflt) != 0) { return -1; } } @@ -1178,7 +1183,7 @@ _PyXI_Enter(PyInterpreterState *interp, PyObject *nsupdates, // Apply the cross-interpreter data. if (sharedns != NULL) { - if (_PyXI_ApplyNamespace(sharedns, ns) < 0) { + if (_PyXI_ApplyNamespace(sharedns, ns, NULL) < 0) { goto error; } _PyXI_FreeNamespace(sharedns); From caef7175d8cf023cc593d4ca38f36dbd157fa6ac Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 30 Oct 2023 18:45:44 -0600 Subject: [PATCH 10/22] Allocate xid dynamically when in target interpreter. --- Python/crossinterp.c | 58 ++++++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 1e2340978aa5bb..a87abdf66faed2 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -648,10 +648,12 @@ _copy_string_obj_raw(PyObject *strobj) } static int -_release_xid_data(_PyCrossInterpreterData *data) +_release_xid_data(_PyCrossInterpreterData *data, int rawfree) { PyObject *exc = PyErr_GetRaisedException(); - int res = _PyCrossInterpreterData_Release(data); + int res = rawfree + ? _PyCrossInterpreterData_Release(data) + : _PyCrossInterpreterData_ReleaseAndRawFree(data); if (res < 0) { /* The owning interpreter is already destroyed. */ _PyCrossInterpreterData_Clear(NULL, data); @@ -763,21 +765,24 @@ _PyXI_ApplyExceptionInfo(_PyXI_exception_info *info, PyObject *exctype) /* shared namespaces */ typedef struct _sharednsitem { + int64_t interpid; const char *name; - int hasdata; - _PyCrossInterpreterData data; + _PyCrossInterpreterData *data; + _PyCrossInterpreterData _data; } _PyXI_namespace_item; static void _sharednsitem_clear(_PyXI_namespace_item *); // forward static int -_sharednsitem_init(_PyXI_namespace_item *item, PyObject *key) +_sharednsitem_init(_PyXI_namespace_item *item, int64_t interpid, PyObject *key) { + assert(interpid >= 0); + item->interpid = interpid; item->name = _copy_string_obj_raw(key); if (item->name == NULL) { return -1; } - item->hasdata = 0; + item->data = NULL; return 0; } @@ -785,10 +790,23 @@ static int _sharednsitem_set_value(_PyXI_namespace_item *item, PyObject *value) { assert(item->name != NULL); - assert(!item->hasdata); - item->hasdata = 1; - if (_PyObject_GetCrossInterpreterData(value, &item->data) != 0) { - item->hasdata = 0; + assert(item->data == NULL); + item->data = &item->_data; + if (item->interpid == PyInterpreterState_GetID(PyInterpreterState_Get())) { + item->data = &item->_data; + } + else { + item->data = PyMem_RawMalloc(sizeof(_PyCrossInterpreterData)); + if (item->data == NULL) { + PyErr_NoMemory(); + return -1; + } + } + if (_PyObject_GetCrossInterpreterData(value, item->data) != 0) { + if (item->data != &item->_data) { + PyMem_RawFree(item->data); + } + item->data = NULL; return -1; } return 0; @@ -801,9 +819,11 @@ _sharednsitem_clear(_PyXI_namespace_item *item) PyMem_RawFree((void *)item->name); item->name = NULL; } - if (item->hasdata) { - item->hasdata = 0; - (void)_release_xid_data(&item->data); + _PyCrossInterpreterData *data = item->data; + if (data != NULL) { + item->data = NULL; + int rawfree = (data == &item->_data); + (void)_release_xid_data(data, rawfree); } } @@ -815,8 +835,8 @@ _sharednsitem_apply(_PyXI_namespace_item *item, PyObject *ns, PyObject *dflt) return -1; } PyObject *value; - if (item->hasdata) { - value = _PyCrossInterpreterData_NewObject(&item->data); + if (item->data != NULL) { + value = _PyCrossInterpreterData_NewObject(item->data); if (value == NULL) { Py_DECREF(name); return -1; @@ -889,7 +909,7 @@ _PyXI_FreeNamespace(_PyXI_namespace *ns) } else { // We can assume the first item represents all items. - assert(ns->items[0].data.interpid == interp->id); + assert(ns->items[0].data->interpid == interp->id); if (interp == PyInterpreterState_Get()) { // We can avoid pending calls. _free_xi_namespace(ns); @@ -918,13 +938,14 @@ _PyXI_NamespaceFromNames(PyObject *names) if (ns == NULL) { return NULL; } + int64_t interpid = PyInterpreterState_Get()->id; for (Py_ssize_t i=0; i < len; i++) { PyObject *key = PySequence_GetItem(names, i); if (key == NULL) { break; } struct _sharednsitem *item = &ns->items[i]; - int res = _sharednsitem_init(item, key); + int res = _sharednsitem_init(item, interpid, key); Py_DECREF(key); if (res < 0) { break; @@ -958,6 +979,7 @@ _PyXI_NamespaceFromDict(PyObject *nsobj) return NULL; } ns->interp = PyInterpreterState_Get(); + int64_t interpid = ns->interp->id; Py_ssize_t pos = 0; for (Py_ssize_t i=0; i < len; i++) { @@ -966,7 +988,7 @@ _PyXI_NamespaceFromDict(PyObject *nsobj) break; } _PyXI_namespace_item *item = &ns->items[i]; - if (_sharednsitem_init(item, key) != 0) { + if (_sharednsitem_init(item, interpid, key) != 0) { break; } if (_sharednsitem_set_value(item, value) < 0) { From 0bd42e03f1a339f4af31ebf08eda715bb2b8ede2 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 30 Oct 2023 18:54:17 -0600 Subject: [PATCH 11/22] Add _PyXI_FillNamespaceFromDict(). --- Include/internal/pycore_crossinterp.h | 3 ++ Python/crossinterp.c | 51 ++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 4c5a4dfcf77c66..6c85b0b307630b 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -180,6 +180,9 @@ typedef struct _sharedns _PyXI_namespace; PyAPI_FUNC(void) _PyXI_FreeNamespace(_PyXI_namespace *ns); PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromNames(PyObject *names); PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromDict(PyObject *nsobj); +PyAPI_FUNC(int) _PyXI_FillNamespaceFromDict( + _PyXI_namespace *ns, + PyObject *nsobj); PyAPI_FUNC(int) _PyXI_ApplyNamespace( _PyXI_namespace *ns, PyObject *nsobj, diff --git a/Python/crossinterp.c b/Python/crossinterp.c index a87abdf66faed2..a1e494ce8da39f 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -813,12 +813,8 @@ _sharednsitem_set_value(_PyXI_namespace_item *item, PyObject *value) } static void -_sharednsitem_clear(_PyXI_namespace_item *item) +_sharednsitem_clear_data(_PyXI_namespace_item *item) { - if (item->name != NULL) { - PyMem_RawFree((void *)item->name); - item->name = NULL; - } _PyCrossInterpreterData *data = item->data; if (data != NULL) { item->data = NULL; @@ -827,6 +823,35 @@ _sharednsitem_clear(_PyXI_namespace_item *item) } } +static void +_sharednsitem_clear(_PyXI_namespace_item *item) +{ + if (item->name != NULL) { + PyMem_RawFree((void *)item->name); + item->name = NULL; + } + _sharednsitem_clear_data(item); +} + +static int +_sharednsitem_copy_from_ns(struct _sharednsitem *item, PyObject *ns) +{ + assert(item->name != NULL); + assert(item->data == NULL); + PyObject *value = PyDict_GetItemString(ns, item->name); // borrowed + if (value == NULL) { + if (PyErr_Occurred()) { + return -1; + } + // When applied, this item will be set to the default (or fail). + return 0; + } + if (_sharednsitem_set_value(item, value) < 0) { + return -1; + } + return 0; +} + static int _sharednsitem_apply(_PyXI_namespace_item *item, PyObject *ns, PyObject *dflt) { @@ -1003,6 +1028,22 @@ _PyXI_NamespaceFromDict(PyObject *nsobj) return ns; } +int +_PyXI_FillNamespaceFromDict(_PyXI_namespace *ns, PyObject *nsobj) +{ + for (Py_ssize_t i=0; i < ns->len; i++) { + _PyXI_namespace_item *item = &ns->items[i]; + if (_sharednsitem_copy_from_ns(item, nsobj) < 0) { + // Clear out the ones we set so far. + for (Py_ssize_t j=0; j < i; j++) { + _sharednsitem_clear_data(&ns->items[j]); + } + return -1; + } + } + return 0; +} + int _PyXI_ApplyNamespace(_PyXI_namespace *ns, PyObject *nsobj, PyObject *dflt) { From a230f7786d9b6ead45fb2562d7d360e47b08cc93 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 31 Oct 2023 10:33:44 -0600 Subject: [PATCH 12/22] Add xid_state structs and lifecycle funcs. --- Include/internal/pycore_crossinterp.h | 20 ++++++++++++++++++ Include/internal/pycore_interp.h | 4 ++-- Include/internal/pycore_runtime.h | 4 ++-- Python/crossinterp.c | 30 ++++++++++++++++++++++----- Python/pylifecycle.c | 6 ++++++ Python/pystate.c | 14 ++++++------- 6 files changed, 62 insertions(+), 16 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 6c85b0b307630b..6091a37c2dfb9c 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -139,6 +139,26 @@ PyAPI_FUNC(int) _PyCrossInterpreterData_UnregisterClass(PyTypeObject *); PyAPI_FUNC(crossinterpdatafunc) _PyCrossInterpreterData_Lookup(PyObject *); +/*************************/ +/* runtime state */ +/*************************/ + +struct _xi_runtime_state { + // builtin types + // XXX Remove this field once we have a tp_* slot. + struct _xidregistry registry; +}; + +struct _xi_state { + // heap types + // XXX Remove this field once we have a tp_* slot. + struct _xidregistry registry; +}; + +extern PyStatus _PyXI_Init(PyInterpreterState *interp); +extern void _PyXI_Fini(PyInterpreterState *interp); + + /***************************/ /* short-term data sharing */ /***************************/ diff --git a/Include/internal/pycore_interp.h b/Include/internal/pycore_interp.h index a067a60eca05df..78b841afae937e 100644 --- a/Include/internal/pycore_interp.h +++ b/Include/internal/pycore_interp.h @@ -153,8 +153,8 @@ struct _is { Py_ssize_t co_extra_user_count; freefunc co_extra_freefuncs[MAX_CO_EXTRA_USERS]; - // XXX Remove this field once we have a tp_* slot. - struct _xidregistry xidregistry; + /* cross-interpreter data and utils */ + struct _xi_state xi; #ifdef HAVE_FORK PyObject *before_forkers; diff --git a/Include/internal/pycore_runtime.h b/Include/internal/pycore_runtime.h index 320e5bbedc068a..8fb73dd6b7dc0b 100644 --- a/Include/internal/pycore_runtime.h +++ b/Include/internal/pycore_runtime.h @@ -200,8 +200,8 @@ typedef struct pyruntimestate { possible to facilitate out-of-process observability tools. */ - // XXX Remove this field once we have a tp_* slot. - struct _xidregistry xidregistry; + /* cross-interpreter data and utils */ + struct _xi_runtime_state xi; struct _pymem_allocators allocators; struct _obmalloc_global_state obmalloc; diff --git a/Python/crossinterp.c b/Python/crossinterp.c index a1e494ce8da39f..1109a2503b324a 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -4,6 +4,7 @@ #include "Python.h" #include "pycore_ceval.h" // _Py_simple_func #include "pycore_crossinterp.h" // struct _xid +#include "pycore_initconfig.h" // _PyStatus_OK() #include "pycore_pyerrors.h" // _PyErr_Clear() #include "pycore_pystate.h" // _PyInterpreterState_GET() #include "pycore_weakref.h" // _PyWeakref_GET_REF() @@ -352,6 +353,7 @@ _xidregistry_remove_entry(struct _xidregistry *xidregistry, } // This is used in pystate.c (for now). +// XXX Call this is _PyXI_Fini() instead of _PyRuntimeState_Fini()? void _Py_xidregistry_clear(struct _xidregistry *xidregistry) { @@ -394,10 +396,10 @@ _xidregistry_find_type(struct _xidregistry *xidregistry, PyTypeObject *cls) static inline struct _xidregistry * _get_xidregistry(PyInterpreterState *interp, PyTypeObject *cls) { - struct _xidregistry *xidregistry = &interp->runtime->xidregistry; + struct _xidregistry *xidregistry = &interp->runtime->xi.registry; if (cls->tp_flags & Py_TPFLAGS_HEAPTYPE) { - assert(interp->xidregistry.mutex == xidregistry->mutex); - xidregistry = &interp->xidregistry; + assert(interp->xi.registry.mutex == xidregistry->mutex); + xidregistry = &interp->xi.registry; } return xidregistry; } @@ -407,8 +409,8 @@ static void _register_builtins_for_crossinterpreter_data(struct _xidregistry *xi static inline void _ensure_builtins_xid(PyInterpreterState *interp, struct _xidregistry *xidregistry) { - if (xidregistry != &interp->xidregistry) { - assert(xidregistry == &interp->runtime->xidregistry); + if (xidregistry != &interp->xi.registry) { + assert(xidregistry == &interp->runtime->xi.registry); if (xidregistry->head == NULL) { _register_builtins_for_crossinterpreter_data(xidregistry); } @@ -433,6 +435,7 @@ _PyCrossInterpreterData_RegisterClass(PyTypeObject *cls, struct _xidregistry *xidregistry = _get_xidregistry(interp, cls); PyThread_acquire_lock(xidregistry->mutex, WAIT_LOCK); + // XXX Do this once in _PyXI_Init()? _ensure_builtins_xid(interp, xidregistry); struct _xidregitem *matched = _xidregistry_find_type(xidregistry, cls); @@ -1274,3 +1277,20 @@ _PyXI_Exit(_PyXI_session *session) _capture_current_exception(session); _exit_session(session); } + + +/*********************/ +/* runtime lifecycle */ +/*********************/ + +PyStatus +_PyXI_Init(PyInterpreterState *interp) +{ + return _PyStatus_OK(); +} + +void +_PyXI_Fini(PyInterpreterState *interp) +{ + // For now we don't do anything. +} diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 7b56034541756a..58b2e8032e9225 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -854,6 +854,11 @@ pycore_interp_init(PyThreadState *tstate) goto done; } + status = _PyXI_Init(interp); + if (_PyStatus_EXCEPTION(status)) { + goto done; + } + const PyConfig *config = _PyInterpreterState_GetConfig(interp); status = _PyImport_InitCore(tstate, sysmod, config->_install_importlib); @@ -1736,6 +1741,7 @@ finalize_interp_types(PyInterpreterState *interp) { _PyUnicode_FiniTypes(interp); _PySys_FiniTypes(interp); + _PyXI_Fini(interp); _PyExc_Fini(interp); _PyAsyncGen_Fini(interp); _PyContext_Fini(interp); diff --git a/Python/pystate.c b/Python/pystate.c index d97a03caf491c4..c4f1a1c5c099ae 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -382,7 +382,7 @@ _Py_COMP_DIAG_POP #define LOCKS_INIT(runtime) \ { \ &(runtime)->interpreters.mutex, \ - &(runtime)->xidregistry.mutex, \ + &(runtime)->xi.registry.mutex, \ &(runtime)->getargs.mutex, \ &(runtime)->unicode_state.ids.lock, \ &(runtime)->imports.extensions.mutex, \ @@ -505,7 +505,7 @@ _PyRuntimeState_Fini(_PyRuntimeState *runtime) assert(runtime->object_state.interpreter_leaks == 0); #endif - _Py_xidregistry_clear(&runtime->xidregistry); + _Py_xidregistry_clear(&runtime->xi.registry); if (gilstate_tss_initialized(runtime)) { gilstate_tss_fini(runtime); @@ -556,7 +556,7 @@ _PyRuntimeState_ReInitThreads(_PyRuntimeState *runtime) _PyInterpreterState_DeleteExceptMain(), so we only need to update the main interpreter here. */ assert(runtime->interpreters.main != NULL); - runtime->interpreters.main->xidregistry.mutex = runtime->xidregistry.mutex; + runtime->interpreters.main->xi.registry.mutex = runtime->xi.registry.mutex; PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc); @@ -720,8 +720,8 @@ init_interpreter(PyInterpreterState *interp, } interp->f_opcode_trace_set = false; - assert(runtime->xidregistry.mutex != NULL); - interp->xidregistry.mutex = runtime->xidregistry.mutex; + assert(runtime->xi.registry.mutex != NULL); + interp->xi.registry.mutex = runtime->xi.registry.mutex; interp->_initialized = 1; return _PyStatus_OK(); @@ -948,9 +948,9 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) Py_CLEAR(interp->sysdict); Py_CLEAR(interp->builtins); - _Py_xidregistry_clear(&interp->xidregistry); + _Py_xidregistry_clear(&interp->xi.registry); /* The lock is owned by the runtime, so we don't free it here. */ - interp->xidregistry.mutex = NULL; + interp->xi.registry.mutex = NULL; if (tstate->interp == interp) { /* We are now safe to fix tstate->_status.cleared. */ From 5675f86ecce18baaaf33433c4ae1c0f8a7f151fb Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 31 Oct 2023 11:01:29 -0600 Subject: [PATCH 13/22] Add PyExc_NotShareableError. --- Include/internal/pycore_crossinterp.h | 3 ++ Python/crossinterp.c | 69 +++++++++++++++++++-------- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 6091a37c2dfb9c..740ce62f068e5a 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -153,6 +153,9 @@ struct _xi_state { // heap types // XXX Remove this field once we have a tp_* slot. struct _xidregistry registry; + + // heap types + PyObject *PyExc_NotShareableError; }; extern PyStatus _PyXI_Init(PyInterpreterState *interp); diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 1109a2503b324a..8d336aa573a6af 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -172,25 +172,49 @@ _check_xidata(PyThreadState *tstate, _PyCrossInterpreterData *data) return 0; } -crossinterpdatafunc _PyCrossInterpreterData_Lookup(PyObject *); +static crossinterpdatafunc _lookup_getdata_from_registry( + PyInterpreterState *, PyObject *); -/* This is a separate func from _PyCrossInterpreterData_Lookup in order - to keep the registry code separate. */ static crossinterpdatafunc -_lookup_getdata(PyObject *obj) +_lookup_getdata(PyInterpreterState *interp, PyObject *obj) { - crossinterpdatafunc getdata = _PyCrossInterpreterData_Lookup(obj); - if (getdata == NULL && PyErr_Occurred() == 0) - PyErr_Format(PyExc_ValueError, + /* Cross-interpreter objects are looked up by exact match on the class. + We can reassess this policy when we move from a global registry to a + tp_* slot. */ + return _lookup_getdata_from_registry(interp, obj); +} + +crossinterpdatafunc +_PyCrossInterpreterData_Lookup(PyObject *obj) +{ + PyInterpreterState *interp = _PyInterpreterState_GET(); + return _lookup_getdata(interp, obj); +} + +static inline void +_set_xid_lookup_failure(PyInterpreterState *interp, PyObject *obj) +{ + PyObject *exctype = interp->xi.PyExc_NotShareableError; + assert(exctype != NULL); + if (obj == NULL) { + PyErr_SetString(exctype, + "object does not support cross-interpreter data"); + } + else { + PyErr_Format(exctype, "%S does not support cross-interpreter data", obj); - return getdata; + } } int _PyObject_CheckCrossInterpreterData(PyObject *obj) { - crossinterpdatafunc getdata = _lookup_getdata(obj); + PyInterpreterState *interp = _PyInterpreterState_GET(); + crossinterpdatafunc getdata = _lookup_getdata(interp, obj); if (getdata == NULL) { + if (!PyErr_Occurred()) { + _set_xid_lookup_failure(interp, obj); + } return -1; } return 0; @@ -212,9 +236,12 @@ _PyObject_GetCrossInterpreterData(PyObject *obj, _PyCrossInterpreterData *data) // Call the "getdata" func for the object. Py_INCREF(obj); - crossinterpdatafunc getdata = _lookup_getdata(obj); + crossinterpdatafunc getdata = _lookup_getdata(interp, obj); if (getdata == NULL) { Py_DECREF(obj); + if (!PyErr_Occurred()) { + _set_xid_lookup_failure(interp, obj); + } return -1; } int res = getdata(tstate, obj, data); @@ -474,17 +501,11 @@ _PyCrossInterpreterData_UnregisterClass(PyTypeObject *cls) return res; } - -/* Cross-interpreter objects are looked up by exact match on the class. - We can reassess this policy when we move from a global registry to a - tp_* slot. */ - -crossinterpdatafunc -_PyCrossInterpreterData_Lookup(PyObject *obj) +static crossinterpdatafunc +_lookup_getdata_from_registry(PyInterpreterState *interp, PyObject *obj) { PyTypeObject *cls = Py_TYPE(obj); - PyInterpreterState *interp = _PyInterpreterState_GET(); struct _xidregistry *xidregistry = _get_xidregistry(interp, cls); PyThread_acquire_lock(xidregistry->mutex, WAIT_LOCK); @@ -1286,11 +1307,21 @@ _PyXI_Exit(_PyXI_session *session) PyStatus _PyXI_Init(PyInterpreterState *interp) { + // Initialize NotShareableError (a heap type). + PyObject *exctype = PyErr_NewException("_interpreters.NotShareableError", + PyExc_ValueError, NULL); + if (exctype == NULL) { + PyErr_Clear(); + return _PyStatus_ERR("could not initialize NotShareableError"); + } + interp->xi.PyExc_NotShareableError = exctype; + return _PyStatus_OK(); } void _PyXI_Fini(PyInterpreterState *interp) { - // For now we don't do anything. + // Dealloc heap type exceptions. + Py_CLEAR(interp->xi.PyExc_NotShareableError); } From 45488f20dcc699099fb83db32c294d4ed1654433 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 31 Oct 2023 11:10:31 -0600 Subject: [PATCH 14/22] Propagate the ValueError when a value is not shareable. --- Include/internal/pycore_crossinterp.h | 18 ++++--- Modules/_xxsubinterpretersmodule.c | 2 +- Python/crossinterp.c | 75 +++++++++++++++++++++------ 3 files changed, 72 insertions(+), 23 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 740ce62f068e5a..e300a0d1fe74bf 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -172,6 +172,7 @@ typedef enum error_code { _PyXI_ERR_OTHER = -2, _PyXI_ERR_NO_MEMORY = -3, _PyXI_ERR_ALREADY_RUNNING = -4, + _PyXI_ERR_NOT_SHAREABLE = -5, } _PyXI_errcode; PyAPI_FUNC(int) _PyXI_ApplyErrorCode( @@ -198,14 +199,18 @@ PyAPI_FUNC(void) _PyXI_ApplyExceptionInfo( PyObject *exctype); +typedef struct xi_session _PyXI_session; typedef struct _sharedns _PyXI_namespace; PyAPI_FUNC(void) _PyXI_FreeNamespace(_PyXI_namespace *ns); PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromNames(PyObject *names); -PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromDict(PyObject *nsobj); +PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromDict( + PyObject *nsobj, + _PyXI_session *session); PyAPI_FUNC(int) _PyXI_FillNamespaceFromDict( _PyXI_namespace *ns, - PyObject *nsobj); + PyObject *nsobj, + _PyXI_session *session); PyAPI_FUNC(int) _PyXI_ApplyNamespace( _PyXI_namespace *ns, PyObject *nsobj, @@ -222,7 +227,7 @@ PyAPI_FUNC(int) _PyXI_ApplyNamespace( // isolation between interpreters. This includes setting objects // in the target's __main__ module on the way in, and capturing // uncaught exceptions on the way out. -typedef struct xi_session { +struct xi_session { // Once a session has been entered, this is the tstate that was // current before the session. If it is different from cur_tstate // then we must have switched interpreters. Either way, this will @@ -253,12 +258,13 @@ typedef struct xi_session { // -- pre-allocated memory -- _PyXI_exception_info _exc; -} _PyXI_session; + _PyXI_errcode _exc_override; +}; PyAPI_FUNC(int) _PyXI_Enter( + _PyXI_session *session, PyInterpreterState *interp, - PyObject *nsupdates, - _PyXI_session *session); + PyObject *nsupdates); PyAPI_FUNC(void) _PyXI_Exit(_PyXI_session *session); PyAPI_FUNC(void) _PyXI_ApplyCapturedException( diff --git a/Modules/_xxsubinterpretersmodule.c b/Modules/_xxsubinterpretersmodule.c index ee8586cb117c1a..001fa887847cbd 100644 --- a/Modules/_xxsubinterpretersmodule.c +++ b/Modules/_xxsubinterpretersmodule.c @@ -235,7 +235,7 @@ _run_in_interpreter(PyInterpreterState *interp, _PyXI_session session = {0}; // Prep and switch interpreters. - if (_PyXI_Enter(interp, shareables, &session) < 0) { + if (_PyXI_Enter(&session, interp, shareables) < 0) { assert(!PyErr_Occurred()); _PyXI_ApplyExceptionInfo(session.exc, excwrapper); assert(PyErr_Occurred()); diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 8d336aa573a6af..dead6e24abb183 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -192,11 +192,16 @@ _PyCrossInterpreterData_Lookup(PyObject *obj) } static inline void -_set_xid_lookup_failure(PyInterpreterState *interp, PyObject *obj) +_set_xid_lookup_failure(PyInterpreterState *interp, + PyObject *obj, const char *msg) { PyObject *exctype = interp->xi.PyExc_NotShareableError; assert(exctype != NULL); - if (obj == NULL) { + if (msg != NULL) { + assert(obj == NULL); + PyErr_SetString(exctype, msg); + } + else if (obj == NULL) { PyErr_SetString(exctype, "object does not support cross-interpreter data"); } @@ -213,7 +218,7 @@ _PyObject_CheckCrossInterpreterData(PyObject *obj) crossinterpdatafunc getdata = _lookup_getdata(interp, obj); if (getdata == NULL) { if (!PyErr_Occurred()) { - _set_xid_lookup_failure(interp, obj); + _set_xid_lookup_failure(interp, obj, NULL); } return -1; } @@ -240,7 +245,7 @@ _PyObject_GetCrossInterpreterData(PyObject *obj, _PyCrossInterpreterData *data) if (getdata == NULL) { Py_DECREF(obj); if (!PyErr_Occurred()) { - _set_xid_lookup_failure(interp, obj); + _set_xid_lookup_failure(interp, obj, NULL); } return -1; } @@ -719,6 +724,9 @@ _PyXI_ApplyErrorCode(_PyXI_errcode code, PyInterpreterState *interp) assert(_PyInterpreterState_IsRunningMain(interp)); _PyInterpreterState_FailIfRunningMain(interp); break; + case _PyXI_ERR_NOT_SHAREABLE: + _set_xid_lookup_failure(interp, NULL, NULL); + break; default: #ifdef Py_DEBUG Py_UNREACHABLE(); @@ -778,6 +786,10 @@ _PyXI_ApplyExceptionInfo(_PyXI_exception_info *info, PyObject *exctype) // Raise an exception that proxies the propagated exception. _Py_excinfo_Apply(&info->uncaught, exctype); } + else if (info->code == _PyXI_ERR_NOT_SHAREABLE) { + // Propagate the exception directly. + _set_xid_lookup_failure(info->interp, NULL, info->uncaught.msg); + } else { // Raise an exception corresponding to the code. assert(info->code != _PyXI_ERR_NO_ERROR); @@ -831,6 +843,8 @@ _sharednsitem_set_value(_PyXI_namespace_item *item, PyObject *value) PyMem_RawFree(item->data); } item->data = NULL; + // The caller may want to propagate PyExc_NotShareableError + // if currently switched between interpreters. return -1; } return 0; @@ -1007,9 +1021,14 @@ _PyXI_NamespaceFromNames(PyObject *names) return ns; } +static void _propagate_not_shareable_error(_PyXI_session *); + +// All items are expected to be shareable. _PyXI_namespace * -_PyXI_NamespaceFromDict(PyObject *nsobj) +_PyXI_NamespaceFromDict(PyObject *nsobj, _PyXI_session *session) { + // session must be entered already, if provided. + assert(session == NULL || session->init_tstate != NULL); if (nsobj == NULL || nsobj == Py_None) { return NULL; } @@ -1034,30 +1053,37 @@ _PyXI_NamespaceFromDict(PyObject *nsobj) for (Py_ssize_t i=0; i < len; i++) { PyObject *key, *value; if (!PyDict_Next(nsobj, &pos, &key, &value)) { - break; + goto error; } _PyXI_namespace_item *item = &ns->items[i]; if (_sharednsitem_init(item, interpid, key) != 0) { - break; + goto error; } if (_sharednsitem_set_value(item, value) < 0) { _sharednsitem_clear(item); - break; + _propagate_not_shareable_error(session); + goto error; } } - if (PyErr_Occurred()) { - _PyXI_FreeNamespace(ns); - return NULL; - } return ns; + +error: + assert(PyErr_Occurred() + || (session != NULL && session->exc_override != NULL)); + _PyXI_FreeNamespace(ns); + return NULL; } int -_PyXI_FillNamespaceFromDict(_PyXI_namespace *ns, PyObject *nsobj) +_PyXI_FillNamespaceFromDict(_PyXI_namespace *ns, PyObject *nsobj, + _PyXI_session *session) { + // session must be entered already, if provided. + assert(session == NULL || session->init_tstate != NULL); for (Py_ssize_t i=0; i < ns->len; i++) { _PyXI_namespace_item *item = &ns->items[i]; if (_sharednsitem_copy_from_ns(item, nsobj) < 0) { + _propagate_not_shareable_error(session); // Clear out the ones we set so far. for (Py_ssize_t j=0; j < i; j++) { _sharednsitem_clear_data(&ns->items[j]); @@ -1151,6 +1177,20 @@ _exit_session(_PyXI_session *session) session->init_tstate = NULL; } +static void +_propagate_not_shareable_error(_PyXI_session *session) +{ + if (session == NULL) { + return; + } + PyInterpreterState *interp = _PyInterpreterState_GET(); + if (PyErr_ExceptionMatches(interp->xi.PyExc_NotShareableError)) { + // We want to propagate the exception directly. + session->_exc_override = _PyXI_ERR_NOT_SHAREABLE; + session->exc_override = &session->_exc_override; + } +} + static void _capture_current_exception(_PyXI_session *session) { @@ -1172,6 +1212,9 @@ _capture_current_exception(_PyXI_session *session) // We want to actually capture the current exception. excval = PyErr_GetRaisedException(); } + else if (errcode == _PyXI_ERR_NOT_SHAREABLE) { + // We will set the errcode, in addition to capturing the exception. + } else { // We could do a variety of things here, depending on errcode. // However, for now we simply ignore the exception and rely @@ -1229,13 +1272,13 @@ _PyXI_HasCapturedException(_PyXI_session *session) } int -_PyXI_Enter(PyInterpreterState *interp, PyObject *nsupdates, - _PyXI_session *session) +_PyXI_Enter(_PyXI_session *session, + PyInterpreterState *interp, PyObject *nsupdates) { // Convert the attrs for cross-interpreter use. _PyXI_namespace *sharedns = NULL; if (nsupdates != NULL) { - sharedns = _PyXI_NamespaceFromDict(nsupdates); + sharedns = _PyXI_NamespaceFromDict(nsupdates, NULL); if (sharedns == NULL && PyErr_Occurred()) { assert(session->exc == NULL); return -1; From 6f0736442dbde8340fd0191139cc667be1cd2607 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 31 Oct 2023 13:47:46 -0600 Subject: [PATCH 15/22] Propagate errors in _PyXI_Enter() directly. --- Include/internal/pycore_crossinterp.h | 4 ++- Python/crossinterp.c | 49 ++++++++++++++++++++++----- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index e300a0d1fe74bf..9f3e9e83e6440c 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -172,7 +172,9 @@ typedef enum error_code { _PyXI_ERR_OTHER = -2, _PyXI_ERR_NO_MEMORY = -3, _PyXI_ERR_ALREADY_RUNNING = -4, - _PyXI_ERR_NOT_SHAREABLE = -5, + _PyXI_ERR_MAIN_NS_FAILURE = -5, + _PyXI_ERR_APPLY_NS_FAILURE = -6, + _PyXI_ERR_NOT_SHAREABLE = -7, } _PyXI_errcode; PyAPI_FUNC(int) _PyXI_ApplyErrorCode( diff --git a/Python/crossinterp.c b/Python/crossinterp.c index dead6e24abb183..744fdeba7f4731 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -724,6 +724,14 @@ _PyXI_ApplyErrorCode(_PyXI_errcode code, PyInterpreterState *interp) assert(_PyInterpreterState_IsRunningMain(interp)); _PyInterpreterState_FailIfRunningMain(interp); break; + case _PyXI_ERR_MAIN_NS_FAILURE: + PyErr_SetString(PyExc_RuntimeError, + "failed to get __main__ namespace"); + break; + case _PyXI_ERR_APPLY_NS_FAILURE: + PyErr_SetString(PyExc_RuntimeError, + "failed to apply namespace to __main__"); + break; case _PyXI_ERR_NOT_SHAREABLE: _set_xid_lookup_failure(interp, NULL, NULL); break; @@ -794,6 +802,14 @@ _PyXI_ApplyExceptionInfo(_PyXI_exception_info *info, PyObject *exctype) // Raise an exception corresponding to the code. assert(info->code != _PyXI_ERR_NO_ERROR); (void)_PyXI_ApplyErrorCode(info->code, info->interp); + if (info->uncaught.type != NULL || info->uncaught.msg != NULL) { + // __context__ will be set to a proxy of the propagated exception. + PyObject *exc = PyErr_GetRaisedException(); + _Py_excinfo_Apply(&info->uncaught, exctype); + PyObject *exc2 = PyErr_GetRaisedException(); + PyException_SetContext(exc, exc2); + PyErr_SetRaisedException(exc); + } } assert(PyErr_Occurred()); } @@ -1212,14 +1228,15 @@ _capture_current_exception(_PyXI_session *session) // We want to actually capture the current exception. excval = PyErr_GetRaisedException(); } - else if (errcode == _PyXI_ERR_NOT_SHAREABLE) { - // We will set the errcode, in addition to capturing the exception. + else if (errcode == _PyXI_ERR_ALREADY_RUNNING) { + // We don't need the exception info. + PyErr_Clear(); } else { // We could do a variety of things here, depending on errcode. - // However, for now we simply ignore the exception and rely - // strictly on errcode. - PyErr_Clear(); + // However, for now we simply capture the exception and save + // the errcode. + excval = PyErr_GetRaisedException(); } // Capture the exception. @@ -1227,7 +1244,17 @@ _capture_current_exception(_PyXI_session *session) *exc = (_PyXI_exception_info){ .interp = session->init_tstate->interp, }; - const char *failure = _PyXI_InitExceptionInfo(exc, excval, errcode); + const char *failure; + if (excval == NULL) { + failure = _PyXI_InitExceptionInfo(exc, NULL, errcode); + } + else { + failure = _PyXI_InitExceptionInfo(exc, excval, + _PyXI_ERR_UNCAUGHT_EXCEPTION); + if (failure == NULL && session->exc_override != NULL) { + exc->code = errcode; + } + } // Handle capture failure. if (failure != NULL) { @@ -1302,11 +1329,13 @@ _PyXI_Enter(_PyXI_session *session, // Cache __main__.__dict__. PyObject *main_mod = PyUnstable_InterpreterState_GetMainModule(interp); if (main_mod == NULL) { + errcode = _PyXI_ERR_MAIN_NS_FAILURE; goto error; } PyObject *ns = PyModule_GetDict(main_mod); // borrowed Py_DECREF(main_mod); if (ns == NULL) { + errcode = _PyXI_ERR_MAIN_NS_FAILURE; goto error; } session->main_ns = Py_NewRef(ns); @@ -1314,19 +1343,21 @@ _PyXI_Enter(_PyXI_session *session, // Apply the cross-interpreter data. if (sharedns != NULL) { if (_PyXI_ApplyNamespace(sharedns, ns, NULL) < 0) { + errcode = _PyXI_ERR_APPLY_NS_FAILURE; goto error; } _PyXI_FreeNamespace(sharedns); } errcode = _PyXI_ERR_NO_ERROR; + assert(!PyErr_Occurred()); return 0; error: assert(PyErr_Occurred()); - if (errcode != _PyXI_ERR_UNCAUGHT_EXCEPTION) { - session->exc_override = &errcode; - } + // We want to propagate all exceptions here directly (best effort). + assert(errcode != _PyXI_ERR_UNCAUGHT_EXCEPTION); + session->exc_override = &errcode; _capture_current_exception(session); _exit_session(session); if (sharedns != NULL) { From 0201b7f6141fa0b91dca6ac0d47aadd665841eb9 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 1 Nov 2023 14:05:02 -0600 Subject: [PATCH 16/22] Factor out _init_not_shareable_error_type() and _fini_not_shareable_error_type(). --- Python/crossinterp.c | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 744fdeba7f4731..685aecd55c4a8d 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -65,6 +65,31 @@ _PyCrossInterpreterData_Free(_PyCrossInterpreterData *xid) } +/* exceptions */ + +static PyStatus +_init_not_shareable_error_type(PyInterpreterState *interp) +{ + const char *name = "_interpreters.NotShareableError"; + PyObject *base = PyExc_ValueError; + PyObject *ns = NULL; + PyObject *exctype = PyErr_NewException(name, base, ns); + if (exctype == NULL) { + PyErr_Clear(); + return _PyStatus_ERR("could not initialize NotShareableError"); + } + + interp->xi.PyExc_NotShareableError = exctype; + return _PyStatus_OK(); +} + +static void +_fini_not_shareable_error_type(PyInterpreterState *interp) +{ + Py_CLEAR(interp->xi.PyExc_NotShareableError); +} + + /* defining cross-interpreter data */ static inline void @@ -1381,14 +1406,13 @@ _PyXI_Exit(_PyXI_session *session) PyStatus _PyXI_Init(PyInterpreterState *interp) { - // Initialize NotShareableError (a heap type). - PyObject *exctype = PyErr_NewException("_interpreters.NotShareableError", - PyExc_ValueError, NULL); - if (exctype == NULL) { - PyErr_Clear(); - return _PyStatus_ERR("could not initialize NotShareableError"); + PyStatus status; + + // Initialize exceptions (heap types). + status = _init_not_shareable_error_type(interp); + if (_PyStatus_EXCEPTION(status)) { + return status; } - interp->xi.PyExc_NotShareableError = exctype; return _PyStatus_OK(); } @@ -1396,6 +1420,6 @@ _PyXI_Init(PyInterpreterState *interp) void _PyXI_Fini(PyInterpreterState *interp) { - // Dealloc heap type exceptions. - Py_CLEAR(interp->xi.PyExc_NotShareableError); + // Finalize exceptions (heap types). + _fini_not_shareable_error_type(interp); } From d32a918ae3bb9ea94f65e071a9cef8f7f908c406 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 1 Nov 2023 14:18:23 -0600 Subject: [PATCH 17/22] Drop some duplicate lines. --- Include/internal/pycore_crossinterp.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 9f3e9e83e6440c..0727d1ba8f4ce7 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -11,10 +11,6 @@ extern "C" { #include "pycore_pyerrors.h" -/**************************/ -/* cross-interpreter data */ -/**************************/ - /***************************/ /* cross-interpreter calls */ /***************************/ From 1d4fc875aee91480306e680d14f8dd30ec54966b Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 1 Nov 2023 14:36:34 -0600 Subject: [PATCH 18/22] Fix a comment. --- Include/internal/pycore_crossinterp.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 0727d1ba8f4ce7..1c9cc85601fcff 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -135,9 +135,9 @@ PyAPI_FUNC(int) _PyCrossInterpreterData_UnregisterClass(PyTypeObject *); PyAPI_FUNC(crossinterpdatafunc) _PyCrossInterpreterData_Lookup(PyObject *); -/*************************/ -/* runtime state */ -/*************************/ +/*****************************/ +/* runtime state & lifecycle */ +/*****************************/ struct _xi_runtime_state { // builtin types From 88c9d54e346c8890a705429318ad45bc7ddbae7c Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 1 Nov 2023 14:38:08 -0600 Subject: [PATCH 19/22] Call _PyXI_Fini() *before* the interpreter is cleared. --- Python/crossinterp.c | 7 +++++++ Python/pylifecycle.c | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 685aecd55c4a8d..1d5f690b4210b0 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -1408,6 +1408,8 @@ _PyXI_Init(PyInterpreterState *interp) { PyStatus status; + // XXX Initialize xidregistry. + // Initialize exceptions (heap types). status = _init_not_shareable_error_type(interp); if (_PyStatus_EXCEPTION(status)) { @@ -1417,9 +1419,14 @@ _PyXI_Init(PyInterpreterState *interp) return _PyStatus_OK(); } +// _PyXI_Fini() must be called before the interpreter is cleared, +// since we must clear some heap objects. + void _PyXI_Fini(PyInterpreterState *interp) { // Finalize exceptions (heap types). _fini_not_shareable_error_type(interp); + + // XXX Clear xidregistry. } diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 58b2e8032e9225..6248eef871d49a 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -738,6 +738,7 @@ pycore_init_types(PyInterpreterState *interp) if (_PyStatus_EXCEPTION(status)) { return status; } + return _PyStatus_OK(); } @@ -1741,7 +1742,6 @@ finalize_interp_types(PyInterpreterState *interp) { _PyUnicode_FiniTypes(interp); _PySys_FiniTypes(interp); - _PyXI_Fini(interp); _PyExc_Fini(interp); _PyAsyncGen_Fini(interp); _PyContext_Fini(interp); @@ -1778,6 +1778,7 @@ finalize_interp_clear(PyThreadState *tstate) { int is_main_interp = _Py_IsMainInterpreter(tstate->interp); + _PyXI_Fini(tstate->interp); _PyExc_ClearExceptionGroupType(tstate->interp); _Py_clear_generic_types(tstate->interp); From 2edcb49c64cb941e84e52eff46c88f9fe51ce454 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 1 Nov 2023 15:53:44 -0600 Subject: [PATCH 20/22] Fix init/fini. --- Include/internal/pycore_crossinterp.h | 2 + Include/internal/pycore_runtime_init.h | 5 + Python/crossinterp.c | 152 ++++++++++++++++++------- Python/pystate.c | 17 --- 4 files changed, 115 insertions(+), 61 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 1c9cc85601fcff..d53301128f03ed 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -126,6 +126,8 @@ struct _xidregitem { }; struct _xidregistry { + int global; /* builtin types or heap types */ + int initialized; PyThread_type_lock mutex; struct _xidregitem *head; }; diff --git a/Include/internal/pycore_runtime_init.h b/Include/internal/pycore_runtime_init.h index 0799b7e701ce95..fa5d8114abf0d7 100644 --- a/Include/internal/pycore_runtime_init.h +++ b/Include/internal/pycore_runtime_init.h @@ -95,6 +95,11 @@ extern PyTypeObject _PyExc_MemoryError; until _PyInterpreterState_Enable() is called. */ \ .next_id = -1, \ }, \ + .xi = { \ + .registry = { \ + .global = 1, \ + }, \ + }, \ /* A TSS key must be initialized with Py_tss_NEEDS_INIT \ in accordance with the specification. */ \ .autoTSSkey = Py_tss_NEEDS_INIT, \ diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 1d5f690b4210b0..1960addb1e80ad 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -358,6 +358,28 @@ _PyCrossInterpreterData_ReleaseAndRawFree(_PyCrossInterpreterData *data) alternative would be to add a tp_* slot for a class's crossinterpdatafunc. It would be simpler and more efficient. */ +static inline struct _xidregistry * +_get_global_xidregistry(_PyRuntimeState *runtime) +{ + return &runtime->xi.registry; +} + +static inline struct _xidregistry * +_get_xidregistry(PyInterpreterState *interp) +{ + return &interp->xi.registry; +} + +static inline struct _xidregistry * +_get_xidregistry_for_type(PyInterpreterState *interp, PyTypeObject *cls) +{ + struct _xidregistry *registry = _get_global_xidregistry(interp->runtime); + if (cls->tp_flags & Py_TPFLAGS_HEAPTYPE) { + registry = _get_xidregistry(interp); + } + return registry; +} + static int _xidregistry_add_type(struct _xidregistry *xidregistry, PyTypeObject *cls, crossinterpdatafunc getdata) @@ -409,10 +431,8 @@ _xidregistry_remove_entry(struct _xidregistry *xidregistry, return next; } -// This is used in pystate.c (for now). -// XXX Call this is _PyXI_Fini() instead of _PyRuntimeState_Fini()? -void -_Py_xidregistry_clear(struct _xidregistry *xidregistry) +static void +_xidregistry_clear(struct _xidregistry *xidregistry) { struct _xidregitem *cur = xidregistry->head; xidregistry->head = NULL; @@ -424,6 +444,22 @@ _Py_xidregistry_clear(struct _xidregistry *xidregistry) } } +static void +_xidregistry_lock(struct _xidregistry *registry) +{ + if (registry->mutex != NULL) { + PyThread_acquire_lock(registry->mutex, WAIT_LOCK); + } +} + +static void +_xidregistry_unlock(struct _xidregistry *registry) +{ + if (registry->mutex != NULL) { + PyThread_release_lock(registry->mutex); + } +} + static struct _xidregitem * _xidregistry_find_type(struct _xidregistry *xidregistry, PyTypeObject *cls) { @@ -450,30 +486,6 @@ _xidregistry_find_type(struct _xidregistry *xidregistry, PyTypeObject *cls) return NULL; } -static inline struct _xidregistry * -_get_xidregistry(PyInterpreterState *interp, PyTypeObject *cls) -{ - struct _xidregistry *xidregistry = &interp->runtime->xi.registry; - if (cls->tp_flags & Py_TPFLAGS_HEAPTYPE) { - assert(interp->xi.registry.mutex == xidregistry->mutex); - xidregistry = &interp->xi.registry; - } - return xidregistry; -} - -static void _register_builtins_for_crossinterpreter_data(struct _xidregistry *xidregistry); - -static inline void -_ensure_builtins_xid(PyInterpreterState *interp, struct _xidregistry *xidregistry) -{ - if (xidregistry != &interp->xi.registry) { - assert(xidregistry == &interp->runtime->xi.registry); - if (xidregistry->head == NULL) { - _register_builtins_for_crossinterpreter_data(xidregistry); - } - } -} - int _PyCrossInterpreterData_RegisterClass(PyTypeObject *cls, crossinterpdatafunc getdata) @@ -489,11 +501,8 @@ _PyCrossInterpreterData_RegisterClass(PyTypeObject *cls, int res = 0; PyInterpreterState *interp = _PyInterpreterState_GET(); - struct _xidregistry *xidregistry = _get_xidregistry(interp, cls); - PyThread_acquire_lock(xidregistry->mutex, WAIT_LOCK); - - // XXX Do this once in _PyXI_Init()? - _ensure_builtins_xid(interp, xidregistry); + struct _xidregistry *xidregistry = _get_xidregistry_for_type(interp, cls); + _xidregistry_lock(xidregistry); struct _xidregitem *matched = _xidregistry_find_type(xidregistry, cls); if (matched != NULL) { @@ -505,7 +514,7 @@ _PyCrossInterpreterData_RegisterClass(PyTypeObject *cls, res = _xidregistry_add_type(xidregistry, cls, getdata); finally: - PyThread_release_lock(xidregistry->mutex); + _xidregistry_unlock(xidregistry); return res; } @@ -514,8 +523,8 @@ _PyCrossInterpreterData_UnregisterClass(PyTypeObject *cls) { int res = 0; PyInterpreterState *interp = _PyInterpreterState_GET(); - struct _xidregistry *xidregistry = _get_xidregistry(interp, cls); - PyThread_acquire_lock(xidregistry->mutex, WAIT_LOCK); + struct _xidregistry *xidregistry = _get_xidregistry_for_type(interp, cls); + _xidregistry_lock(xidregistry); struct _xidregitem *matched = _xidregistry_find_type(xidregistry, cls); if (matched != NULL) { @@ -527,7 +536,7 @@ _PyCrossInterpreterData_UnregisterClass(PyTypeObject *cls) res = 1; } - PyThread_release_lock(xidregistry->mutex); + _xidregistry_unlock(xidregistry); return res; } @@ -536,15 +545,13 @@ _lookup_getdata_from_registry(PyInterpreterState *interp, PyObject *obj) { PyTypeObject *cls = Py_TYPE(obj); - struct _xidregistry *xidregistry = _get_xidregistry(interp, cls); - PyThread_acquire_lock(xidregistry->mutex, WAIT_LOCK); - - _ensure_builtins_xid(interp, xidregistry); + struct _xidregistry *xidregistry = _get_xidregistry_for_type(interp, cls); + _xidregistry_lock(xidregistry); struct _xidregitem *matched = _xidregistry_find_type(xidregistry, cls); crossinterpdatafunc func = matched != NULL ? matched->getdata : NULL; - PyThread_release_lock(xidregistry->mutex); + _xidregistry_unlock(xidregistry); return func; } @@ -680,6 +687,55 @@ _register_builtins_for_crossinterpreter_data(struct _xidregistry *xidregistry) } } +/* registry lifecycle */ + +static void +_xidregistry_init(struct _xidregistry *registry) +{ + if (registry->initialized) { + return; + } + registry->initialized = 1; + + if (registry->global) { + // We manage the mutex lifecycle in pystate.c. + assert(registry->mutex != NULL); + + // Registering the builtins is cheap so we don't bother doing it lazily. + assert(registry->head == NULL); + _register_builtins_for_crossinterpreter_data(registry); + } + else { + // Within an interpreter we rely on the GIL instead of a separate lock. + assert(registry->mutex == NULL); + + // There's nothing else to initialize. + } +} + +static void +_xidregistry_fini(struct _xidregistry *registry) +{ + if (!registry->initialized) { + return; + } + registry->initialized = 0; + + _xidregistry_clear(registry); + + if (registry->global) { + // We manage the mutex lifecycle in pystate.c. + assert(registry->mutex != NULL); + } + else { + // There's nothing else to finalize. + + // Within an interpreter we rely on the GIL instead of a separate lock. + assert(registry->mutex == NULL); + } +} + + /*************************/ /* convenience utilities */ /*************************/ @@ -1408,7 +1464,11 @@ _PyXI_Init(PyInterpreterState *interp) { PyStatus status; - // XXX Initialize xidregistry. + // Initialize the XID registry. + if (_Py_IsMainInterpreter(interp)) { + _xidregistry_init(_get_global_xidregistry(interp->runtime)); + } + _xidregistry_init(_get_xidregistry(interp)); // Initialize exceptions (heap types). status = _init_not_shareable_error_type(interp); @@ -1428,5 +1488,9 @@ _PyXI_Fini(PyInterpreterState *interp) // Finalize exceptions (heap types). _fini_not_shareable_error_type(interp); - // XXX Clear xidregistry. + // Finalize the XID registry. + _xidregistry_fini(_get_xidregistry(interp)); + if (_Py_IsMainInterpreter(interp)) { + _xidregistry_fini(_get_global_xidregistry(interp->runtime)); + } } diff --git a/Python/pystate.c b/Python/pystate.c index c4f1a1c5c099ae..8970e17a3c101b 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -494,9 +494,6 @@ _PyRuntimeState_Init(_PyRuntimeState *runtime) return _PyStatus_OK(); } -// This is defined in crossinterp.c (for now). -extern void _Py_xidregistry_clear(struct _xidregistry *); - void _PyRuntimeState_Fini(_PyRuntimeState *runtime) { @@ -505,8 +502,6 @@ _PyRuntimeState_Fini(_PyRuntimeState *runtime) assert(runtime->object_state.interpreter_leaks == 0); #endif - _Py_xidregistry_clear(&runtime->xi.registry); - if (gilstate_tss_initialized(runtime)) { gilstate_tss_fini(runtime); } @@ -552,11 +547,6 @@ _PyRuntimeState_ReInitThreads(_PyRuntimeState *runtime) for (int i = 0; i < NUMLOCKS; i++) { reinit_err += _PyThread_at_fork_reinit(lockptrs[i]); } - /* PyOS_AfterFork_Child(), which calls this function, later calls - _PyInterpreterState_DeleteExceptMain(), so we only need to update - the main interpreter here. */ - assert(runtime->interpreters.main != NULL); - runtime->interpreters.main->xi.registry.mutex = runtime->xi.registry.mutex; PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc); @@ -720,9 +710,6 @@ init_interpreter(PyInterpreterState *interp, } interp->f_opcode_trace_set = false; - assert(runtime->xi.registry.mutex != NULL); - interp->xi.registry.mutex = runtime->xi.registry.mutex; - interp->_initialized = 1; return _PyStatus_OK(); } @@ -948,10 +935,6 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) Py_CLEAR(interp->sysdict); Py_CLEAR(interp->builtins); - _Py_xidregistry_clear(&interp->xi.registry); - /* The lock is owned by the runtime, so we don't free it here. */ - interp->xi.registry.mutex = NULL; - if (tstate->interp == interp) { /* We are now safe to fix tstate->_status.cleared. */ // XXX Do this (much) earlier? From 8e53752a5b3d7410f3b1667c9ff59c64b944ce84 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 1 Nov 2023 15:56:59 -0600 Subject: [PATCH 21/22] Add _get_not_shareable_error_type(). --- Python/crossinterp.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 1960addb1e80ad..cfadda38b571ec 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -89,6 +89,13 @@ _fini_not_shareable_error_type(PyInterpreterState *interp) Py_CLEAR(interp->xi.PyExc_NotShareableError); } +static PyObject * +_get_not_shareable_error_type(PyInterpreterState *interp) +{ + assert(interp->xi.PyExc_NotShareableError != NULL); + return interp->xi.PyExc_NotShareableError; +} + /* defining cross-interpreter data */ @@ -220,7 +227,7 @@ static inline void _set_xid_lookup_failure(PyInterpreterState *interp, PyObject *obj, const char *msg) { - PyObject *exctype = interp->xi.PyExc_NotShareableError; + PyObject *exctype = _get_not_shareable_error_type(interp); assert(exctype != NULL); if (msg != NULL) { assert(obj == NULL); @@ -1281,7 +1288,7 @@ _propagate_not_shareable_error(_PyXI_session *session) return; } PyInterpreterState *interp = _PyInterpreterState_GET(); - if (PyErr_ExceptionMatches(interp->xi.PyExc_NotShareableError)) { + if (PyErr_ExceptionMatches(_get_not_shareable_error_type(interp))) { // We want to propagate the exception directly. session->_exc_override = _PyXI_ERR_NOT_SHAREABLE; session->exc_override = &session->_exc_override; From 53764c1273e94538271bfc17157dfc348272101e Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 1 Nov 2023 16:14:49 -0600 Subject: [PATCH 22/22] Export fewer symbols. --- Include/internal/pycore_crossinterp.h | 12 ------------ Include/internal/pycore_pyerrors.h | 10 +++++----- Python/crossinterp.c | 6 +++--- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index d53301128f03ed..9600dfb9600e60 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -175,10 +175,6 @@ typedef enum error_code { _PyXI_ERR_NOT_SHAREABLE = -7, } _PyXI_errcode; -PyAPI_FUNC(int) _PyXI_ApplyErrorCode( - _PyXI_errcode code, - PyInterpreterState *interp); - typedef struct _sharedexception { // The originating interpreter. @@ -190,23 +186,15 @@ typedef struct _sharedexception { _Py_excinfo uncaught; } _PyXI_exception_info; -PyAPI_FUNC(const char *) _PyXI_InitExceptionInfo( - _PyXI_exception_info *info, - PyObject *exc, - _PyXI_errcode code); PyAPI_FUNC(void) _PyXI_ApplyExceptionInfo( _PyXI_exception_info *info, PyObject *exctype); - typedef struct xi_session _PyXI_session; typedef struct _sharedns _PyXI_namespace; PyAPI_FUNC(void) _PyXI_FreeNamespace(_PyXI_namespace *ns); PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromNames(PyObject *names); -PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromDict( - PyObject *nsobj, - _PyXI_session *session); PyAPI_FUNC(int) _PyXI_FillNamespaceFromDict( _PyXI_namespace *ns, PyObject *nsobj, diff --git a/Include/internal/pycore_pyerrors.h b/Include/internal/pycore_pyerrors.h index a05a626a5cdcf7..67ef71c2616541 100644 --- a/Include/internal/pycore_pyerrors.h +++ b/Include/internal/pycore_pyerrors.h @@ -80,13 +80,13 @@ typedef struct _excinfo { const char *msg; } _Py_excinfo; -PyAPI_FUNC(void) _Py_excinfo_Clear(_Py_excinfo *info); -PyAPI_FUNC(int) _Py_excinfo_Copy(_Py_excinfo *dest, _Py_excinfo *src); -PyAPI_FUNC(const char *) _Py_excinfo_InitFromException( +extern void _Py_excinfo_Clear(_Py_excinfo *info); +extern int _Py_excinfo_Copy(_Py_excinfo *dest, _Py_excinfo *src); +extern const char * _Py_excinfo_InitFromException( _Py_excinfo *info, PyObject *exc); -PyAPI_FUNC(void) _Py_excinfo_Apply(_Py_excinfo *info, PyObject *exctype); -PyAPI_FUNC(const char *) _Py_excinfo_AsUTF8( +extern void _Py_excinfo_Apply(_Py_excinfo *info, PyObject *exctype); +extern const char * _Py_excinfo_AsUTF8( _Py_excinfo *info, char *buf, size_t bufsize); diff --git a/Python/crossinterp.c b/Python/crossinterp.c index cfadda38b571ec..d55393f4e05677 100644 --- a/Python/crossinterp.c +++ b/Python/crossinterp.c @@ -788,7 +788,7 @@ _release_xid_data(_PyCrossInterpreterData *data, int rawfree) /* error codes */ -int +static int _PyXI_ApplyErrorCode(_PyXI_errcode code, PyInterpreterState *interp) { assert(!PyErr_Occurred()); @@ -836,7 +836,7 @@ _PyXI_ApplyErrorCode(_PyXI_errcode code, PyInterpreterState *interp) /* shared exceptions */ -const char * +static const char * _PyXI_InitExceptionInfo(_PyXI_exception_info *info, PyObject *excobj, _PyXI_errcode code) { @@ -1128,7 +1128,7 @@ _PyXI_NamespaceFromNames(PyObject *names) static void _propagate_not_shareable_error(_PyXI_session *); // All items are expected to be shareable. -_PyXI_namespace * +static _PyXI_namespace * _PyXI_NamespaceFromDict(PyObject *nsobj, _PyXI_session *session) { // session must be entered already, if provided.