summaryrefslogtreecommitdiffstats
path: root/Python/crossinterp.c
diff options
context:
space:
mode:
authorEric Snow <ericsnowcurrently@gmail.com>2023-11-01 23:36:40 (GMT)
committerGitHub <noreply@github.com>2023-11-01 23:36:40 (GMT)
commit9322ce90ac8f4d4647a59bbfab48fad6f4e4e856 (patch)
treec3dd4e202f02a639bca0412b71d1464fd4f624c5 /Python/crossinterp.c
parentcde1071b2a72e8261ca66053ef61431b7f3a81fd (diff)
downloadcpython-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.
Diffstat (limited to 'Python/crossinterp.c')
-rw-r--r--Python/crossinterp.c992
1 files changed, 934 insertions, 58 deletions
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));
+ }
+}