Skip to content

Commit

Permalink
pythongh-76785: Crossinterp utils additions (pythongh-111530)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ericsnowcurrently authored and FullteaR committed Nov 3, 2023
1 parent 4db128c commit d8c683d
Show file tree
Hide file tree
Showing 11 changed files with 1,308 additions and 471 deletions.
128 changes: 128 additions & 0 deletions Include/internal/pycore_crossinterp.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

#include "pycore_pyerrors.h"


/***************************/
/* cross-interpreter calls */
Expand Down Expand Up @@ -124,6 +126,8 @@ struct _xidregitem {
};

struct _xidregistry {
int global; /* builtin types or heap types */
int initialized;
PyThread_type_lock mutex;
struct _xidregitem *head;
};
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions Include/internal/pycore_pyerrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions Include/internal/pycore_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_runtime_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/support/interpreters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading

0 comments on commit d8c683d

Please sign in to comment.