gh-132775: Clean Up Cross-Interpreter Error Handling (gh-135369)

In this refactor we:

* move some code around
* make a couple of typedefs opaque
* decouple errors from session state
* improve tracebacks for propagated exceptions

This change helps simplify several upcoming changes.
This commit is contained in:
Eric Snow 2025-06-13 16:45:21 -06:00 committed by GitHub
parent 6eb6c5dbfb
commit c7f4a80079
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 527 additions and 315 deletions

View file

@ -303,10 +303,10 @@ typedef struct _excinfo {
const char *errdisplay; const char *errdisplay;
} _PyXI_excinfo; } _PyXI_excinfo;
PyAPI_FUNC(int) _PyXI_InitExcInfo(_PyXI_excinfo *info, PyObject *exc); PyAPI_FUNC(_PyXI_excinfo *) _PyXI_NewExcInfo(PyObject *exc);
PyAPI_FUNC(void) _PyXI_FreeExcInfo(_PyXI_excinfo *info);
PyAPI_FUNC(PyObject *) _PyXI_FormatExcInfo(_PyXI_excinfo *info); PyAPI_FUNC(PyObject *) _PyXI_FormatExcInfo(_PyXI_excinfo *info);
PyAPI_FUNC(PyObject *) _PyXI_ExcInfoAsObject(_PyXI_excinfo *info); PyAPI_FUNC(PyObject *) _PyXI_ExcInfoAsObject(_PyXI_excinfo *info);
PyAPI_FUNC(void) _PyXI_ClearExcInfo(_PyXI_excinfo *info);
typedef enum error_code { typedef enum error_code {
@ -322,19 +322,20 @@ typedef enum error_code {
_PyXI_ERR_NOT_SHAREABLE = -9, _PyXI_ERR_NOT_SHAREABLE = -9,
} _PyXI_errcode; } _PyXI_errcode;
typedef struct xi_failure _PyXI_failure;
typedef struct _sharedexception { PyAPI_FUNC(_PyXI_failure *) _PyXI_NewFailure(void);
// The originating interpreter. PyAPI_FUNC(void) _PyXI_FreeFailure(_PyXI_failure *);
PyInterpreterState *interp; PyAPI_FUNC(_PyXI_errcode) _PyXI_GetFailureCode(_PyXI_failure *);
// The kind of error to propagate. PyAPI_FUNC(int) _PyXI_InitFailure(_PyXI_failure *, _PyXI_errcode, PyObject *);
_PyXI_errcode code; PyAPI_FUNC(void) _PyXI_InitFailureUTF8(
// The exception information to propagate, if applicable. _PyXI_failure *,
// This is populated only for some error codes, _PyXI_errcode,
// but always for _PyXI_ERR_UNCAUGHT_EXCEPTION. const char *);
_PyXI_excinfo uncaught;
} _PyXI_error;
PyAPI_FUNC(PyObject *) _PyXI_ApplyError(_PyXI_error *err); PyAPI_FUNC(int) _PyXI_UnwrapNotShareableError(
PyThreadState *,
_PyXI_failure *);
// A cross-interpreter session involves entering an interpreter // A cross-interpreter session involves entering an interpreter
@ -366,19 +367,21 @@ PyAPI_FUNC(int) _PyXI_Enter(
_PyXI_session_result *); _PyXI_session_result *);
PyAPI_FUNC(int) _PyXI_Exit( PyAPI_FUNC(int) _PyXI_Exit(
_PyXI_session *, _PyXI_session *,
_PyXI_errcode, _PyXI_failure *,
_PyXI_session_result *); _PyXI_session_result *);
PyAPI_FUNC(PyObject *) _PyXI_GetMainNamespace( PyAPI_FUNC(PyObject *) _PyXI_GetMainNamespace(
_PyXI_session *, _PyXI_session *,
_PyXI_errcode *); _PyXI_failure *);
PyAPI_FUNC(int) _PyXI_Preserve( PyAPI_FUNC(int) _PyXI_Preserve(
_PyXI_session *, _PyXI_session *,
const char *, const char *,
PyObject *, PyObject *,
_PyXI_errcode *); _PyXI_failure *);
PyAPI_FUNC(PyObject *) _PyXI_GetPreserved(_PyXI_session_result *, const char *); PyAPI_FUNC(PyObject *) _PyXI_GetPreserved(
_PyXI_session_result *,
const char *);
/*************/ /*************/

View file

@ -80,21 +80,11 @@ is_notshareable_raised(PyThreadState *tstate)
} }
static void static void
unwrap_not_shareable(PyThreadState *tstate) unwrap_not_shareable(PyThreadState *tstate, _PyXI_failure *failure)
{ {
if (!is_notshareable_raised(tstate)) { if (_PyXI_UnwrapNotShareableError(tstate, failure) < 0) {
return; _PyErr_Clear(tstate);
} }
PyObject *exc = _PyErr_GetRaisedException(tstate);
PyObject *cause = PyException_GetCause(exc);
if (cause != NULL) {
Py_DECREF(exc);
exc = cause;
}
else {
assert(PyException_GetContext(exc) == NULL);
}
_PyErr_SetRaisedException(tstate, exc);
} }
@ -532,13 +522,30 @@ _interp_call_pack(PyThreadState *tstate, struct interp_call *call,
return 0; return 0;
} }
static void
wrap_notshareable(PyThreadState *tstate, const char *label)
{
if (!is_notshareable_raised(tstate)) {
return;
}
assert(label != NULL && strlen(label) > 0);
PyObject *cause = _PyErr_GetRaisedException(tstate);
_PyXIData_FormatNotShareableError(tstate, "%s not shareable", label);
PyObject *exc = _PyErr_GetRaisedException(tstate);
PyException_SetCause(exc, cause);
_PyErr_SetRaisedException(tstate, exc);
}
static int static int
_interp_call_unpack(struct interp_call *call, _interp_call_unpack(struct interp_call *call,
PyObject **p_func, PyObject **p_args, PyObject **p_kwargs) PyObject **p_func, PyObject **p_args, PyObject **p_kwargs)
{ {
PyThreadState *tstate = PyThreadState_Get();
// Unpack the func. // Unpack the func.
PyObject *func = _PyXIData_NewObject(call->func); PyObject *func = _PyXIData_NewObject(call->func);
if (func == NULL) { if (func == NULL) {
wrap_notshareable(tstate, "func");
return -1; return -1;
} }
// Unpack the args. // Unpack the args.
@ -553,6 +560,7 @@ _interp_call_unpack(struct interp_call *call,
else { else {
args = _PyXIData_NewObject(call->args); args = _PyXIData_NewObject(call->args);
if (args == NULL) { if (args == NULL) {
wrap_notshareable(tstate, "args");
Py_DECREF(func); Py_DECREF(func);
return -1; return -1;
} }
@ -563,6 +571,7 @@ _interp_call_unpack(struct interp_call *call,
if (call->kwargs != NULL) { if (call->kwargs != NULL) {
kwargs = _PyXIData_NewObject(call->kwargs); kwargs = _PyXIData_NewObject(call->kwargs);
if (kwargs == NULL) { if (kwargs == NULL) {
wrap_notshareable(tstate, "kwargs");
Py_DECREF(func); Py_DECREF(func);
Py_DECREF(args); Py_DECREF(args);
return -1; return -1;
@ -577,7 +586,7 @@ _interp_call_unpack(struct interp_call *call,
static int static int
_make_call(struct interp_call *call, _make_call(struct interp_call *call,
PyObject **p_result, _PyXI_errcode *p_errcode) PyObject **p_result, _PyXI_failure *failure)
{ {
assert(call != NULL && call->func != NULL); assert(call != NULL && call->func != NULL);
PyThreadState *tstate = _PyThreadState_GET(); PyThreadState *tstate = _PyThreadState_GET();
@ -588,12 +597,10 @@ _make_call(struct interp_call *call,
assert(func == NULL); assert(func == NULL);
assert(args == NULL); assert(args == NULL);
assert(kwargs == NULL); assert(kwargs == NULL);
*p_errcode = is_notshareable_raised(tstate) _PyXI_InitFailure(failure, _PyXI_ERR_OTHER, NULL);
? _PyXI_ERR_NOT_SHAREABLE unwrap_not_shareable(tstate, failure);
: _PyXI_ERR_OTHER;
return -1; return -1;
} }
*p_errcode = _PyXI_ERR_NO_ERROR;
// Make the call. // Make the call.
PyObject *resobj = PyObject_Call(func, args, kwargs); PyObject *resobj = PyObject_Call(func, args, kwargs);
@ -608,17 +615,17 @@ _make_call(struct interp_call *call,
} }
static int static int
_run_script(_PyXIData_t *script, PyObject *ns, _PyXI_errcode *p_errcode) _run_script(_PyXIData_t *script, PyObject *ns, _PyXI_failure *failure)
{ {
PyObject *code = _PyXIData_NewObject(script); PyObject *code = _PyXIData_NewObject(script);
if (code == NULL) { if (code == NULL) {
*p_errcode = _PyXI_ERR_NOT_SHAREABLE; _PyXI_InitFailure(failure, _PyXI_ERR_NOT_SHAREABLE, NULL);
return -1; return -1;
} }
PyObject *result = PyEval_EvalCode(code, ns, ns); PyObject *result = PyEval_EvalCode(code, ns, ns);
Py_DECREF(code); Py_DECREF(code);
if (result == NULL) { if (result == NULL) {
*p_errcode = _PyXI_ERR_UNCAUGHT_EXCEPTION; _PyXI_InitFailure(failure, _PyXI_ERR_UNCAUGHT_EXCEPTION, NULL);
return -1; return -1;
} }
assert(result == Py_None); assert(result == Py_None);
@ -644,8 +651,14 @@ _run_in_interpreter(PyThreadState *tstate, PyInterpreterState *interp,
PyObject *shareables, struct run_result *runres) PyObject *shareables, struct run_result *runres)
{ {
assert(!_PyErr_Occurred(tstate)); assert(!_PyErr_Occurred(tstate));
int res = -1;
_PyXI_failure *failure = _PyXI_NewFailure();
if (failure == NULL) {
return -1;
}
_PyXI_session *session = _PyXI_NewSession(); _PyXI_session *session = _PyXI_NewSession();
if (session == NULL) { if (session == NULL) {
_PyXI_FreeFailure(failure);
return -1; return -1;
} }
_PyXI_session_result result = {0}; _PyXI_session_result result = {0};
@ -655,43 +668,44 @@ _run_in_interpreter(PyThreadState *tstate, PyInterpreterState *interp,
// If an error occured at this step, it means that interp // If an error occured at this step, it means that interp
// was not prepared and switched. // was not prepared and switched.
_PyXI_FreeSession(session); _PyXI_FreeSession(session);
_PyXI_FreeFailure(failure);
assert(result.excinfo == NULL); assert(result.excinfo == NULL);
return -1; return -1;
} }
// Run in the interpreter. // Run in the interpreter.
int res = -1;
_PyXI_errcode errcode = _PyXI_ERR_NO_ERROR;
if (script != NULL) { if (script != NULL) {
assert(call == NULL); assert(call == NULL);
PyObject *mainns = _PyXI_GetMainNamespace(session, &errcode); PyObject *mainns = _PyXI_GetMainNamespace(session, failure);
if (mainns == NULL) { if (mainns == NULL) {
goto finally; goto finally;
} }
res = _run_script(script, mainns, &errcode); res = _run_script(script, mainns, failure);
} }
else { else {
assert(call != NULL); assert(call != NULL);
PyObject *resobj; PyObject *resobj;
res = _make_call(call, &resobj, &errcode); res = _make_call(call, &resobj, failure);
if (res == 0) { if (res == 0) {
res = _PyXI_Preserve(session, "resobj", resobj, &errcode); res = _PyXI_Preserve(session, "resobj", resobj, failure);
Py_DECREF(resobj); Py_DECREF(resobj);
if (res < 0) { if (res < 0) {
goto finally; goto finally;
} }
} }
} }
int exitres;
finally: finally:
// Clean up and switch back. // Clean up and switch back.
exitres = _PyXI_Exit(session, errcode, &result); (void)res;
int exitres = _PyXI_Exit(session, failure, &result);
assert(res == 0 || exitres != 0); assert(res == 0 || exitres != 0);
_PyXI_FreeSession(session); _PyXI_FreeSession(session);
_PyXI_FreeFailure(failure);
res = exitres; res = exitres;
if (_PyErr_Occurred(tstate)) { if (_PyErr_Occurred(tstate)) {
// It's a directly propagated exception.
assert(res < 0); assert(res < 0);
} }
else if (res < 0) { else if (res < 0) {
@ -1064,7 +1078,7 @@ interp_set___main___attrs(PyObject *self, PyObject *args, PyObject *kwargs)
// Clean up and switch back. // Clean up and switch back.
assert(!PyErr_Occurred()); assert(!PyErr_Occurred());
int res = _PyXI_Exit(session, _PyXI_ERR_NO_ERROR, NULL); int res = _PyXI_Exit(session, NULL, NULL);
_PyXI_FreeSession(session); _PyXI_FreeSession(session);
assert(res == 0); assert(res == 0);
if (res < 0) { if (res < 0) {
@ -1124,7 +1138,7 @@ interp_exec(PyObject *self, PyObject *args, PyObject *kwds)
// global variables. They will be resolved against __main__. // global variables. They will be resolved against __main__.
_PyXIData_t xidata = {0}; _PyXIData_t xidata = {0};
if (_PyCode_GetScriptXIData(tstate, code, &xidata) < 0) { if (_PyCode_GetScriptXIData(tstate, code, &xidata) < 0) {
unwrap_not_shareable(tstate); unwrap_not_shareable(tstate, NULL);
return NULL; return NULL;
} }
@ -1188,7 +1202,7 @@ interp_run_string(PyObject *self, PyObject *args, PyObject *kwds)
_PyXIData_t xidata = {0}; _PyXIData_t xidata = {0};
if (_PyCode_GetScriptXIData(tstate, script, &xidata) < 0) { if (_PyCode_GetScriptXIData(tstate, script, &xidata) < 0) {
unwrap_not_shareable(tstate); unwrap_not_shareable(tstate, NULL);
return NULL; return NULL;
} }
@ -1251,7 +1265,7 @@ interp_run_func(PyObject *self, PyObject *args, PyObject *kwds)
_PyXIData_t xidata = {0}; _PyXIData_t xidata = {0};
if (_PyCode_GetScriptXIData(tstate, code, &xidata) < 0) { if (_PyCode_GetScriptXIData(tstate, code, &xidata) < 0) {
unwrap_not_shareable(tstate); unwrap_not_shareable(tstate, NULL);
return NULL; return NULL;
} }
@ -1542,16 +1556,16 @@ capture_exception(PyObject *self, PyObject *args, PyObject *kwds)
} }
PyObject *captured = NULL; PyObject *captured = NULL;
_PyXI_excinfo info = {0}; _PyXI_excinfo *info = _PyXI_NewExcInfo(exc);
if (_PyXI_InitExcInfo(&info, exc) < 0) { if (info == NULL) {
goto finally; goto finally;
} }
captured = _PyXI_ExcInfoAsObject(&info); captured = _PyXI_ExcInfoAsObject(info);
if (captured == NULL) { if (captured == NULL) {
goto finally; goto finally;
} }
PyObject *formatted = _PyXI_FormatExcInfo(&info); PyObject *formatted = _PyXI_FormatExcInfo(info);
if (formatted == NULL) { if (formatted == NULL) {
Py_CLEAR(captured); Py_CLEAR(captured);
goto finally; goto finally;
@ -1564,7 +1578,7 @@ capture_exception(PyObject *self, PyObject *args, PyObject *kwds)
} }
finally: finally:
_PyXI_ClearExcInfo(&info); _PyXI_FreeExcInfo(info);
if (exc != exc_arg) { if (exc != exc_arg) {
if (PyErr_Occurred()) { if (PyErr_Occurred()) {
PyErr_SetRaisedException(exc); PyErr_SetRaisedException(exc);

File diff suppressed because it is too large Load diff

View file

@ -88,6 +88,33 @@ _PyXIData_FormatNotShareableError(PyThreadState *tstate,
va_end(vargs); va_end(vargs);
} }
int
_PyXI_UnwrapNotShareableError(PyThreadState * tstate, _PyXI_failure *failure)
{
PyObject *exctype = get_notshareableerror_type(tstate);
assert(exctype != NULL);
if (!_PyErr_ExceptionMatches(tstate, exctype)) {
return -1;
}
PyObject *exc = _PyErr_GetRaisedException(tstate);
if (failure != NULL) {
_PyXI_errcode code = _PyXI_ERR_NOT_SHAREABLE;
if (_PyXI_InitFailure(failure, code, exc) < 0) {
return -1;
}
}
PyObject *cause = PyException_GetCause(exc);
if (cause != NULL) {
Py_DECREF(exc);
exc = cause;
}
else {
assert(PyException_GetContext(exc) == NULL);
}
_PyErr_SetRaisedException(tstate, exc);
return 0;
}
_PyXIData_getdata_t _PyXIData_getdata_t
_PyXIData_Lookup(PyThreadState *tstate, PyObject *obj) _PyXIData_Lookup(PyThreadState *tstate, PyObject *obj)

View file

@ -7,13 +7,6 @@ _ensure_current_cause(PyThreadState *tstate, PyObject *cause)
} }
PyObject *exc = _PyErr_GetRaisedException(tstate); PyObject *exc = _PyErr_GetRaisedException(tstate);
assert(exc != NULL); assert(exc != NULL);
PyObject *ctx = PyException_GetContext(exc);
if (ctx == NULL) {
PyException_SetContext(exc, Py_NewRef(cause));
}
else {
Py_DECREF(ctx);
}
assert(PyException_GetCause(exc) == NULL); assert(PyException_GetCause(exc) == NULL);
PyException_SetCause(exc, Py_NewRef(cause)); PyException_SetCause(exc, Py_NewRef(cause));
_PyErr_SetRaisedException(tstate, exc); _PyErr_SetRaisedException(tstate, exc);