Skip to content

Commit 9322ce9

Browse files
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.
1 parent cde1071 commit 9322ce9

11 files changed

+1308
-471
lines changed

Include/internal/pycore_crossinterp.h

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ extern "C" {
88
# error "this header requires Py_BUILD_CORE define"
99
#endif
1010

11+
#include "pycore_pyerrors.h"
12+
1113

1214
/***************************/
1315
/* cross-interpreter calls */
@@ -124,6 +126,8 @@ struct _xidregitem {
124126
};
125127

126128
struct _xidregistry {
129+
int global; /* builtin types or heap types */
130+
int initialized;
127131
PyThread_type_lock mutex;
128132
struct _xidregitem *head;
129133
};
@@ -133,6 +137,130 @@ PyAPI_FUNC(int) _PyCrossInterpreterData_UnregisterClass(PyTypeObject *);
133137
PyAPI_FUNC(crossinterpdatafunc) _PyCrossInterpreterData_Lookup(PyObject *);
134138

135139

140+
/*****************************/
141+
/* runtime state & lifecycle */
142+
/*****************************/
143+
144+
struct _xi_runtime_state {
145+
// builtin types
146+
// XXX Remove this field once we have a tp_* slot.
147+
struct _xidregistry registry;
148+
};
149+
150+
struct _xi_state {
151+
// heap types
152+
// XXX Remove this field once we have a tp_* slot.
153+
struct _xidregistry registry;
154+
155+
// heap types
156+
PyObject *PyExc_NotShareableError;
157+
};
158+
159+
extern PyStatus _PyXI_Init(PyInterpreterState *interp);
160+
extern void _PyXI_Fini(PyInterpreterState *interp);
161+
162+
163+
/***************************/
164+
/* short-term data sharing */
165+
/***************************/
166+
167+
typedef enum error_code {
168+
_PyXI_ERR_NO_ERROR = 0,
169+
_PyXI_ERR_UNCAUGHT_EXCEPTION = -1,
170+
_PyXI_ERR_OTHER = -2,
171+
_PyXI_ERR_NO_MEMORY = -3,
172+
_PyXI_ERR_ALREADY_RUNNING = -4,
173+
_PyXI_ERR_MAIN_NS_FAILURE = -5,
174+
_PyXI_ERR_APPLY_NS_FAILURE = -6,
175+
_PyXI_ERR_NOT_SHAREABLE = -7,
176+
} _PyXI_errcode;
177+
178+
179+
typedef struct _sharedexception {
180+
// The originating interpreter.
181+
PyInterpreterState *interp;
182+
// The kind of error to propagate.
183+
_PyXI_errcode code;
184+
// The exception information to propagate, if applicable.
185+
// This is populated only for _PyXI_ERR_UNCAUGHT_EXCEPTION.
186+
_Py_excinfo uncaught;
187+
} _PyXI_exception_info;
188+
189+
PyAPI_FUNC(void) _PyXI_ApplyExceptionInfo(
190+
_PyXI_exception_info *info,
191+
PyObject *exctype);
192+
193+
typedef struct xi_session _PyXI_session;
194+
typedef struct _sharedns _PyXI_namespace;
195+
196+
PyAPI_FUNC(void) _PyXI_FreeNamespace(_PyXI_namespace *ns);
197+
PyAPI_FUNC(_PyXI_namespace *) _PyXI_NamespaceFromNames(PyObject *names);
198+
PyAPI_FUNC(int) _PyXI_FillNamespaceFromDict(
199+
_PyXI_namespace *ns,
200+
PyObject *nsobj,
201+
_PyXI_session *session);
202+
PyAPI_FUNC(int) _PyXI_ApplyNamespace(
203+
_PyXI_namespace *ns,
204+
PyObject *nsobj,
205+
PyObject *dflt);
206+
207+
208+
// A cross-interpreter session involves entering an interpreter
209+
// (_PyXI_Enter()), doing some work with it, and finally exiting
210+
// that interpreter (_PyXI_Exit()).
211+
//
212+
// At the boundaries of the session, both entering and exiting,
213+
// data may be exchanged between the previous interpreter and the
214+
// target one in a thread-safe way that does not violate the
215+
// isolation between interpreters. This includes setting objects
216+
// in the target's __main__ module on the way in, and capturing
217+
// uncaught exceptions on the way out.
218+
struct xi_session {
219+
// Once a session has been entered, this is the tstate that was
220+
// current before the session. If it is different from cur_tstate
221+
// then we must have switched interpreters. Either way, this will
222+
// be the current tstate once we exit the session.
223+
PyThreadState *prev_tstate;
224+
// Once a session has been entered, this is the current tstate.
225+
// It must be current when the session exits.
226+
PyThreadState *init_tstate;
227+
// This is true if init_tstate needs cleanup during exit.
228+
int own_init_tstate;
229+
230+
// This is true if, while entering the session, init_thread took
231+
// "ownership" of the interpreter's __main__ module. This means
232+
// it is the only thread that is allowed to run code there.
233+
// (Caveat: for now, users may still run exec() against the
234+
// __main__ module's dict, though that isn't advisable.)
235+
int running;
236+
// This is a cached reference to the __dict__ of the entered
237+
// interpreter's __main__ module. It is looked up when at the
238+
// beginning of the session as a convenience.
239+
PyObject *main_ns;
240+
241+
// This is set if the interpreter is entered and raised an exception
242+
// that needs to be handled in some special way during exit.
243+
_PyXI_errcode *exc_override;
244+
// This is set if exit captured an exception to propagate.
245+
_PyXI_exception_info *exc;
246+
247+
// -- pre-allocated memory --
248+
_PyXI_exception_info _exc;
249+
_PyXI_errcode _exc_override;
250+
};
251+
252+
PyAPI_FUNC(int) _PyXI_Enter(
253+
_PyXI_session *session,
254+
PyInterpreterState *interp,
255+
PyObject *nsupdates);
256+
PyAPI_FUNC(void) _PyXI_Exit(_PyXI_session *session);
257+
258+
PyAPI_FUNC(void) _PyXI_ApplyCapturedException(
259+
_PyXI_session *session,
260+
PyObject *excwrapper);
261+
PyAPI_FUNC(int) _PyXI_HasCapturedException(_PyXI_session *session);
262+
263+
136264
#ifdef __cplusplus
137265
}
138266
#endif

