mirror of https://github.com/python/cpython
595 lines
16 KiB
C
595 lines
16 KiB
C
|
|
||
|
static crossinterpdatafunc _lookup_getdata_from_registry(
|
||
|
PyInterpreterState *, PyObject *);
|
||
|
|
||
|
static crossinterpdatafunc
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
|
||
|
/***********************************************/
|
||
|
/* a registry of {type -> crossinterpdatafunc} */
|
||
|
/***********************************************/
|
||
|
|
||
|
/* For now we use a global registry of shareable classes. An
|
||
|
alternative would be to add a tp_* slot for a class's
|
||
|
crossinterpdatafunc. It would be simpler and more efficient. */
|
||
|
|
||
|
|
||
|
/* registry lifecycle */
|
||
|
|
||
|
static void _register_builtins_for_crossinterpreter_data(struct _xidregistry *);
|
||
|
|
||
|
static void
|
||
|
_xidregistry_init(struct _xidregistry *registry)
|
||
|
{
|
||
|
if (registry->initialized) {
|
||
|
return;
|
||
|
}
|
||
|
registry->initialized = 1;
|
||
|
|
||
|
if (registry->global) {
|
||
|
// Registering the builtins is cheap so we don't bother doing it lazily.
|
||
|
assert(registry->head == NULL);
|
||
|
_register_builtins_for_crossinterpreter_data(registry);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void _xidregistry_clear(struct _xidregistry *);
|
||
|
|
||
|
static void
|
||
|
_xidregistry_fini(struct _xidregistry *registry)
|
||
|
{
|
||
|
if (!registry->initialized) {
|
||
|
return;
|
||
|
}
|
||
|
registry->initialized = 0;
|
||
|
|
||
|
_xidregistry_clear(registry);
|
||
|
}
|
||
|
|
||
|
static inline struct _xidregistry * _get_global_xidregistry(_PyRuntimeState *);
|
||
|
static inline struct _xidregistry * _get_xidregistry(PyInterpreterState *);
|
||
|
|
||
|
static void
|
||
|
xid_lookup_init(PyInterpreterState *interp)
|
||
|
{
|
||
|
if (_Py_IsMainInterpreter(interp)) {
|
||
|
_xidregistry_init(_get_global_xidregistry(interp->runtime));
|
||
|
}
|
||
|
_xidregistry_init(_get_xidregistry(interp));
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
xid_lookup_fini(PyInterpreterState *interp)
|
||
|
{
|
||
|
_xidregistry_fini(_get_xidregistry(interp));
|
||
|
if (_Py_IsMainInterpreter(interp)) {
|
||
|
_xidregistry_fini(_get_global_xidregistry(interp->runtime));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/* registry thread safety */
|
||
|
|
||
|
static void
|
||
|
_xidregistry_lock(struct _xidregistry *registry)
|
||
|
{
|
||
|
if (registry->global) {
|
||
|
PyMutex_Lock(®istry->mutex);
|
||
|
}
|
||
|
// else: Within an interpreter we rely on the GIL instead of a separate lock.
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
_xidregistry_unlock(struct _xidregistry *registry)
|
||
|
{
|
||
|
if (registry->global) {
|
||
|
PyMutex_Unlock(®istry->mutex);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/* accessing the registry */
|
||
|
|
||
|
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 struct _xidregitem * _xidregistry_remove_entry(
|
||
|
struct _xidregistry *, struct _xidregitem *);
|
||
|
|
||
|
static struct _xidregitem *
|
||
|
_xidregistry_find_type(struct _xidregistry *xidregistry, PyTypeObject *cls)
|
||
|
{
|
||
|
struct _xidregitem *cur = xidregistry->head;
|
||
|
while (cur != NULL) {
|
||
|
if (cur->weakref != NULL) {
|
||
|
// cur is/was a heap type.
|
||
|
PyObject *registered = _PyWeakref_GET_REF(cur->weakref);
|
||
|
if (registered == NULL) {
|
||
|
// The weakly ref'ed object was freed.
|
||
|
cur = _xidregistry_remove_entry(xidregistry, cur);
|
||
|
continue;
|
||
|
}
|
||
|
assert(PyType_Check(registered));
|
||
|
assert(cur->cls == (PyTypeObject *)registered);
|
||
|
assert(cur->cls->tp_flags & Py_TPFLAGS_HEAPTYPE);
|
||
|
Py_DECREF(registered);
|
||
|
}
|
||
|
if (cur->cls == cls) {
|
||
|
return cur;
|
||
|
}
|
||
|
cur = cur->next;
|
||
|
}
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
static crossinterpdatafunc
|
||
|
_lookup_getdata_from_registry(PyInterpreterState *interp, PyObject *obj)
|
||
|
{
|
||
|
PyTypeObject *cls = Py_TYPE(obj);
|
||
|
|
||
|
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;
|
||
|
|
||
|
_xidregistry_unlock(xidregistry);
|
||
|
return func;
|
||
|
}
|
||
|
|
||
|
|
||
|
/* updating the registry */
|
||
|
|
||
|
static int
|
||
|
_xidregistry_add_type(struct _xidregistry *xidregistry,
|
||
|
PyTypeObject *cls, crossinterpdatafunc getdata)
|
||
|
{
|
||
|
struct _xidregitem *newhead = PyMem_RawMalloc(sizeof(struct _xidregitem));
|
||
|
if (newhead == NULL) {
|
||
|
return -1;
|
||
|
}
|
||
|
*newhead = (struct _xidregitem){
|
||
|
// We do not keep a reference, to avoid keeping the class alive.
|
||
|
.cls = cls,
|
||
|
.refcount = 1,
|
||
|
.getdata = getdata,
|
||
|
};
|
||
|
if (cls->tp_flags & Py_TPFLAGS_HEAPTYPE) {
|
||
|
// XXX Assign a callback to clear the entry from the registry?
|
||
|
newhead->weakref = PyWeakref_NewRef((PyObject *)cls, NULL);
|
||
|
if (newhead->weakref == NULL) {
|
||
|
PyMem_RawFree(newhead);
|
||
|
return -1;
|
||
|
}
|
||
|
}
|
||
|
newhead->next = xidregistry->head;
|
||
|
if (newhead->next != NULL) {
|
||
|
newhead->next->prev = newhead;
|
||
|
}
|
||
|
xidregistry->head = newhead;
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static struct _xidregitem *
|
||
|
_xidregistry_remove_entry(struct _xidregistry *xidregistry,
|
||
|
struct _xidregitem *entry)
|
||
|
{
|
||
|
struct _xidregitem *next = entry->next;
|
||
|
if (entry->prev != NULL) {
|
||
|
assert(entry->prev->next == entry);
|
||
|
entry->prev->next = next;
|
||
|
}
|
||
|
else {
|
||
|
assert(xidregistry->head == entry);
|
||
|
xidregistry->head = next;
|
||
|
}
|
||
|
if (next != NULL) {
|
||
|
next->prev = entry->prev;
|
||
|
}
|
||
|
Py_XDECREF(entry->weakref);
|
||
|
PyMem_RawFree(entry);
|
||
|
return next;
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
_xidregistry_clear(struct _xidregistry *xidregistry)
|
||
|
{
|
||
|
struct _xidregitem *cur = xidregistry->head;
|
||
|
xidregistry->head = NULL;
|
||
|
while (cur != NULL) {
|
||
|
struct _xidregitem *next = cur->next;
|
||
|
Py_XDECREF(cur->weakref);
|
||
|
PyMem_RawFree(cur);
|
||
|
cur = next;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
int
|
||
|
_PyCrossInterpreterData_RegisterClass(PyTypeObject *cls,
|
||
|
crossinterpdatafunc getdata)
|
||
|
{
|
||
|
if (!PyType_Check(cls)) {
|
||
|
PyErr_Format(PyExc_ValueError, "only classes may be registered");
|
||
|
return -1;
|
||
|
}
|
||
|
if (getdata == NULL) {
|
||
|
PyErr_Format(PyExc_ValueError, "missing 'getdata' func");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
int res = 0;
|
||
|
PyInterpreterState *interp = _PyInterpreterState_GET();
|
||
|
struct _xidregistry *xidregistry = _get_xidregistry_for_type(interp, cls);
|
||
|
_xidregistry_lock(xidregistry);
|
||
|
|
||
|
struct _xidregitem *matched = _xidregistry_find_type(xidregistry, cls);
|
||
|
if (matched != NULL) {
|
||
|
assert(matched->getdata == getdata);
|
||
|
matched->refcount += 1;
|
||
|
goto finally;
|
||
|
}
|
||
|
|
||
|
res = _xidregistry_add_type(xidregistry, cls, getdata);
|
||
|
|
||
|
finally:
|
||
|
_xidregistry_unlock(xidregistry);
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
int
|
||
|
_PyCrossInterpreterData_UnregisterClass(PyTypeObject *cls)
|
||
|
{
|
||
|
int res = 0;
|
||
|
PyInterpreterState *interp = _PyInterpreterState_GET();
|
||
|
struct _xidregistry *xidregistry = _get_xidregistry_for_type(interp, cls);
|
||
|
_xidregistry_lock(xidregistry);
|
||
|
|
||
|
struct _xidregitem *matched = _xidregistry_find_type(xidregistry, cls);
|
||
|
if (matched != NULL) {
|
||
|
assert(matched->refcount > 0);
|
||
|
matched->refcount -= 1;
|
||
|
if (matched->refcount == 0) {
|
||
|
(void)_xidregistry_remove_entry(xidregistry, matched);
|
||
|
}
|
||
|
res = 1;
|
||
|
}
|
||
|
|
||
|
_xidregistry_unlock(xidregistry);
|
||
|
return res;
|
||
|
}
|
||
|
|
||
|
|
||
|
/********************************************/
|
||
|
/* cross-interpreter data for builtin types */
|
||
|
/********************************************/
|
||
|
|
||
|
// bytes
|
||
|
|
||
|
struct _shared_bytes_data {
|
||
|
char *bytes;
|
||
|
Py_ssize_t len;
|
||
|
};
|
||
|
|
||
|
static PyObject *
|
||
|
_new_bytes_object(_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
struct _shared_bytes_data *shared = (struct _shared_bytes_data *)(data->data);
|
||
|
return PyBytes_FromStringAndSize(shared->bytes, shared->len);
|
||
|
}
|
||
|
|
||
|
static int
|
||
|
_bytes_shared(PyThreadState *tstate, PyObject *obj,
|
||
|
_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
if (_PyCrossInterpreterData_InitWithSize(
|
||
|
data, tstate->interp, sizeof(struct _shared_bytes_data), obj,
|
||
|
_new_bytes_object
|
||
|
) < 0)
|
||
|
{
|
||
|
return -1;
|
||
|
}
|
||
|
struct _shared_bytes_data *shared = (struct _shared_bytes_data *)data->data;
|
||
|
if (PyBytes_AsStringAndSize(obj, &shared->bytes, &shared->len) < 0) {
|
||
|
_PyCrossInterpreterData_Clear(tstate->interp, data);
|
||
|
return -1;
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// str
|
||
|
|
||
|
struct _shared_str_data {
|
||
|
int kind;
|
||
|
const void *buffer;
|
||
|
Py_ssize_t len;
|
||
|
};
|
||
|
|
||
|
static PyObject *
|
||
|
_new_str_object(_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
struct _shared_str_data *shared = (struct _shared_str_data *)(data->data);
|
||
|
return PyUnicode_FromKindAndData(shared->kind, shared->buffer, shared->len);
|
||
|
}
|
||
|
|
||
|
static int
|
||
|
_str_shared(PyThreadState *tstate, PyObject *obj,
|
||
|
_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
if (_PyCrossInterpreterData_InitWithSize(
|
||
|
data, tstate->interp, sizeof(struct _shared_str_data), obj,
|
||
|
_new_str_object
|
||
|
) < 0)
|
||
|
{
|
||
|
return -1;
|
||
|
}
|
||
|
struct _shared_str_data *shared = (struct _shared_str_data *)data->data;
|
||
|
shared->kind = PyUnicode_KIND(obj);
|
||
|
shared->buffer = PyUnicode_DATA(obj);
|
||
|
shared->len = PyUnicode_GET_LENGTH(obj);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// int
|
||
|
|
||
|
static PyObject *
|
||
|
_new_long_object(_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
return PyLong_FromSsize_t((Py_ssize_t)(data->data));
|
||
|
}
|
||
|
|
||
|
static int
|
||
|
_long_shared(PyThreadState *tstate, PyObject *obj,
|
||
|
_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
/* Note that this means the size of shareable ints is bounded by
|
||
|
* sys.maxsize. Hence on 32-bit architectures that is half the
|
||
|
* size of maximum shareable ints on 64-bit.
|
||
|
*/
|
||
|
Py_ssize_t value = PyLong_AsSsize_t(obj);
|
||
|
if (value == -1 && PyErr_Occurred()) {
|
||
|
if (PyErr_ExceptionMatches(PyExc_OverflowError)) {
|
||
|
PyErr_SetString(PyExc_OverflowError, "try sending as bytes");
|
||
|
}
|
||
|
return -1;
|
||
|
}
|
||
|
_PyCrossInterpreterData_Init(data, tstate->interp, (void *)value, NULL,
|
||
|
_new_long_object);
|
||
|
// data->obj and data->free remain NULL
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// float
|
||
|
|
||
|
static PyObject *
|
||
|
_new_float_object(_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
double * value_ptr = data->data;
|
||
|
return PyFloat_FromDouble(*value_ptr);
|
||
|
}
|
||
|
|
||
|
static int
|
||
|
_float_shared(PyThreadState *tstate, PyObject *obj,
|
||
|
_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
if (_PyCrossInterpreterData_InitWithSize(
|
||
|
data, tstate->interp, sizeof(double), NULL,
|
||
|
_new_float_object
|
||
|
) < 0)
|
||
|
{
|
||
|
return -1;
|
||
|
}
|
||
|
double *shared = (double *)data->data;
|
||
|
*shared = PyFloat_AsDouble(obj);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// None
|
||
|
|
||
|
static PyObject *
|
||
|
_new_none_object(_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
// XXX Singleton refcounts are problematic across interpreters...
|
||
|
return Py_NewRef(Py_None);
|
||
|
}
|
||
|
|
||
|
static int
|
||
|
_none_shared(PyThreadState *tstate, PyObject *obj,
|
||
|
_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
_PyCrossInterpreterData_Init(data, tstate->interp, NULL, NULL,
|
||
|
_new_none_object);
|
||
|
// data->data, data->obj and data->free remain NULL
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// bool
|
||
|
|
||
|
static PyObject *
|
||
|
_new_bool_object(_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
if (data->data){
|
||
|
Py_RETURN_TRUE;
|
||
|
}
|
||
|
Py_RETURN_FALSE;
|
||
|
}
|
||
|
|
||
|
static int
|
||
|
_bool_shared(PyThreadState *tstate, PyObject *obj,
|
||
|
_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
_PyCrossInterpreterData_Init(data, tstate->interp,
|
||
|
(void *) (Py_IsTrue(obj) ? (uintptr_t) 1 : (uintptr_t) 0), NULL,
|
||
|
_new_bool_object);
|
||
|
// data->obj and data->free remain NULL
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// tuple
|
||
|
|
||
|
struct _shared_tuple_data {
|
||
|
Py_ssize_t len;
|
||
|
_PyCrossInterpreterData **data;
|
||
|
};
|
||
|
|
||
|
static PyObject *
|
||
|
_new_tuple_object(_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
struct _shared_tuple_data *shared = (struct _shared_tuple_data *)(data->data);
|
||
|
PyObject *tuple = PyTuple_New(shared->len);
|
||
|
if (tuple == NULL) {
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
for (Py_ssize_t i = 0; i < shared->len; i++) {
|
||
|
PyObject *item = _PyCrossInterpreterData_NewObject(shared->data[i]);
|
||
|
if (item == NULL){
|
||
|
Py_DECREF(tuple);
|
||
|
return NULL;
|
||
|
}
|
||
|
PyTuple_SET_ITEM(tuple, i, item);
|
||
|
}
|
||
|
return tuple;
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
_tuple_shared_free(void* data)
|
||
|
{
|
||
|
struct _shared_tuple_data *shared = (struct _shared_tuple_data *)(data);
|
||
|
#ifndef NDEBUG
|
||
|
int64_t interpid = PyInterpreterState_GetID(_PyInterpreterState_GET());
|
||
|
#endif
|
||
|
for (Py_ssize_t i = 0; i < shared->len; i++) {
|
||
|
if (shared->data[i] != NULL) {
|
||
|
assert(_PyCrossInterpreterData_INTERPID(shared->data[i]) == interpid);
|
||
|
_PyCrossInterpreterData_Release(shared->data[i]);
|
||
|
PyMem_RawFree(shared->data[i]);
|
||
|
shared->data[i] = NULL;
|
||
|
}
|
||
|
}
|
||
|
PyMem_Free(shared->data);
|
||
|
PyMem_RawFree(shared);
|
||
|
}
|
||
|
|
||
|
static int
|
||
|
_tuple_shared(PyThreadState *tstate, PyObject *obj,
|
||
|
_PyCrossInterpreterData *data)
|
||
|
{
|
||
|
Py_ssize_t len = PyTuple_GET_SIZE(obj);
|
||
|
if (len < 0) {
|
||
|
return -1;
|
||
|
}
|
||
|
struct _shared_tuple_data *shared = PyMem_RawMalloc(sizeof(struct _shared_tuple_data));
|
||
|
if (shared == NULL){
|
||
|
PyErr_NoMemory();
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
shared->len = len;
|
||
|
shared->data = (_PyCrossInterpreterData **) PyMem_Calloc(shared->len, sizeof(_PyCrossInterpreterData *));
|
||
|
if (shared->data == NULL) {
|
||
|
PyErr_NoMemory();
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
for (Py_ssize_t i = 0; i < shared->len; i++) {
|
||
|
_PyCrossInterpreterData *data = _PyCrossInterpreterData_New();
|
||
|
if (data == NULL) {
|
||
|
goto error; // PyErr_NoMemory already set
|
||
|
}
|
||
|
PyObject *item = PyTuple_GET_ITEM(obj, i);
|
||
|
|
||
|
int res = -1;
|
||
|
if (!_Py_EnterRecursiveCallTstate(tstate, " while sharing a tuple")) {
|
||
|
res = _PyObject_GetCrossInterpreterData(item, data);
|
||
|
_Py_LeaveRecursiveCallTstate(tstate);
|
||
|
}
|
||
|
if (res < 0) {
|
||
|
PyMem_RawFree(data);
|
||
|
goto error;
|
||
|
}
|
||
|
shared->data[i] = data;
|
||
|
}
|
||
|
_PyCrossInterpreterData_Init(
|
||
|
data, tstate->interp, shared, obj, _new_tuple_object);
|
||
|
data->free = _tuple_shared_free;
|
||
|
return 0;
|
||
|
|
||
|
error:
|
||
|
_tuple_shared_free(shared);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
// registration
|
||
|
|
||
|
static void
|
||
|
_register_builtins_for_crossinterpreter_data(struct _xidregistry *xidregistry)
|
||
|
{
|
||
|
// None
|
||
|
if (_xidregistry_add_type(xidregistry, (PyTypeObject *)PyObject_Type(Py_None), _none_shared) != 0) {
|
||
|
Py_FatalError("could not register None for cross-interpreter sharing");
|
||
|
}
|
||
|
|
||
|
// int
|
||
|
if (_xidregistry_add_type(xidregistry, &PyLong_Type, _long_shared) != 0) {
|
||
|
Py_FatalError("could not register int for cross-interpreter sharing");
|
||
|
}
|
||
|
|
||
|
// bytes
|
||
|
if (_xidregistry_add_type(xidregistry, &PyBytes_Type, _bytes_shared) != 0) {
|
||
|
Py_FatalError("could not register bytes for cross-interpreter sharing");
|
||
|
}
|
||
|
|
||
|
// str
|
||
|
if (_xidregistry_add_type(xidregistry, &PyUnicode_Type, _str_shared) != 0) {
|
||
|
Py_FatalError("could not register str for cross-interpreter sharing");
|
||
|
}
|
||
|
|
||
|
// bool
|
||
|
if (_xidregistry_add_type(xidregistry, &PyBool_Type, _bool_shared) != 0) {
|
||
|
Py_FatalError("could not register bool for cross-interpreter sharing");
|
||
|
}
|
||
|
|
||
|
// float
|
||
|
if (_xidregistry_add_type(xidregistry, &PyFloat_Type, _float_shared) != 0) {
|
||
|
Py_FatalError("could not register float for cross-interpreter sharing");
|
||
|
}
|
||
|
|
||
|
// tuple
|
||
|
if (_xidregistry_add_type(xidregistry, &PyTuple_Type, _tuple_shared) != 0) {
|
||
|
Py_FatalError("could not register tuple for cross-interpreter sharing");
|
||
|
}
|
||
|
}
|