Skip to main content

Python/crossinterp.c

cpython 3.14 @ ab2d84fe1023/Python/crossinterp.c

The sub-interpreter isolation layer introduced by PEP 554. Python objects live in a single interpreter; passing them across interpreter boundaries is unsafe because each interpreter manages its own reference counts, type objects, and GIL. crossinterp.c provides a controlled channel: a sending interpreter serialises an object into a _PyCrossInterpreterData buffer (a plain C struct with no Python pointers), and the receiving interpreter reconstructs a new Python object from that buffer.

The file also contains the execution primitives that run a C callable inside another interpreter's thread state (_Py_CallInInterpreter), the _PyXI_Enter / _PyXI_Exit pair that set up a cross-interpreter execution context, and the isolation-level predicate _PyInterpreterState_RequiresIDRef used by the interpreters module to enforce strict separation.

The companion header files Python/crossinterp_data_lookup.h and Python/crossinterp_exceptions.h handle the type registry and exception serialisation respectively; they are included inline from this file.

Map

LinesSymbolRolegopy
1-150Includes, _PyCrossInterpreterData layoutThe data struct: data, obj, interp, new_object function pointer, free function pointer.pythonrun/crossinterp.go:CrossInterpreterData
151-450_PyObject_CheckCrossInterpreterDataVerifies the object's type is registered as shareable; raises ValueError if not.pythonrun/crossinterp.go:CheckCrossInterpreterData
451-800_PyObject_GetCrossInterpreterData / _PyObject_NewCrossInterpreterDataCalls tp_get_xid (the type's registered extractor), fills the _PyCrossInterpreterData buffer, pins the source interpreter.pythonrun/crossinterp.go:GetCrossInterpreterData
801-1100_PyXIData_Release / _PyXIData_ReleaseAndRawFreeDecrement the source object's refcount and free the buffer; must be called from the source interpreter.pythonrun/crossinterp.go:XIDataRelease
1101-1500_PyInterpreterState_RequiresIDRef / _PyInterpreterID_*Isolation-level predicate and interpreter-ID object type.pythonrun/crossinterp.go:RequiresIDRef
1501-2200_PyXI_Enter / _PyXI_ExitPush and pop the cross-interpreter execution context; set up the target thread state, handle namespace propagation.pythonrun/crossinterp.go:XIEnter / XIExit
2201-3304_Py_CallInInterpreter / _Py_CallInInterpreterAndRawFreeRun a crossinterpdatafunc in the target interpreter's thread state; used by channel.send / channel.recv.pythonrun/crossinterp.go:CallInInterpreter

Reading

_PyObject_GetCrossInterpreterData dispatch (lines 451 to 800)

cpython 3.14 @ ab2d84fe1023/Python/crossinterp.c#L451-800

_PyObject_GetCrossInterpreterData is the main entry point for a sender.

int
_PyObject_GetCrossInterpreterData(PyObject *obj, _PyCrossInterpreterData *data)
{
PyInterpreterState *interp = _PyInterpreterState_GET();

/* Look up the registered extractor for obj's type. */
crossinterpdatafunc getdata = lookup_getdata(interp, obj);
if (getdata == NULL) {
if (!PyErr_Occurred()) {
_PyErr_SetStringWithName(interp,
PyExc_ValueError,
"unsupported cross-interpreter type",
Py_TYPE(obj)->tp_name);
}
return -1;
}
Py_INCREF(obj);
int res = getdata(interp, obj, data);
if (res != 0) {
Py_DECREF(obj);
return -1;
}
_PyCrossInterpreterData_SET_INTERPID(data, interp);
return 0;
}

lookup_getdata searches the interpreter's type registry (interp->xid_lookup) for a crossinterpdatafunc keyed by Py_TYPE(obj). Built-in registrations cover int, float, bytes, str, bool, and None. Extension types can register their own extractor via PyInterpreterState_AddXIData. After the extractor fills data->data with a marshalled copy, the interpreter ID is stamped into the struct so _PyXIData_Release can route the free back to the right interpreter.

_PyCrossInterpreterData layout (lines 1 to 150)

cpython 3.14 @ ab2d84fe1023/Python/crossinterp.c#L1-150

struct _PyCrossInterpreterData {
/* Marshalled data; layout is type-defined. */
void *data;
/* Borrowed reference to the original object (source interp only). */
PyObject *obj;
/* Source interpreter ID. */
int64_t interpid;
/* Reconstruct a Python object in the target interpreter. */
xid_newobjectfunc new_object;
/* Free data->data; called from the source interpreter. */
xid_freefunc free;
};

The struct is plain C: no Python headers, no GC participation. data is a void * whose layout is entirely up to the extractor; for str it is a PyBytes object owned by the source interpreter's allocator, while for int it is a heap-allocated long long. The new_object function pointer is called in the target interpreter to reconstruct a Python object; it receives only the _PyCrossInterpreterData * and returns a new reference. free is called in the source interpreter after the transfer completes, or on error paths to avoid leaking the marshalled copy.

_PyXI_Enter and _PyXI_Exit (lines 1501 to 2200)

cpython 3.14 @ ab2d84fe1023/Python/crossinterp.c#L1501-2200

_PyXI_Enter prepares the target interpreter to receive a cross-interpreter call. It acquires the target interpreter's GIL, creates a fresh thread state for the calling OS thread within the target, and optionally propagates a __main__ namespace snapshot encoded as cross-interpreter data.

int
_PyXI_Enter(_PyXIState *xistate, PyInterpreterState *interp,
PyObject *nsupdates)
{
PyThreadState *tstate = _PyThreadState_NewBound(interp, ...);
if (tstate == NULL) return -1;

PyEval_RestoreThread(tstate); /* acquire interp's GIL */
xistate->tstate = tstate;

if (nsupdates != NULL) {
/* propagate the namespace snapshot */
if (_PyXI_ApplyNamespaceOverrides(xistate, nsupdates) < 0) {
_PyXI_Exit(xistate);
return -1;
}
}
return 0;
}

_PyXI_Exit does the symmetric teardown: saves any result or exception back through cross-interpreter data, releases the target GIL via PyEval_SaveThread, and destroys the temporary thread state. The caller (typically _Py_CallInInterpreter) then re-acquires its own interpreter's GIL to process the result.

The pair is the boundary between the two interpreters: everything inside _PyXI_Enter / _PyXI_Exit runs with the target interpreter's GIL held and its thread state current. Nothing Python-heap-allocated in the source interpreter may be touched inside that window.