Include/internal/pycore_interp.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ struct _is {
153153
Py_ssize_t co_extra_user_count;
154154
freefunc co_extra_freefuncs[MAX_CO_EXTRA_USERS];
155155

156-
// XXX Remove this field once we have a tp_* slot.
157-
struct _xidregistry xidregistry;
156+
/* cross-interpreter data and utils */
157+
struct _xi_state xi;
158158

159159
#ifdef HAVE_FORK
160160
PyObject *before_forkers;

Include/internal/pycore_pyerrors.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,30 @@ extern PyStatus _PyErr_InitTypes(PyInterpreterState *);
6868
extern void _PyErr_FiniTypes(PyInterpreterState *);
6969

7070

71+
/* exception snapshots */
72+
73+
// Ultimately we'd like to preserve enough information about the
74+
// exception and traceback that we could re-constitute (or at least
75+
// simulate, a la traceback.TracebackException), and even chain, a copy
76+
// of the exception in the calling interpreter.
77+
78+
typedef struct _excinfo {
79+
const char *type;
80+
const char *msg;
81+
} _Py_excinfo;
82+
83+
extern void _Py_excinfo_Clear(_Py_excinfo *info);
84+
extern int _Py_excinfo_Copy(_Py_excinfo *dest, _Py_excinfo *src);
85+
extern const char * _Py_excinfo_InitFromException(
86+
_Py_excinfo *info,
87+
PyObject *exc);
88+
extern void _Py_excinfo_Apply(_Py_excinfo *info, PyObject *exctype);
89+
extern const char * _Py_excinfo_AsUTF8(
90+
_Py_excinfo *info,
91+
char *buf,
92+
size_t bufsize);
93+
94+
7195
/* other API */
7296

7397
static inline PyObject* _PyErr_Occurred(PyThreadState *tstate)

Include/internal/pycore_runtime.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ typedef struct pyruntimestate {
200200
possible to facilitate out-of-process observability
201201
tools. */
202202

203-
// XXX Remove this field once we have a tp_* slot.
204-
struct _xidregistry xidregistry;
203+
/* cross-interpreter data and utils */
204+
struct _xi_runtime_state xi;
205205

206206
struct _pymem_allocators allocators;
207207
struct _obmalloc_global_state obmalloc;

Include/internal/pycore_runtime_init.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ extern PyTypeObject _PyExc_MemoryError;
9595
until _PyInterpreterState_Enable() is called. */ \
9696
.next_id = -1, \
9797
}, \
98+
.xi = { \
99+
.registry = { \
100+
.global = 1, \
101+
}, \
102+
}, \
98103
/* A TSS key must be initialized with Py_tss_NEEDS_INIT \
99104
in accordance with the specification. */ \
100105
.autoTSSkey = Py_tss_NEEDS_INIT, \

Lib/test/support/interpreters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def close(self):
9292
return _interpreters.destroy(self._id)
9393

9494
# XXX Rename "run" to "exec"?
95-
def run(self, src_str, /, *, channels=None):
95+
def run(self, src_str, /, channels=None):
9696
"""Run the given source code in the interpreter.
9797
9898
This is essentially the same as calling the builtin "exec"

0 commit comments

Comments
 (0)