diff --git a/Include/internal/pycore_crossinterp.h b/Include/internal/pycore_crossinterp.h index 59e4cd9ece780d..9600dfb9600e60 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 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_pyerrors.h b/Include/internal/pycore_pyerrors.h index 184eb35e52b47b..67ef71c2616541 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 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/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/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 ce0d511bc5aa2d..001fa887847cbd 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 17c476ba4369c4..00eccbdf979504 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 f75c3e1fbd3f6e..30be7faea55a6e 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 3c57056fb81e81..ea84ca0b9c3c2a 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 d97a03caf491c4..8970e17a3c101b 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?
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: