From 3bb21fff034fb071124b78eb9b45c0d32783d3f9 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 4 Jan 2025 11:24:55 -0500 Subject: [PATCH 01/24] Lock pointer reads and writes in ctypes. --- Modules/_ctypes/_ctypes.c | 117 +++++++++++++++++++++++++++----------- Modules/_ctypes/ctypes.h | 47 +++++++++++++++ 2 files changed, 132 insertions(+), 32 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index ac520ffaad6c90..4a8eb218fc9554 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -598,7 +598,7 @@ StructUnionType_paramfunc(ctypes_state *st, CDataObject *self) if (ptr == NULL) { return NULL; } - memcpy(ptr, self->b_ptr, self->b_size); + locked_memcpy_from(ptr, self, self->b_size); /* Create a Python object which calls PyMem_Free(ptr) in its deallocator. The object will be destroyed @@ -907,8 +907,7 @@ CDataType_from_buffer_copy_impl(PyObject *type, PyTypeObject *cls, result = generic_pycdata_new(st, (PyTypeObject *)type, NULL, NULL); if (result != NULL) { - memcpy(((CDataObject *)result)->b_ptr, - (char *)buffer->buf + offset, info->size); + locked_memcpy_to((CDataObject *) result, buffer->buf + offset, info->size); } return result; } @@ -1195,7 +1194,7 @@ PyCPointerType_paramfunc(ctypes_state *st, CDataObject *self) parg->tag = 'P'; parg->pffi_type = &ffi_type_pointer; parg->obj = Py_NewRef(self); - parg->value.p = *(void **)self->b_ptr; + parg->value.p = locked_deref(self); return parg; } @@ -1432,7 +1431,7 @@ CharArray_set_raw(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored)) goto fail; } - memcpy(self->b_ptr, ptr, size); + locked_memcpy_to(self, ptr, size); PyBuffer_Release(&view); return 0; @@ -1444,18 +1443,25 @@ CharArray_set_raw(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored)) static PyObject * CharArray_get_raw(CDataObject *self, void *Py_UNUSED(ignored)) { - return PyBytes_FromStringAndSize(self->b_ptr, self->b_size); + LOCK_PTR(self); + // XXX Is it possible that PyBytes_FromStringAndSize is re-entrant? + PyObject *res = PyBytes_FromStringAndSize(self->b_ptr, self->b_size); + UNLOCK_PTR(self); + return res; } static PyObject * CharArray_get_value(CDataObject *self, void *Py_UNUSED(ignored)) { Py_ssize_t i; + LOCK_PTR(self); char *ptr = self->b_ptr; for (i = 0; i < self->b_size; ++i) if (*ptr++ == '\0') break; - return PyBytes_FromStringAndSize(self->b_ptr, i); + PyObject *res = PyBytes_FromStringAndSize(self->b_ptr, i); + UNLOCK_PTR(self); + return res; } static int @@ -1486,9 +1492,11 @@ CharArray_set_value(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored) } ptr = PyBytes_AS_STRING(value); + LOCK_PTR(self); memcpy(self->b_ptr, ptr, size); if (size < self->b_size) self->b_ptr[size] = '\0'; + UNLOCK_PTR(self); Py_DECREF(value); return 0; @@ -1507,10 +1515,13 @@ WCharArray_get_value(CDataObject *self, void *Py_UNUSED(ignored)) { Py_ssize_t i; wchar_t *ptr = (wchar_t *)self->b_ptr; + LOCK_PTR(self); for (i = 0; i < self->b_size/(Py_ssize_t)sizeof(wchar_t); ++i) if (*ptr++ == (wchar_t)0) break; - return PyUnicode_FromWideChar((wchar_t *)self->b_ptr, i); + PyObject *res = PyUnicode_FromWideChar((wchar_t *)self->b_ptr, i); + UNLOCK_PTR(self); + return res; } static int @@ -1540,9 +1551,12 @@ WCharArray_set_value(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored PyErr_SetString(PyExc_ValueError, "string too long"); return -1; } + LOCK_PTR(self); if (PyUnicode_AsWideChar(value, (wchar_t *)self->b_ptr, size) < 0) { + UNLOCK_PTR(self); return -1; } + UNLOCK_PTR(self); return 0; } @@ -2053,6 +2067,7 @@ c_void_p_from_param_impl(PyObject *type, PyTypeObject *cls, PyObject *value) parg->pffi_type = &ffi_type_pointer; parg->tag = 'P'; Py_INCREF(value); + // Function pointers don't change their contents, no need to lock parg->value.p = *(void **)func->b_ptr; parg->obj = value; return (PyObject *)parg; @@ -2079,7 +2094,7 @@ c_void_p_from_param_impl(PyObject *type, PyTypeObject *cls, PyObject *value) parg->tag = 'Z'; parg->obj = Py_NewRef(value); /* Remember: b_ptr points to where the pointer is stored! */ - parg->value.p = *(void **)(((CDataObject *)value)->b_ptr); + parg->value.p = locked_deref((CDataObject *)value); return (PyObject *)parg; } } @@ -2196,7 +2211,7 @@ PyCSimpleType_paramfunc(ctypes_state *st, CDataObject *self) parg->tag = fmt[0]; parg->pffi_type = fd->pffi_type; parg->obj = Py_NewRef(self); - memcpy(&parg->value, self->b_ptr, self->b_size); + locked_memcpy_from(&parg->value, self, self->b_size); return parg; } @@ -2697,7 +2712,7 @@ PyCFuncPtrType_paramfunc(ctypes_state *st, CDataObject *self) parg->tag = 'P'; parg->pffi_type = &ffi_type_pointer; parg->obj = Py_NewRef(self); - parg->value.p = *(void **)self->b_ptr; + parg->value.p = locked_deref(self); return parg; } @@ -3017,8 +3032,11 @@ PyCData_reduce_impl(PyObject *myself, PyTypeObject *cls) if (dict == NULL) { return NULL; } + LOCK_PTR(self); + PyObject *bytes = PyBytes_FromStringAndSize(self->b_ptr, self->b_size); + UNLOCK_PTR(self); return Py_BuildValue("O(O(NN))", st->_unpickle, Py_TYPE(myself), dict, - PyBytes_FromStringAndSize(self->b_ptr, self->b_size)); + bytes); } static PyObject * @@ -3036,7 +3054,10 @@ PyCData_setstate(PyObject *myself, PyObject *args) } if (len > self->b_size) len = self->b_size; + // XXX Can we use locked_memcpy_to()? + LOCK_PTR(self); memmove(self->b_ptr, data, len); + UNLOCK_PTR(self); mydict = PyObject_GetAttrString(myself, "__dict__"); if (mydict == NULL) { return NULL; @@ -3094,6 +3115,7 @@ static PyType_Spec pycdata_spec = { static int PyCData_MallocBuffer(CDataObject *obj, StgInfo *info) { + // We don't have to lock in this function if ((size_t)info->size <= sizeof(obj->b_value)) { /* No need to call malloc, can use the default buffer */ obj->b_ptr = (char *)&obj->b_value; @@ -3194,6 +3216,7 @@ PyCData_AtAddress(ctypes_state *st, PyObject *type, void *buf) return NULL; } assert(CDataObject_Check(st, pd)); + // XXX Use an atomic load if the size is 1, 2, 4, or 8 bytes? pd->b_ptr = (char *)buf; pd->b_length = info->length; pd->b_size = info->size; @@ -3288,9 +3311,7 @@ _PyCData_set(ctypes_state *st, if (err == -1) return NULL; if (err) { - memcpy(ptr, - src->b_ptr, - size); + locked_memcpy_from(ptr, src, size); if (PyCPointerTypeObject_Check(st, type)) { /* XXX */ @@ -3891,6 +3912,7 @@ PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, PyObject *kwds) self->paramflags = Py_XNewRef(paramflags); + // No other threads can have this object *(void **)self->b_ptr = address; Py_INCREF(dll); Py_DECREF(ftuple); @@ -4823,18 +4845,23 @@ Array_subscript(PyObject *myself, PyObject *item) if (slicelen <= 0) return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); if (step == 1) { - return PyBytes_FromStringAndSize(ptr + start, + LOCK_PTR(self); + PyObject *res = PyBytes_FromStringAndSize(ptr + start, slicelen); + UNLOCK_PTR(self); + return res; } dest = (char *)PyMem_Malloc(slicelen); if (dest == NULL) return PyErr_NoMemory(); + LOCK_PTR(self); for (cur = start, i = 0; i < slicelen; cur += step, i++) { dest[i] = ptr[cur]; } + UNLOCK_PTR(self); np = PyBytes_FromStringAndSize(dest, slicelen); PyMem_Free(dest); @@ -4847,8 +4874,11 @@ Array_subscript(PyObject *myself, PyObject *item) if (slicelen <= 0) return Py_GetConstant(Py_CONSTANT_EMPTY_STR); if (step == 1) { - return PyUnicode_FromWideChar(ptr + start, + LOCK_PTR(self); + PyObject *res = PyUnicode_FromWideChar(ptr + start, slicelen); + UNLOCK_PTR(self); + return res; } dest = PyMem_New(wchar_t, slicelen); @@ -4857,10 +4887,12 @@ Array_subscript(PyObject *myself, PyObject *item) return NULL; } + LOCK_PTR(self); for (cur = start, i = 0; i < slicelen; cur += step, i++) { dest[i] = ptr[cur]; } + UNLOCK_PTR(self); np = PyUnicode_FromWideChar(dest, slicelen); PyMem_Free(dest); @@ -5118,7 +5150,9 @@ Simple_set_value(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored)) assert(info); /* Cannot be NULL for CDataObject instances */ assert(info->setfunc); + LOCK_PTR(self); result = info->setfunc(self->b_ptr, value, info->size); + UNLOCK_PTR(self); if (!result) return -1; @@ -5147,7 +5181,10 @@ Simple_get_value(CDataObject *self, void *Py_UNUSED(ignored)) } assert(info); /* Cannot be NULL for CDataObject instances */ assert(info->getfunc); - return info->getfunc(self->b_ptr, self->b_size); + LOCK_PTR(self); + PyObject *res = info->getfunc(self->b_ptr, self->b_size); + UNLOCK_PTR(self); + return res; } static PyGetSetDef Simple_getsets[] = { @@ -5183,7 +5220,10 @@ static PyMethodDef Simple_methods[] = { static int Simple_bool(CDataObject *self) { - return memcmp(self->b_ptr, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", self->b_size); + LOCK_PTR(self); + int cmp = memcmp(self->b_ptr, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", self->b_size); + UNLOCK_PTR(self); + return cmp; } /* "%s(%s)" % (self.__class__.__name__, self.value) */ @@ -5239,8 +5279,9 @@ Pointer_item(PyObject *myself, Py_ssize_t index) Py_ssize_t size; Py_ssize_t offset; PyObject *proto; + void *deref = locked_deref(self); - if (*(void **)self->b_ptr == NULL) { + if (deref == NULL) { PyErr_SetString(PyExc_ValueError, "NULL pointer access"); return NULL; @@ -5267,7 +5308,7 @@ Pointer_item(PyObject *myself, Py_ssize_t index) offset = index * iteminfo->size; return PyCData_get(st, proto, stginfo->getfunc, (PyObject *)self, - index, size, (*(char **)self->b_ptr) + offset); + index, size, (deref + offset)); } static int @@ -5284,7 +5325,8 @@ Pointer_ass_item(PyObject *myself, Py_ssize_t index, PyObject *value) return -1; } - if (*(void **)self->b_ptr == NULL) { + void *deref = locked_deref(self); + if (deref == NULL) { PyErr_SetString(PyExc_ValueError, "NULL pointer access"); return -1; @@ -5311,13 +5353,14 @@ Pointer_ass_item(PyObject *myself, Py_ssize_t index, PyObject *value) offset = index * iteminfo->size; return PyCData_set(st, (PyObject *)self, proto, stginfo->setfunc, value, - index, size, (*(char **)self->b_ptr) + offset); + index, size, (deref + offset)); } static PyObject * Pointer_get_contents(CDataObject *self, void *closure) { - if (*(void **)self->b_ptr == NULL) { + void *deref = locked_deref(self); + if (deref == NULL) { PyErr_SetString(PyExc_ValueError, "NULL pointer access"); return NULL; @@ -5332,7 +5375,7 @@ Pointer_get_contents(CDataObject *self, void *closure) return PyCData_FromBaseObj(st, stginfo->proto, (PyObject *)self, 0, - *(void **)self->b_ptr); + deref); } static int @@ -5367,7 +5410,7 @@ Pointer_set_contents(CDataObject *self, PyObject *value, void *closure) } dst = (CDataObject *)value; - *(void **)self->b_ptr = dst->b_ptr; + locked_deref_assign(self, dst->b_ptr); /* A Pointer instance must keep the value it points to alive. So, a @@ -5502,41 +5545,51 @@ Pointer_subscript(PyObject *myself, PyObject *item) } assert(iteminfo); if (iteminfo->getfunc == _ctypes_get_fielddesc("c")->getfunc) { - char *ptr = *(char **)self->b_ptr; + char *ptr = locked_deref(self); char *dest; if (len <= 0) return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); if (step == 1) { - return PyBytes_FromStringAndSize(ptr + start, + LOCK_PTR(self); + PyObject *res = PyBytes_FromStringAndSize(ptr + start, len); + UNLOCK_PTR(self); + return res; } dest = (char *)PyMem_Malloc(len); if (dest == NULL) return PyErr_NoMemory(); + LOCK_PTR(self); for (cur = start, i = 0; i < len; cur += step, i++) { dest[i] = ptr[cur]; } + UNLOCK_PTR(self); np = PyBytes_FromStringAndSize(dest, len); PyMem_Free(dest); return np; } if (iteminfo->getfunc == _ctypes_get_fielddesc("u")->getfunc) { - wchar_t *ptr = *(wchar_t **)self->b_ptr; + wchar_t *ptr = locked_deref(self); wchar_t *dest; if (len <= 0) return Py_GetConstant(Py_CONSTANT_EMPTY_STR); if (step == 1) { - return PyUnicode_FromWideChar(ptr + start, + LOCK_PTR(self); + PyObject *res = PyUnicode_FromWideChar(ptr + start, len); + UNLOCK_PTR(self); + return res; } dest = PyMem_New(wchar_t, len); if (dest == NULL) return PyErr_NoMemory(); + LOCK_PTR(self); for (cur = start, i = 0; i < len; cur += step, i++) { dest[i] = ptr[cur]; } + UNLOCK_PTR(self); np = PyUnicode_FromWideChar(dest, len); PyMem_Free(dest); return np; @@ -5562,7 +5615,7 @@ Pointer_subscript(PyObject *myself, PyObject *item) static int Pointer_bool(CDataObject *self) { - return (*(void **)self->b_ptr != NULL); + return locked_deref(self) != NULL; } static PyType_Slot pycpointer_slots[] = { @@ -5770,7 +5823,7 @@ cast(void *ptr, PyObject *src, PyObject *ctype) } } /* Should we assert that result is a pointer type? */ - memcpy(result->b_ptr, &ptr, sizeof(void *)); + locked_memcpy_to(result, &ptr, sizeof(void *)); return (PyObject *)result; failed: diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index 45e00a538fb5a5..ce4acdfef820c6 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -152,6 +152,9 @@ union value { struct tagCDataObject { PyObject_HEAD +#ifdef Py_GIL_DISABLED + PyMutex b_ptr_lock; +#endif char *b_ptr; /* pointer to memory block */ int b_needsfree; /* need _we_ free the memory? */ CDataObject *b_base; /* pointer to base object or NULL */ @@ -181,6 +184,9 @@ typedef struct { typedef struct { /* First part identical to tagCDataObject */ PyObject_HEAD +#ifdef Py_GIL_DISABLED + PyMutex b_ptr_lock; +#endif char *b_ptr; /* pointer to memory block */ int b_needsfree; /* need _we_ free the memory? */ CDataObject *b_base; /* pointer to base object or NULL */ @@ -543,3 +549,44 @@ PyStgInfo_Init(ctypes_state *state, PyTypeObject *type) info->initialized = 1; return info; } + +#ifdef Py_GIL_DISABLED +# define LOCK_PTR(self) PyMutex_Lock(&self->b_ptr_lock) +# define UNLOCK_PTR(self) PyMutex_Unlock(&self->b_ptr_lock) +#else +# define LOCK_PTR(self) +# define UNLOCK_PTR(self) +#endif + +static inline void +locked_memcpy_to(CDataObject *self, void *buf, Py_ssize_t size) +{ + LOCK_PTR(self); + memcpy(self->b_ptr, buf, size); + UNLOCK_PTR(self); +} + +static inline void +locked_memcpy_from(void *buf, CDataObject *self, Py_ssize_t size) +{ + LOCK_PTR(self); + memcpy(buf, self->b_ptr, size); + UNLOCK_PTR(self); +} + +static inline void * +locked_deref(CDataObject *self) +{ + LOCK_PTR(self); + void *ptr = *(void **)self->b_ptr; + UNLOCK_PTR(self); + return ptr; +} + +static inline void +locked_deref_assign(CDataObject *self, void *new_ptr) +{ + LOCK_PTR(self); + *self->b_ptr = new_ptr; + UNLOCK_PTR(self); +} From 8c4c49a4c1862274b66c620fbbd72f8e807e42ef Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 4 Jan 2025 11:29:32 -0500 Subject: [PATCH 02/24] Add a test. --- Lib/test/test_ctypes/test_arrays.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index c80fdff5de685d..da61837db1fc3b 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -5,7 +5,7 @@ create_string_buffer, create_unicode_buffer, c_char, c_wchar, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, c_long, c_ulonglong, c_float, c_double, c_longdouble) -from test.support import bigmemtest, _2G +from test.support import bigmemtest, _2G, threading_helper from ._support import (_CData, PyCArrayType, Py_TPFLAGS_DISALLOW_INSTANTIATION, Py_TPFLAGS_IMMUTABLETYPE) @@ -267,6 +267,24 @@ def test_bpo36504_signed_int_overflow(self): def test_large_array(self, size): c_char * size + @threading_helper.requires_working_threading() + @unittest.skipUnless(support.Py_GIL_DISABLED, "only meaningful if the GIL is disabled") + def test_thread_safety(self): + from threading import Thread + + buffer = (ctypes.c_char_p * 10)() + + def run(): + for i in range(100): + buffer.value = b"hello" + buffer[0] = b"j" + + with threading_helper.catch_threading_exception() as cm: + with threading_helper.start_threads((run for _ in range(25))): + pass + + self.assertIsNone(cm.exc_value) + if __name__ == '__main__': unittest.main() From da293c14262a946b00c08fa45aa3ea7dd2551fda Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 4 Jan 2025 11:31:52 -0500 Subject: [PATCH 03/24] Fix the test. --- Lib/test/test_ctypes/test_arrays.py | 6 +++--- Modules/_ctypes/ctypes.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index da61837db1fc3b..a575ebc76ff582 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -5,7 +5,7 @@ create_string_buffer, create_unicode_buffer, c_char, c_wchar, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint, c_long, c_ulonglong, c_float, c_double, c_longdouble) -from test.support import bigmemtest, _2G, threading_helper +from test.support import bigmemtest, _2G, threading_helper, Py_GIL_DISABLED from ._support import (_CData, PyCArrayType, Py_TPFLAGS_DISALLOW_INSTANTIATION, Py_TPFLAGS_IMMUTABLETYPE) @@ -268,7 +268,7 @@ def test_large_array(self, size): c_char * size @threading_helper.requires_working_threading() - @unittest.skipUnless(support.Py_GIL_DISABLED, "only meaningful if the GIL is disabled") + @unittest.skipUnless(Py_GIL_DISABLED, "only meaningful if the GIL is disabled") def test_thread_safety(self): from threading import Thread @@ -280,7 +280,7 @@ def run(): buffer[0] = b"j" with threading_helper.catch_threading_exception() as cm: - with threading_helper.start_threads((run for _ in range(25))): + with threading_helper.start_threads((Thread(target=run) for _ in range(25))): pass self.assertIsNone(cm.exc_value) diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index ce4acdfef820c6..77db066d62734c 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -587,6 +587,6 @@ static inline void locked_deref_assign(CDataObject *self, void *new_ptr) { LOCK_PTR(self); - *self->b_ptr = new_ptr; + *(void **)self->b_ptr = new_ptr; UNLOCK_PTR(self); } From d01b757f596d97987f64e73d413fdf551b7c43fd Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 4 Jan 2025 11:32:52 -0500 Subject: [PATCH 04/24] Add blurb. --- .../next/Library/2025-01-04-11-32-46.gh-issue-128182.SJ2Zsa.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-01-04-11-32-46.gh-issue-128182.SJ2Zsa.rst diff --git a/Misc/NEWS.d/next/Library/2025-01-04-11-32-46.gh-issue-128182.SJ2Zsa.rst b/Misc/NEWS.d/next/Library/2025-01-04-11-32-46.gh-issue-128182.SJ2Zsa.rst new file mode 100644 index 00000000000000..038fecb5710436 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-01-04-11-32-46.gh-issue-128182.SJ2Zsa.rst @@ -0,0 +1,2 @@ +Fix crash when using :mod:`ctypes` pointers concurrently on the :term:`free +threaded ` build. From 8bb980f519854417ebb28c1550dd8c57e0b0f4a3 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 4 Jan 2025 11:57:44 -0500 Subject: [PATCH 05/24] Add a lock for generic helper functions. --- Modules/_ctypes/_ctypes.c | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 4a8eb218fc9554..7ec7241c1881fd 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3216,7 +3216,6 @@ PyCData_AtAddress(ctypes_state *st, PyObject *type, void *buf) return NULL; } assert(CDataObject_Check(st, pd)); - // XXX Use an atomic load if the size is 1, 2, 4, or 8 bytes? pd->b_ptr = (char *)buf; pd->b_length = info->length; pd->b_size = info->size; @@ -3242,15 +3241,23 @@ PyObject * PyCData_get(ctypes_state *st, PyObject *type, GETFUNC getfunc, PyObject *src, Py_ssize_t index, Py_ssize_t size, char *adr) { - if (getfunc) - return getfunc(adr, size); + CDataObject *cdata = (CDataObject *)src; + if (getfunc) { + LOCK_PTR(cdata); + PyObject *res = getfunc(adr, size); + UNLOCK_PTR(cdata); + return res; + } assert(type); StgInfo *info; if (PyStgInfo_FromType(st, type, &info) < 0) { return NULL; } if (info && info->getfunc && !_ctypes_simple_instance(st, type)) { - return info->getfunc(adr, size); + LOCK_PTR(cdata); + PyObject *res = info->getfunc(adr, size); + UNLOCK_PTR(cdata); + return res; } return PyCData_FromBaseObj(st, type, src, index, adr); } @@ -3267,15 +3274,22 @@ _PyCData_set(ctypes_state *st, int err; if (setfunc) { - return setfunc(ptr, value, size); + LOCK_PTR(dst); + PyObject *res = setfunc(ptr, value, size); + UNLOCK_PTR(dst); + return res; } if (!CDataObject_Check(st, value)) { StgInfo *info; if (PyStgInfo_FromType(st, type, &info) < 0) { return NULL; } - if (info && info->setfunc) - return info->setfunc(ptr, value, size); + if (info && info->setfunc) { + LOCK_PTR(dst); + PyObject *res = info->setfunc(ptr, value, size); + UNLOCK_PTR(dst); + return res; + } /* If value is a tuple, we try to call the type with the tuple and use the result! @@ -3295,7 +3309,9 @@ _PyCData_set(ctypes_state *st, Py_DECREF(ob); return result; } else if (value == Py_None && PyCPointerTypeObject_Check(st, type)) { + LOCK_PTR(dst); *(void **)ptr = NULL; + UNLOCK_PTR(dst); Py_RETURN_NONE; } else { PyErr_Format(PyExc_TypeError, @@ -3345,7 +3361,9 @@ _PyCData_set(ctypes_state *st, ((PyTypeObject *)type)->tp_name); return NULL; } + LOCK_PTR(dst); *(void **)ptr = src->b_ptr; + UNLOCK_PTR(dst); keep = GetKeepedObjects(src); if (keep == NULL) From d60f2e237d4f8ee4265c873186e3dbfb6c93a29b Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 4 Jan 2025 12:20:13 -0500 Subject: [PATCH 06/24] Add documentation note. --- Doc/library/ctypes.rst | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 09692e56d29a39..c0c2cf35d6a0a6 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -870,6 +870,35 @@ invalid non-\ ``NULL`` pointers would crash Python):: ValueError: NULL pointer access >>> +.. _ctypes-thread-safety: + +Thread Safety Without The GIL +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In Python 3.13, the :term:`GIL` may be disabled on :term:`experimental free threaded ` builds. +In ctypes, reads and writes to a single object concurrently is safe, but not across multiple objects.:: + + >>> number = c_int(42) + >>> pointer_a = pointer(number) + >>> pointer_b = pointer(number) + +In the above, it's only safe for one object to read and write to the address at once if the :term:`GIL` is disabled. +So, ``pointer_a`` can be shared and written to across multiple threads, but only if ``pointer_b`` +is not also attempting to do the same. If this is an issue, consider using a :class:`threading.Lock` +to synchronize access to memory.:: + + >>> import threading + >>> lock = threading.Lock() + >>> # Thread 1 + >>> with lock: + ... pointer_a.contents = 24 + >>> # Thread 2 + >>> with lock: + ... pointer_b.contents = 42 + +.. seealso:: + + :func:`sys._is_gil_enabled` if you would like to dynamically synchronize your application. .. _ctypes-type-conversions: From de4c4eabe421b688cf3d50d10fd14f163500a28a Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 4 Jan 2025 12:42:29 -0500 Subject: [PATCH 07/24] Fix compiler warning on GIL-ful builds. --- Modules/_ctypes/_ctypes.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 7ec7241c1881fd..f5ac476789ed54 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3241,7 +3241,10 @@ PyObject * PyCData_get(ctypes_state *st, PyObject *type, GETFUNC getfunc, PyObject *src, Py_ssize_t index, Py_ssize_t size, char *adr) { +#ifdef Py_GIL_DISABLED + // This isn't used if the GIL is enabled, so it causes a compiler warning. CDataObject *cdata = (CDataObject *)src; +#endif if (getfunc) { LOCK_PTR(cdata); PyObject *res = getfunc(adr, size); From 23d8e21f7c716d164a71dbe39dd1f51cc86a8bea Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 4 Jan 2025 12:54:59 -0500 Subject: [PATCH 08/24] Fix MSVC compiler warnings. --- Modules/_ctypes/_ctypes.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index f5ac476789ed54..f9123ff5a96032 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -907,7 +907,7 @@ CDataType_from_buffer_copy_impl(PyObject *type, PyTypeObject *cls, result = generic_pycdata_new(st, (PyTypeObject *)type, NULL, NULL); if (result != NULL) { - locked_memcpy_to((CDataObject *) result, buffer->buf + offset, info->size); + locked_memcpy_to((CDataObject *) result, (char *)buffer->buf + offset, info->size); } return result; } @@ -5329,7 +5329,7 @@ Pointer_item(PyObject *myself, Py_ssize_t index) offset = index * iteminfo->size; return PyCData_get(st, proto, stginfo->getfunc, (PyObject *)self, - index, size, (deref + offset)); + index, size, (char *)(deref + offset)); } static int @@ -5374,7 +5374,7 @@ Pointer_ass_item(PyObject *myself, Py_ssize_t index, PyObject *value) offset = index * iteminfo->size; return PyCData_set(st, (PyObject *)self, proto, stginfo->setfunc, value, - index, size, (deref + offset)); + index, size, (char *)(deref + offset)); } static PyObject * From bdee2b253527dfd2a3407a4349d10ab0c545473b Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 4 Jan 2025 15:47:55 -0500 Subject: [PATCH 09/24] Fix MSVC compiler warnings (actually this time) --- Modules/_ctypes/_ctypes.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index f9123ff5a96032..35837a1b91a47f 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5329,7 +5329,7 @@ Pointer_item(PyObject *myself, Py_ssize_t index) offset = index * iteminfo->size; return PyCData_get(st, proto, stginfo->getfunc, (PyObject *)self, - index, size, (char *)(deref + offset)); + index, size, (char *)((char *)deref + offset)); } static int @@ -5374,7 +5374,7 @@ Pointer_ass_item(PyObject *myself, Py_ssize_t index, PyObject *value) offset = index * iteminfo->size; return PyCData_set(st, (PyObject *)self, proto, stginfo->setfunc, value, - index, size, (char *)(deref + offset)); + index, size, ((char *)deref + offset)); } static PyObject * From 9f272d94a848ab109322477e2a82cfeb5bd5ac2f Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 6 Jan 2025 09:51:33 -0500 Subject: [PATCH 10/24] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/library/ctypes.rst | 14 ++++++++------ Lib/test/test_ctypes/test_arrays.py | 3 ++- Modules/_ctypes/ctypes.h | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index c0c2cf35d6a0a6..b78e65ab5f0b62 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -872,15 +872,17 @@ invalid non-\ ``NULL`` pointers would crash Python):: .. _ctypes-thread-safety: -Thread Safety Without The GIL +Thread safety without the GIL ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In Python 3.13, the :term:`GIL` may be disabled on :term:`experimental free threaded ` builds. -In ctypes, reads and writes to a single object concurrently is safe, but not across multiple objects.:: +In ctypes, reads and writes to a single object concurrently is safe, but not across multiple objects: - >>> number = c_int(42) - >>> pointer_a = pointer(number) - >>> pointer_b = pointer(number) + .. code-block:: pycon + + >>> number = c_int(42) + >>> pointer_a = pointer(number) + >>> pointer_b = pointer(number) In the above, it's only safe for one object to read and write to the address at once if the :term:`GIL` is disabled. So, ``pointer_a`` can be shared and written to across multiple threads, but only if ``pointer_b`` @@ -898,7 +900,7 @@ to synchronize access to memory.:: .. seealso:: - :func:`sys._is_gil_enabled` if you would like to dynamically synchronize your application. + Use :func:`sys._is_gil_enabled` to dynamically synchronize your application. .. _ctypes-type-conversions: diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index a575ebc76ff582..fe76a7f382d49c 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -280,7 +280,8 @@ def run(): buffer[0] = b"j" with threading_helper.catch_threading_exception() as cm: - with threading_helper.start_threads((Thread(target=run) for _ in range(25))): + threads = (Thread(target=run) for _ in range(25)) + with threading_helper.start_threads(threads): pass self.assertIsNone(cm.exc_value) diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index 77db066d62734c..5e004403c4189a 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -562,7 +562,7 @@ static inline void locked_memcpy_to(CDataObject *self, void *buf, Py_ssize_t size) { LOCK_PTR(self); - memcpy(self->b_ptr, buf, size); + (void)memcpy(self->b_ptr, buf, size); UNLOCK_PTR(self); } @@ -570,7 +570,7 @@ static inline void locked_memcpy_from(void *buf, CDataObject *self, Py_ssize_t size) { LOCK_PTR(self); - memcpy(buf, self->b_ptr, size); + (void)memcpy(buf, self->b_ptr, size); UNLOCK_PTR(self); } From 32a885feee29c3a4b4397a50016d3c186478e119 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 6 Jan 2025 10:23:14 -0500 Subject: [PATCH 11/24] Fix Sphinx code block. --- Doc/library/ctypes.rst | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index b78e65ab5f0b62..2a7977fcd9a0d3 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -887,16 +887,18 @@ In ctypes, reads and writes to a single object concurrently is safe, but not acr In the above, it's only safe for one object to read and write to the address at once if the :term:`GIL` is disabled. So, ``pointer_a`` can be shared and written to across multiple threads, but only if ``pointer_b`` is not also attempting to do the same. If this is an issue, consider using a :class:`threading.Lock` -to synchronize access to memory.:: - - >>> import threading - >>> lock = threading.Lock() - >>> # Thread 1 - >>> with lock: - ... pointer_a.contents = 24 - >>> # Thread 2 - >>> with lock: - ... pointer_b.contents = 42 +to synchronize access to memory: + + .. code-block:: pycon + + >>> import threading + >>> lock = threading.Lock() + >>> # Thread 1 + >>> with lock: + ... pointer_a.contents = 24 + >>> # Thread 2 + >>> with lock: + ... pointer_b.contents = 42 .. seealso:: From 1c92b4bacaabff63539ad09cb2f882f87075080e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 6 Jan 2025 10:24:17 -0500 Subject: [PATCH 12/24] Flatten call to PyUnicode_AsWideChar --- Modules/_ctypes/_ctypes.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 35837a1b91a47f..282971e42abe68 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -1552,12 +1552,9 @@ WCharArray_set_value(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored return -1; } LOCK_PTR(self); - if (PyUnicode_AsWideChar(value, (wchar_t *)self->b_ptr, size) < 0) { - UNLOCK_PTR(self); - return -1; - } + int rc = PyUnicode_AsWideChar(value, (wchar_t *)self->b_ptr, size); UNLOCK_PTR(self); - return 0; + return rc; } static PyGetSetDef WCharArray_getsets[] = { From ff34cc2c49e26e18b31c7f4c0a934b33214f9c52 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 6 Jan 2025 10:26:34 -0500 Subject: [PATCH 13/24] Fix lock. --- Modules/_ctypes/_ctypes.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 282971e42abe68..519ecca51ee3f1 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3361,9 +3361,9 @@ _PyCData_set(ctypes_state *st, ((PyTypeObject *)type)->tp_name); return NULL; } - LOCK_PTR(dst); + LOCK_PTR(src); *(void **)ptr = src->b_ptr; - UNLOCK_PTR(dst); + UNLOCK_PTR(src); keep = GetKeepedObjects(src); if (keep == NULL) From 804f1b65304767059bf645a5b6e9bcb19f756e79 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 6 Jan 2025 10:28:33 -0500 Subject: [PATCH 14/24] Fix indentation. --- Modules/_ctypes/_ctypes.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 519ecca51ee3f1..7739830e4f0b59 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -4865,7 +4865,7 @@ Array_subscript(PyObject *myself, PyObject *item) if (step == 1) { LOCK_PTR(self); PyObject *res = PyBytes_FromStringAndSize(ptr + start, - slicelen); + slicelen); UNLOCK_PTR(self); return res; } @@ -4894,7 +4894,7 @@ Array_subscript(PyObject *myself, PyObject *item) if (step == 1) { LOCK_PTR(self); PyObject *res = PyUnicode_FromWideChar(ptr + start, - slicelen); + slicelen); UNLOCK_PTR(self); return res; } @@ -5371,7 +5371,7 @@ Pointer_ass_item(PyObject *myself, Py_ssize_t index, PyObject *value) offset = index * iteminfo->size; return PyCData_set(st, (PyObject *)self, proto, stginfo->setfunc, value, - index, size, ((char *)deref + offset)); + index, size, ((char *)deref + offset)); } static PyObject * @@ -5571,7 +5571,7 @@ Pointer_subscript(PyObject *myself, PyObject *item) if (step == 1) { LOCK_PTR(self); PyObject *res = PyBytes_FromStringAndSize(ptr + start, - len); + len); UNLOCK_PTR(self); return res; } @@ -5596,7 +5596,7 @@ Pointer_subscript(PyObject *myself, PyObject *item) if (step == 1) { LOCK_PTR(self); PyObject *res = PyUnicode_FromWideChar(ptr + start, - len); + len); UNLOCK_PTR(self); return res; } From bae3c5bb27f54f077cf0d2afa5931fb1a7d86552 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 6 Jan 2025 10:41:20 -0500 Subject: [PATCH 15/24] Revert "Flatten call to PyUnicode_AsWideChar" This reverts commit 1c92b4bacaabff63539ad09cb2f882f87075080e. --- Modules/_ctypes/_ctypes.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 7739830e4f0b59..b591097826e956 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -1552,9 +1552,12 @@ WCharArray_set_value(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored return -1; } LOCK_PTR(self); - int rc = PyUnicode_AsWideChar(value, (wchar_t *)self->b_ptr, size); + if (PyUnicode_AsWideChar(value, (wchar_t *)self->b_ptr, size) < 0) { + UNLOCK_PTR(self); + return -1; + } UNLOCK_PTR(self); - return rc; + return 0; } static PyGetSetDef WCharArray_getsets[] = { From d720d9ab18dc506f2bb36fee3ea5b5145483159b Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 6 Jan 2025 10:42:14 -0500 Subject: [PATCH 16/24] Properly flatten call to PyUnicode_AsWideChar --- Modules/_ctypes/_ctypes.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index b591097826e956..da3df76f024d41 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -1552,12 +1552,9 @@ WCharArray_set_value(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored return -1; } LOCK_PTR(self); - if (PyUnicode_AsWideChar(value, (wchar_t *)self->b_ptr, size) < 0) { - UNLOCK_PTR(self); - return -1; - } + int rc = PyUnicode_AsWideChar(value, (wchar_t *)self->b_ptr, size); UNLOCK_PTR(self); - return 0; + return rc < 0 ? -1 : 0; } static PyGetSetDef WCharArray_getsets[] = { From 04b32527c45cb1fb32d10fa175814606ad5f20e4 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 6 Jan 2025 10:43:19 -0500 Subject: [PATCH 17/24] Fix indentation. --- Modules/_ctypes/_ctypes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index da3df76f024d41..91c3ce060d9525 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5326,7 +5326,7 @@ Pointer_item(PyObject *myself, Py_ssize_t index) offset = index * iteminfo->size; return PyCData_get(st, proto, stginfo->getfunc, (PyObject *)self, - index, size, (char *)((char *)deref + offset)); + index, size, (char *)((char *)deref + offset)); } static int From 6401bbcecfc204ff2984a41e59122b5abb8efbca Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 6 Jan 2025 10:44:31 -0500 Subject: [PATCH 18/24] Clarify comment --- Modules/_ctypes/_ctypes.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 91c3ce060d9525..b89590bddcd597 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3112,7 +3112,9 @@ static PyType_Spec pycdata_spec = { static int PyCData_MallocBuffer(CDataObject *obj, StgInfo *info) { - // We don't have to lock in this function + /* We don't have to lock in this function, because it's only + used in constructors and therefore does not have concurrent + access. */ if ((size_t)info->size <= sizeof(obj->b_value)) { /* No need to call malloc, can use the default buffer */ obj->b_ptr = (char *)&obj->b_value; From 9d35e86f68fc8d4fbccb2660ea9899d875e5c8e8 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Wed, 8 Jan 2025 16:45:04 -0500 Subject: [PATCH 19/24] Switch to critical sections. --- Modules/_ctypes/_ctypes.c | 55 +++++++++++++++++++++++++-------------- Modules/_ctypes/ctypes.h | 15 +++++------ 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index b89590bddcd597..138ef2d66f3416 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -1443,9 +1443,9 @@ CharArray_set_raw(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored)) static PyObject * CharArray_get_raw(CDataObject *self, void *Py_UNUSED(ignored)) { + PyObject *res; LOCK_PTR(self); - // XXX Is it possible that PyBytes_FromStringAndSize is re-entrant? - PyObject *res = PyBytes_FromStringAndSize(self->b_ptr, self->b_size); + res = PyBytes_FromStringAndSize(self->b_ptr, self->b_size); UNLOCK_PTR(self); return res; } @@ -1454,12 +1454,13 @@ static PyObject * CharArray_get_value(CDataObject *self, void *Py_UNUSED(ignored)) { Py_ssize_t i; + PyObject *res; LOCK_PTR(self); char *ptr = self->b_ptr; for (i = 0; i < self->b_size; ++i) if (*ptr++ == '\0') break; - PyObject *res = PyBytes_FromStringAndSize(self->b_ptr, i); + res = PyBytes_FromStringAndSize(self->b_ptr, i); UNLOCK_PTR(self); return res; } @@ -1514,12 +1515,13 @@ static PyObject * WCharArray_get_value(CDataObject *self, void *Py_UNUSED(ignored)) { Py_ssize_t i; + PyObject *res; wchar_t *ptr = (wchar_t *)self->b_ptr; LOCK_PTR(self); for (i = 0; i < self->b_size/(Py_ssize_t)sizeof(wchar_t); ++i) if (*ptr++ == (wchar_t)0) break; - PyObject *res = PyUnicode_FromWideChar((wchar_t *)self->b_ptr, i); + res = PyUnicode_FromWideChar((wchar_t *)self->b_ptr, i); UNLOCK_PTR(self); return res; } @@ -1551,8 +1553,9 @@ WCharArray_set_value(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored PyErr_SetString(PyExc_ValueError, "string too long"); return -1; } + int rc; LOCK_PTR(self); - int rc = PyUnicode_AsWideChar(value, (wchar_t *)self->b_ptr, size); + rc = PyUnicode_AsWideChar(value, (wchar_t *)self->b_ptr, size); UNLOCK_PTR(self); return rc < 0 ? -1 : 0; } @@ -3029,8 +3032,9 @@ PyCData_reduce_impl(PyObject *myself, PyTypeObject *cls) if (dict == NULL) { return NULL; } + PyObject *bytes; LOCK_PTR(self); - PyObject *bytes = PyBytes_FromStringAndSize(self->b_ptr, self->b_size); + bytes = PyBytes_FromStringAndSize(self->b_ptr, self->b_size); UNLOCK_PTR(self); return Py_BuildValue("O(O(NN))", st->_unpickle, Py_TYPE(myself), dict, bytes); @@ -3245,8 +3249,9 @@ PyCData_get(ctypes_state *st, PyObject *type, GETFUNC getfunc, PyObject *src, CDataObject *cdata = (CDataObject *)src; #endif if (getfunc) { + PyObject *res; LOCK_PTR(cdata); - PyObject *res = getfunc(adr, size); + res = getfunc(adr, size); UNLOCK_PTR(cdata); return res; } @@ -3256,8 +3261,9 @@ PyCData_get(ctypes_state *st, PyObject *type, GETFUNC getfunc, PyObject *src, return NULL; } if (info && info->getfunc && !_ctypes_simple_instance(st, type)) { + PyObject *res; LOCK_PTR(cdata); - PyObject *res = info->getfunc(adr, size); + res = info->getfunc(adr, size); UNLOCK_PTR(cdata); return res; } @@ -3276,8 +3282,9 @@ _PyCData_set(ctypes_state *st, int err; if (setfunc) { + PyObject *res; LOCK_PTR(dst); - PyObject *res = setfunc(ptr, value, size); + res = setfunc(ptr, value, size); UNLOCK_PTR(dst); return res; } @@ -3287,8 +3294,9 @@ _PyCData_set(ctypes_state *st, return NULL; } if (info && info->setfunc) { + PyObject *res; LOCK_PTR(dst); - PyObject *res = info->setfunc(ptr, value, size); + res = info->setfunc(ptr, value, size); UNLOCK_PTR(dst); return res; } @@ -3932,7 +3940,8 @@ PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, PyObject *kwds) self->paramflags = Py_XNewRef(paramflags); - // No other threads can have this object + // No other threads can have this object, no need to + // lock it. *(void **)self->b_ptr = address; Py_INCREF(dll); Py_DECREF(ftuple); @@ -4865,9 +4874,10 @@ Array_subscript(PyObject *myself, PyObject *item) if (slicelen <= 0) return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); if (step == 1) { + PyObject *res; LOCK_PTR(self); - PyObject *res = PyBytes_FromStringAndSize(ptr + start, - slicelen); + res = PyBytes_FromStringAndSize(ptr + start, + slicelen); UNLOCK_PTR(self); return res; } @@ -4894,8 +4904,9 @@ Array_subscript(PyObject *myself, PyObject *item) if (slicelen <= 0) return Py_GetConstant(Py_CONSTANT_EMPTY_STR); if (step == 1) { + PyObject *res; LOCK_PTR(self); - PyObject *res = PyUnicode_FromWideChar(ptr + start, + res = PyUnicode_FromWideChar(ptr + start, slicelen); UNLOCK_PTR(self); return res; @@ -5201,8 +5212,9 @@ Simple_get_value(CDataObject *self, void *Py_UNUSED(ignored)) } assert(info); /* Cannot be NULL for CDataObject instances */ assert(info->getfunc); + PyObject *res; LOCK_PTR(self); - PyObject *res = info->getfunc(self->b_ptr, self->b_size); + res = info->getfunc(self->b_ptr, self->b_size); UNLOCK_PTR(self); return res; } @@ -5240,8 +5252,9 @@ static PyMethodDef Simple_methods[] = { static int Simple_bool(CDataObject *self) { + int cmp; LOCK_PTR(self); - int cmp = memcmp(self->b_ptr, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", self->b_size); + cmp = memcmp(self->b_ptr, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", self->b_size); UNLOCK_PTR(self); return cmp; } @@ -5571,9 +5584,10 @@ Pointer_subscript(PyObject *myself, PyObject *item) if (len <= 0) return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES); if (step == 1) { + PyObject *res; LOCK_PTR(self); - PyObject *res = PyBytes_FromStringAndSize(ptr + start, - len); + res = PyBytes_FromStringAndSize(ptr + start, + len); UNLOCK_PTR(self); return res; } @@ -5596,9 +5610,10 @@ Pointer_subscript(PyObject *myself, PyObject *item) if (len <= 0) return Py_GetConstant(Py_CONSTANT_EMPTY_STR); if (step == 1) { + PyObject *res; LOCK_PTR(self); - PyObject *res = PyUnicode_FromWideChar(ptr + start, - len); + res = PyUnicode_FromWideChar(ptr + start, + len); UNLOCK_PTR(self); return res; } diff --git a/Modules/_ctypes/ctypes.h b/Modules/_ctypes/ctypes.h index 5e004403c4189a..cc09639e21f7c2 100644 --- a/Modules/_ctypes/ctypes.h +++ b/Modules/_ctypes/ctypes.h @@ -152,9 +152,6 @@ union value { struct tagCDataObject { PyObject_HEAD -#ifdef Py_GIL_DISABLED - PyMutex b_ptr_lock; -#endif char *b_ptr; /* pointer to memory block */ int b_needsfree; /* need _we_ free the memory? */ CDataObject *b_base; /* pointer to base object or NULL */ @@ -184,9 +181,6 @@ typedef struct { typedef struct { /* First part identical to tagCDataObject */ PyObject_HEAD -#ifdef Py_GIL_DISABLED - PyMutex b_ptr_lock; -#endif char *b_ptr; /* pointer to memory block */ int b_needsfree; /* need _we_ free the memory? */ CDataObject *b_base; /* pointer to base object or NULL */ @@ -550,9 +544,11 @@ PyStgInfo_Init(ctypes_state *state, PyTypeObject *type) return info; } +/* See discussion in gh-128490. The plan here is to eventually use a per-object + * lock rather than a critical section, but that work is for later. */ #ifdef Py_GIL_DISABLED -# define LOCK_PTR(self) PyMutex_Lock(&self->b_ptr_lock) -# define UNLOCK_PTR(self) PyMutex_Unlock(&self->b_ptr_lock) +# define LOCK_PTR(self) Py_BEGIN_CRITICAL_SECTION(self) +# define UNLOCK_PTR(self) Py_END_CRITICAL_SECTION() #else # define LOCK_PTR(self) # define UNLOCK_PTR(self) @@ -577,8 +573,9 @@ locked_memcpy_from(void *buf, CDataObject *self, Py_ssize_t size) static inline void * locked_deref(CDataObject *self) { + void *ptr; LOCK_PTR(self); - void *ptr = *(void **)self->b_ptr; + ptr = *(void **)self->b_ptr; UNLOCK_PTR(self); return ptr; } From ce942340bd905a6493200d673ce43151bd90f40f Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 9 Jan 2025 10:42:56 -0500 Subject: [PATCH 20/24] Update test_arrays.py Co-authored-by: Petr Viktorin --- Lib/test/test_ctypes/test_arrays.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_ctypes/test_arrays.py b/Lib/test/test_ctypes/test_arrays.py index fe76a7f382d49c..7f1f6cf58402c9 100644 --- a/Lib/test/test_ctypes/test_arrays.py +++ b/Lib/test/test_ctypes/test_arrays.py @@ -284,7 +284,8 @@ def run(): with threading_helper.start_threads(threads): pass - self.assertIsNone(cm.exc_value) + if cm.exc_value: + raise cm.exc_value if __name__ == '__main__': From d6a4bc846a5932632c40643863c4d651b79ad4ce Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 9 Jan 2025 11:08:04 -0500 Subject: [PATCH 21/24] Update Doc/library/ctypes.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/library/ctypes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 2a7977fcd9a0d3..e40fb52e312422 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -884,7 +884,7 @@ In ctypes, reads and writes to a single object concurrently is safe, but not acr >>> pointer_a = pointer(number) >>> pointer_b = pointer(number) -In the above, it's only safe for one object to read and write to the address at once if the :term:`GIL` is disabled. +In the above, it's only safe for one object to read and write to the address at once if the GIL is disabled. So, ``pointer_a`` can be shared and written to across multiple threads, but only if ``pointer_b`` is not also attempting to do the same. If this is an issue, consider using a :class:`threading.Lock` to synchronize access to memory: From 5a4c1d69263411bbcbea9a8513aba3ea1cfe9155 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 9 Jan 2025 11:08:19 -0500 Subject: [PATCH 22/24] Update Modules/_ctypes/_ctypes.c Co-authored-by: Petr Viktorin --- Modules/_ctypes/_ctypes.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 138ef2d66f3416..ee4dcd4f8ccc30 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -3117,8 +3117,11 @@ static int PyCData_MallocBuffer(CDataObject *obj, StgInfo *info) { /* We don't have to lock in this function, because it's only - used in constructors and therefore does not have concurrent - access. */ + * used in constructors and therefore does not have concurrent + * access. + */ + assert (Py_REFCNT(obj) == 1); + if ((size_t)info->size <= sizeof(obj->b_value)) { /* No need to call malloc, can use the default buffer */ obj->b_ptr = (char *)&obj->b_value; From 774a6ec19b259888b8254f56419f36622545eb8a Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 9 Jan 2025 11:10:41 -0500 Subject: [PATCH 23/24] Update Modules/_ctypes/_ctypes.c Co-authored-by: Petr Viktorin --- Modules/_ctypes/_ctypes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index ee4dcd4f8ccc30..495d4f9224055a 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -1553,7 +1553,7 @@ WCharArray_set_value(CDataObject *self, PyObject *value, void *Py_UNUSED(ignored PyErr_SetString(PyExc_ValueError, "string too long"); return -1; } - int rc; + Py_ssize_t rc; LOCK_PTR(self); rc = PyUnicode_AsWideChar(value, (wchar_t *)self->b_ptr, size); UNLOCK_PTR(self); From c8f2cd3e3d65987af50d4bbe418dd411b67c6921 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 9 Jan 2025 19:18:36 -0500 Subject: [PATCH 24/24] Remove silly seealso. --- Doc/library/ctypes.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index e40fb52e312422..714051c9f57482 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -900,9 +900,6 @@ to synchronize access to memory: >>> with lock: ... pointer_b.contents = 42 -.. seealso:: - - Use :func:`sys._is_gil_enabled` to dynamically synchronize your application. .. _ctypes-type-conversions: