diff options
author | Eric Snow <ericsnowcurrently@gmail.com> | 2023-11-01 23:36:40 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-01 23:36:40 (GMT) |
commit | 9322ce90ac8f4d4647a59bbfab48fad6f4e4e856 (patch) | |
tree | c3dd4e202f02a639bca0412b71d1464fd4f624c5 | |
parent | cde1071b2a72e8261ca66053ef61431b7f3a81fd (diff) | |
download | cpython-9322ce90ac8f4d4647a59bbfab48fad6f4e4e856.zip cpython-9322ce90ac8f4d4647a59bbfab48fad6f4e4e856.tar.gz cpython-9322ce90ac8f4d4647a59bbfab48fad6f4e4e856.tar.bz2 |
gh-76785: Crossinterp utils additions (gh-111530)
This moves several general internal APIs out of _xxsubinterpretersmodule.c and into the new Python/crossinterp.c (and the corresponding internal headers).
Specifically:
* _Py_excinfo, etc.: the initial implementation for non-object exception snapshots (in pycore_pyerrors.h and Python/errors.c)
* _PyXI_exception_info, etc.: helpers for passing an exception beween interpreters (wraps _Py_excinfo)
* _PyXI_namespace, etc.: helpers for copying a dict of attrs between interpreters
* _PyXI_Enter(), _PyXI_Exit(): functions that abstract out the transitions between one interpreter and a second that will do some work temporarily
Again, these were all abstracted out of _xxsubinterpretersmodule.c as generalizations. I plan on proposing these as public API at some point.
-rw-r--r-- | Include/internal/pycore_crossinterp.h | 128 | ||||
-rw-r--r-- | Include/internal/pycore_interp.h | 4 | ||||
-rw-r--r-- | Include/internal/pycore_pyerrors.h | 24 | ||||
-rw-r--r-- | Include/internal/pycore_runtime.h | 4 | ||||
-rw-r--r-- | Include/internal/pycore_runtime_init.h | 5 | ||||
-rw-r--r-- | Lib/test/support/interpreters.py | 2 | ||||
-rw-r--r-- | Modules/_xxsubinterpretersmodule.c | 419 | ||||
-rw-r--r-- | Python/crossinterp.c | 992 | ||||
-rw-r--r-- | Python/errors.c | 175 | ||||
-rw-r--r-- | Python/pylifecycle.c | 7 | ||||
-rw-r--r-- | Python/pystate.c | 19 |
11 files changed, 1308 insertions, 471 deletions
diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 59e4cd9..9600dfb 100644 --- a/Include/internal/pycore_crossinterp.h +++ b/Include/internal/pycore_crossinterp.h @@ -8,6 +8,8 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif +#include "pycore_pyerrors.h" + /***************************/ /* cross-interpreter calls */ @@ -124,6 +126,8 @@ struct _xidregitem { }; struct _xidregistry { + int global; /* builtin types or heap types */ + int initialized; PyThread_type_lock mutex; struct _xidregitem *head; }; @@ -133,6 +137,130 @@ PyAPI_FUNC(int) _PyCrossInterpreterData_UnregisterClass(PyTypeObject *); PyAPI_FUNC(crossinterpdatafunc) _PyCrossInterpreterData_Lookup(PyObject *); +/*****************************/ +/* runtime state & lifecycle */ +/*****************************/ + +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; + + // heap types + PyObject *PyExc_NotShareableError; +}; + +extern PyStatus _PyXI_Init(PyInterpreterState *interp); +extern void _PyXI_Fini(PyInterpreterState *interp); + + +/***************************/ +/* 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_ERR_MAIN_NS_FAILURE = -5, + _PyXI_ERR_APPLY_NS_FAILURE = -6, + _PyXI_ERR_NOT_SHAREABLE = -7, +} _PyXI_errcode; + + +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(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(int) _PyXI_FillNamespaceFromDict( + _PyXI_namespace *ns, + PyObject *nsobj, + _PyXI_session *session); +PyAPI_FUNC(int) _PyXI_ApplyNamespace( + _PyXI_namespace *ns, + PyObject *nsobj, + PyObject *dflt); + + +// 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. +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_errcode _exc_override; +}; + +PyAPI_FUNC(int) _PyXI_Enter( + _PyXI_session *session, + PyInterpreterState *interp, + PyObject *nsupdates); +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/Include/internal/pycore_interp.h b/Include/internal/pycore_interp.h index a067a60..78b841a 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_pyerrors.h b/Include/internal/pycore_pyerrors.h index 184eb35..67ef71c 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; + +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); +extern void _Py_excinfo_Apply(_Py_excinfo *info, PyObject *exctype); +extern 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/Include/internal/pycore_runtime.h b/Include/internal/pycore_runtime.h index 320e5bb..8fb73dd 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/Include/internal/pycore_runtime_init.h b/Include/internal/pycore_runtime_init.h index 0799b7e..fa5d811 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/Lib/test/support/interpreters.py b/Lib/test/support/interpreters.py index 182f47b..ab9342b 100644 --- a/Lib/test/support/interpreters.py +++ b/Lib/test/support/interpreters.py @@ -92,7 +92,7 @@ class Interpreter: 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 ce0d511..001fa88 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() @@ -19,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) { @@ -62,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 *************************************************************/ @@ -113,263 +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; -} - -// 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 = PyObject_Str(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 **************************************************************/ static const char * @@ -489,43 +202,8 @@ exceptions_init(PyObject *mod) } static int -_run_script(PyInterpreterState *interp, - const char *codestr, Py_ssize_t codestrlen, - _sharedns *shared, _sharedexception *sharedexc, int flags) +_run_script(PyObject *ns, const char *codestr, Py_ssize_t codestrlen, int flags) { - int errcode = ERR_NOT_SET; - - 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(); - errcode = ERR_ALREADY_RUNNING; - goto error; - } - - PyObject *excval = NULL; - 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; - } - Py_INCREF(ns); - - // Apply the cross-interpreter data. - if (shared != NULL) { - if (_sharedns_apply(shared, ns) != 0) { - Py_DECREF(ns); - goto error; - } - } - - // Run the script/code/etc. PyObject *result = NULL; if (flags & RUN_TEXT) { result = PyRun_StringFlags(codestr, Py_file_input, ns, ns, NULL); @@ -540,86 +218,46 @@ _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 = no_exception; + Py_DECREF(result); // We throw away the result. return 0; - -error: - excval = PyErr_GetRaisedException(); - const char *failure = _sharedexception_bind(excval, errcode, sharedexc); - if (failure != NULL) { - fprintf(stderr, - "RunFailedError: script raised an uncaught exception (%s)", - failure); - } - 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); - } - if (errcode != ERR_ALREADY_RUNNING) { - _PyInterpreterState_SetNotRunningMain(interp); - } - assert(!PyErr_Occurred()); - return -1; } 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) { - module_state *state = get_module_state(mod); - assert(state != NULL); + assert(!PyErr_Occurred()); + _PyXI_session session = {0}; - _sharedns *shared = _get_shared_ns(shareables); - if (shared == NULL && PyErr_Occurred()) { + // Prep and switch interpreters. + if (_PyXI_Enter(&session, interp, shareables) < 0) { + assert(!PyErr_Occurred()); + _PyXI_ApplyExceptionInfo(session.exc, excwrapper); + assert(PyErr_Occurred()); return -1; } - // 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); - } - // Run the script. - _sharedexception exc = (_sharedexception){ .interp = interp }; - int result = _run_script(interp, codestr, codestrlen, shared, &exc, flags); + int res = _run_script(session.main_ns, codestr, codestrlen, flags); - // Switch back. - if (save_tstate != NULL) { - PyThreadState_Clear(tstate); - PyThreadState_Swap(save_tstate); - PyThreadState_Delete(tstate); - } + // Clean up and switch back. + _PyXI_Exit(&session); // Propagate any exception out to the caller. - if (result < 0) { - assert(!PyErr_Occurred()); - _sharedexception_apply(&exc, state->RunFailedError); - assert(PyErr_Occurred()); + assert(!PyErr_Occurred()); + if (res < 0) { + _PyXI_ApplyCapturedException(&session, excwrapper); } - - if (shared != NULL) { - _sharedns_free(shared); + else { + assert(!_PyXI_HasCapturedException(&session)); } - return result; + return res; } @@ -805,7 +443,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) @@ -903,10 +540,12 @@ _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) { + if (res < 0) { return -1; } @@ -981,7 +620,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; diff --git a/Python/crossinterp.c b/Python/crossinterp.c index 17c476b..00eccbd 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() @@ -64,6 +65,38 @@ _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); +} + +static PyObject * +_get_not_shareable_error_type(PyInterpreterState *interp) +{ + assert(interp->xi.PyExc_NotShareableError != NULL); + return interp->xi.PyExc_NotShareableError; +} + + /* defining cross-interpreter data */ static inline void @@ -171,25 +204,54 @@ _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) +{ + /* 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, const char *msg) { - crossinterpdatafunc getdata = _PyCrossInterpreterData_Lookup(obj); - if (getdata == NULL && PyErr_Occurred() == 0) - PyErr_Format(PyExc_ValueError, + PyObject *exctype = _get_not_shareable_error_type(interp); + assert(exctype != 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"); + } + 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, NULL); + } return -1; } return 0; @@ -211,9 +273,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, NULL); + } return -1; } int res = getdata(tstate, obj, data); @@ -300,6 +365,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) @@ -351,9 +438,8 @@ _xidregistry_remove_entry(struct _xidregistry *xidregistry, return next; } -// This is used in pystate.c (for now). -void -_Py_xidregistry_clear(struct _xidregistry *xidregistry) +static void +_xidregistry_clear(struct _xidregistry *xidregistry) { struct _xidregitem *cur = xidregistry->head; xidregistry->head = NULL; @@ -365,6 +451,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) { @@ -391,30 +493,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->xidregistry; - if (cls->tp_flags & Py_TPFLAGS_HEAPTYPE) { - assert(interp->xidregistry.mutex == xidregistry->mutex); - xidregistry = &interp->xidregistry; - } - 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->xidregistry) { - assert(xidregistry == &interp->runtime->xidregistry); - if (xidregistry->head == NULL) { - _register_builtins_for_crossinterpreter_data(xidregistry); - } - } -} - int _PyCrossInterpreterData_RegisterClass(PyTypeObject *cls, crossinterpdatafunc getdata) @@ -430,10 +508,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); - - _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) { @@ -445,7 +521,7 @@ _PyCrossInterpreterData_RegisterClass(PyTypeObject *cls, res = _xidregistry_add_type(xidregistry, cls, getdata); finally: - PyThread_release_lock(xidregistry->mutex); + _xidregistry_unlock(xidregistry); return res; } @@ -454,8 +530,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) { @@ -467,30 +543,22 @@ _PyCrossInterpreterData_UnregisterClass(PyTypeObject *cls) res = 1; } - PyThread_release_lock(xidregistry->mutex); + _xidregistry_unlock(xidregistry); 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); - - _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; } @@ -653,3 +721,811 @@ _register_builtins_for_crossinterpreter_data(struct _xidregistry *xidregistry) Py_FatalError("could not register float for cross-interpreter sharing"); } } + +/* 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 */ +/*************************/ + +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, int rawfree) +{ + PyObject *exc = PyErr_GetRaisedException(); + int res = rawfree + ? _PyCrossInterpreterData_Release(data) + : _PyCrossInterpreterData_ReleaseAndRawFree(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 */ +/***************************/ + +/* error codes */ + +static 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; + 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; + default: +#ifdef Py_DEBUG + Py_UNREACHABLE(); +#else + PyErr_Format(PyExc_RuntimeError, "unsupported error code %d", code); +#endif + } + assert(PyErr_Occurred()); + return -1; +} + +/* shared exceptions */ + +static 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 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); + (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()); +} + +/* shared namespaces */ + +typedef struct _sharednsitem { + int64_t interpid; + const char *name; + _PyCrossInterpreterData *data; + _PyCrossInterpreterData _data; +} _PyXI_namespace_item; + +static void _sharednsitem_clear(_PyXI_namespace_item *); // forward + +static int +_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->data = NULL; + return 0; +} + +static int +_sharednsitem_set_value(_PyXI_namespace_item *item, PyObject *value) +{ + assert(item->name != NULL); + 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; + // The caller may want to propagate PyExc_NotShareableError + // if currently switched between interpreters. + return -1; + } + return 0; +} + +static void +_sharednsitem_clear_data(_PyXI_namespace_item *item) +{ + _PyCrossInterpreterData *data = item->data; + if (data != NULL) { + item->data = NULL; + int rawfree = (data == &item->_data); + (void)_release_xid_data(data, rawfree); + } +} + +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) +{ + PyObject *name = PyUnicode_FromString(item->name); + if (name == NULL) { + return -1; + } + PyObject *value; + if (item->data != NULL) { + 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); + Py_DECREF(value); + return res; +} + +struct _sharedns { + PyInterpreterState *interp; + 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; +} + +static void +_free_xi_namespace(_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); +} + +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_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; + } + 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, interpid, key); + Py_DECREF(key); + if (res < 0) { + break; + } + } + if (PyErr_Occurred()) { + _PyXI_FreeNamespace(ns); + return NULL; + } + return ns; +} + +static void _propagate_not_shareable_error(_PyXI_session *); + +// All items are expected to be shareable. +static _PyXI_namespace * +_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; + } + 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; + } + + _PyXI_namespace *ns = _sharedns_new(len); + if (ns == NULL) { + 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++) { + PyObject *key, *value; + if (!PyDict_Next(nsobj, &pos, &key, &value)) { + goto error; + } + _PyXI_namespace_item *item = &ns->items[i]; + if (_sharednsitem_init(item, interpid, key) != 0) { + goto error; + } + if (_sharednsitem_set_value(item, value) < 0) { + _sharednsitem_clear(item); + _propagate_not_shareable_error(session); + goto error; + } + } + 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_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]); + } + return -1; + } + } + return 0; +} + +int +_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, dflt) != 0) { + return -1; + } + } + 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 +_propagate_not_shareable_error(_PyXI_session *session) +{ + if (session == NULL) { + return; + } + PyInterpreterState *interp = _PyInterpreterState_GET(); + 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; + } +} + +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 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 capture the exception and save + // the errcode. + excval = PyErr_GetRaisedException(); + } + + // Capture the exception. + _PyXI_exception_info *exc = &session->_exc; + *exc = (_PyXI_exception_info){ + .interp = session->init_tstate->interp, + }; + 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) { + // 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(_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, NULL); + 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) { + 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); + + // 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()); + // 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) { + _PyXI_FreeNamespace(sharedns); + } + return -1; +} + +void +_PyXI_Exit(_PyXI_session *session) +{ + _capture_current_exception(session); + _exit_session(session); +} + + +/*********************/ +/* runtime lifecycle */ +/*********************/ + +PyStatus +_PyXI_Init(PyInterpreterState *interp) +{ + PyStatus status; + + // 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); + if (_PyStatus_EXCEPTION(status)) { + return status; + } + + 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); + + // Finalize the XID registry. + _xidregistry_fini(_get_xidregistry(interp)); + if (_Py_IsMainInterpreter(interp)) { + _xidregistry_fini(_get_global_xidregistry(interp->runtime)); + } +} diff --git a/Python/errors.c b/Python/errors.c index f75c3e1..30be7fa 100644 --- a/Python/errors.c +++ b/Python/errors.c @@ -1945,3 +1945,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 = PyObject_Str(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; + } +} diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 3c57056..ea84ca0 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(); } @@ -854,6 +855,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); @@ -1772,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); diff --git a/Python/pystate.c b/Python/pystate.c index d97a03c..8970e17 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, \ @@ -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->xidregistry); - 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->xidregistry.mutex = runtime->xidregistry.mutex; PyMem_SetAllocator(PYMEM_DOMAIN_RAW, &old_alloc); @@ -720,9 +710,6 @@ init_interpreter(PyInterpreterState *interp, } interp->f_opcode_trace_set = false; - assert(runtime->xidregistry.mutex != NULL); - interp->xidregistry.mutex = runtime->xidregistry.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->xidregistry); - /* The lock is owned by the runtime, so we don't free it here. */ - interp->xidregistry.mutex = NULL; - if (tstate->interp == interp) { /* We are now safe to fix tstate->_status.cleared. */ // XXX Do this (much) earlier? |