diff --git a/Include/internal/pycore_dynarray.h b/Include/internal/pycore_dynarray.h new file mode 100644 index 00000000000000..6dfc2381028fb7 --- /dev/null +++ b/Include/internal/pycore_dynarray.h @@ -0,0 +1,233 @@ +#ifndef Py_INTERNAL_DYNARRAY_H +#define Py_INTERNAL_DYNARRAY_H +#ifdef __cplusplus +extern "C" { +#endif + +#include "Python.h" // Py_ssize_t + +#ifndef Py_BUILD_CORE +# error "this header requires Py_BUILD_CORE define" +#endif + +#define _PyDynArray_DEFAULT_SIZE 16 + +/* + * Deallocator for items on a _PyDynArray structure. A NULL pointer + * will never be given to the deallocator. + */ +typedef void (*_PyDynArray_Deallocator)(void *); + +/* + * Internal only dynamic array for CPython. + */ +typedef struct { + /* + * The actual items in the dynamic array. + * Don't access this field publicly to get + * items--use _PyDynArray_GET_ITEM() instead. + */ + void **items; + /* + * The length of the actual items array allocation. + */ + Py_ssize_t capacity; + /* + * The number of items in the array. + * Don't use this field publicly--use _PyDynArray_LENGTH() + */ + Py_ssize_t length; + /* + * The deallocator, set by one of the initializer functions. + * This may be NULL. + */ + _PyDynArray_Deallocator deallocator; +} _PyDynArray; + + +static inline void +_PyDynArray_ASSERT_VALID(_PyDynArray *array) +{ + assert(array != NULL); + assert(array->items != NULL); +} + +static inline void +_PyDynArray_ASSERT_INDEX(_PyDynArray *array, Py_ssize_t index) +{ + // Ensure the index is valid + assert(index >= 0); + assert(index < array->length); +} + +/* + * Initialize a dynamic array with an initial size and deallocator. + * + * If the deallocator is NULL, then nothing happens to items upon + * removal and upon array clearing. + * + * Returns -1 upon failure, 0 otherwise. + */ +PyAPI_FUNC(int) +_PyDynArray_InitWithSize(_PyDynArray *array, + _PyDynArray_Deallocator deallocator, + Py_ssize_t initial); + +/* + * Append to the array. + * + * Returns -1 upon failure, 0 otherwise. + * If this fails, the deallocator is not ran on the item. + */ +PyAPI_FUNC(int) _PyDynArray_Append(_PyDynArray *array, void *item); + +/* + * Insert an item at the target index. The index + * must currently be a valid index in the array. + * + * Returns -1 upon failure, 0 otherwise. + * If this fails, the deallocator is not ran on the item. + */ +PyAPI_FUNC(int) +_PyDynArray_Insert(_PyDynArray *array, Py_ssize_t index, void *item); + + +/* + * Clear all the fields on the array. + * + * Note that this does *not* free the actual dynamic array + * structure--use _PyDynArray_Free() for that. + * + * It's safe to call _PyDynArray_Init() or InitWithSize() again + * on the array after calling this. + */ +PyAPI_FUNC(void) _PyDynArray_Clear(_PyDynArray *array); + +/* + * Set a value at index in the array. + * + * If an item already exists at the target index, the deallocator + * is called on it, if the array has one set. + * + * This cannot fail. + */ +PyAPI_FUNC(void) +_PyDynArray_Set(_PyDynArray *array, Py_ssize_t index, void *item); + +/* + * Remove the item at the index, and call the deallocator on it (if the array + * has one set). + * + * This cannot fail. + */ +PyAPI_FUNC(void) +_PyDynArray_Remove(_PyDynArray *array, Py_ssize_t index); + +/* + * Remove the item at the index *without* deallocating it, and + * return the item. + * + * This cannot fail. + */ +PyAPI_FUNC(void *) +_PyDynArray_Pop(_PyDynArray *array, Py_ssize_t index); + +/* + * Clear all the fields on a dynamic array, and then + * free the dynamic array structure itself. + * + * The array must have been created by _PyDynArray_New() + */ +static inline void +_PyDynArray_Free(_PyDynArray *array) +{ + _PyDynArray_ASSERT_VALID(array); + _PyDynArray_Clear(array); + PyMem_RawFree(array); +} + +/* + * Equivalent to _PyDynArray_InitWithSize() with a default size of 16. + * + * Returns -1 upon failure, 0 otherwise. + */ +static inline int +_PyDynArray_Init(_PyDynArray *array, _PyDynArray_Deallocator deallocator) +{ + return _PyDynArray_InitWithSize(array, deallocator, _PyDynArray_DEFAULT_SIZE); +} + +/* + * Allocate and create a new dynamic array on the heap. + * + * The returned pointer should be freed with _PyDynArray_Free() + * If this function fails, it returns NULL. + */ +static inline _PyDynArray * +_PyDynArray_NewWithSize(_PyDynArray_Deallocator deallocator, Py_ssize_t initial) +{ + _PyDynArray *array = PyMem_RawMalloc(sizeof(_PyDynArray)); + if (array == NULL) + { + return NULL; + } + + if (_PyDynArray_InitWithSize(array, deallocator, initial) < 0) + { + PyMem_RawFree(array); + return NULL; + } + + _PyDynArray_ASSERT_VALID(array); // Sanity check + return array; +} + +/* + * Equivalent to _PyDynArray_NewWithSize() with a size of 16. + * + * The returned array must be freed with _PyDynArray_Free(). + * Returns NULL on failure. + */ +static inline _PyDynArray * +_PyDynArray_New(_PyDynArray_Deallocator deallocator) +{ + return _PyDynArray_NewWithSize(deallocator, _PyDynArray_DEFAULT_SIZE); +} + +/* + * Get an item from the array. This cannot fail. + * + * If the index is not valid, this is undefined behavior. + */ +static inline void * +_PyDynArray_GET_ITEM(_PyDynArray *array, Py_ssize_t index) +{ + _PyDynArray_ASSERT_VALID(array); + _PyDynArray_ASSERT_INDEX(array, index); + return array->items[index]; +} + +/* + * Get the length of the array. This cannot fail. + */ +static inline Py_ssize_t +_PyDynArray_LENGTH(_PyDynArray *array) +{ + _PyDynArray_ASSERT_VALID(array); + return array->length; +} + +/* + * Pop the item at the end the array. + * This function cannot fail. + */ +static inline void * +_PyDynArray_PopTop(_PyDynArray *array) +{ + return _PyDynArray_Pop(array, _PyDynArray_LENGTH(array) - 1); +} + +#ifdef __cplusplus +} +#endif +#endif /* !Py_INTERNAL_DYNARRAY_H */ diff --git a/Makefile.pre.in b/Makefile.pre.in index 07c8a4d20142db..1b0f12840f2773 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -435,6 +435,7 @@ PYTHON_OBJS= \ Python/critical_section.o \ Python/crossinterp.o \ Python/dynamic_annotations.o \ + Python/dynarray.o \ Python/errors.o \ Python/flowgraph.o \ Python/frame.o \ @@ -1197,6 +1198,7 @@ PYTHON_HEADERS= \ $(srcdir)/Include/internal/pycore_dict.h \ $(srcdir)/Include/internal/pycore_dict_state.h \ $(srcdir)/Include/internal/pycore_dtoa.h \ + $(srcdir)/Include/internal/pycore_dynarray.h \ $(srcdir)/Include/internal/pycore_exceptions.h \ $(srcdir)/Include/internal/pycore_faulthandler.h \ $(srcdir)/Include/internal/pycore_fileutils.h \ diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index c403075fbb2501..2c25026d9e293d 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -17,6 +17,7 @@ #include "pycore_compile.h" // _PyCompile_CodeGen() #include "pycore_context.h" // _PyContext_NewHamtForTests() #include "pycore_dict.h" // _PyManagedDictPointer_GetValues() +#include "pycore_dynarray.h" // _PyDynArray #include "pycore_fileutils.h" // _Py_normpath() #include "pycore_flowgraph.h" // _PyCompile_OptimizeCfg() #include "pycore_frame.h" // _PyInterpreterFrame @@ -2048,6 +2049,176 @@ identify_type_slot_wrappers(PyObject *self, PyObject *Py_UNUSED(ignored)) return _PyType_GetSlotWrapperNames(); } +static int +test_dynarray_common(_PyDynArray *array) +{ +#define APPEND(ptr) do { \ + if (_PyDynArray_Append(array, ptr) < 0) \ + { \ + return -1; \ + } \ +} while (0); + + // Some dummy pointers + APPEND(Py_None); + APPEND(Py_True); + APPEND(Py_False); + + // Make sure that indexes work + assert(_PyDynArray_GET_ITEM(array, 0) == Py_None); + assert(_PyDynArray_GET_ITEM(array, 1) == Py_True); + assert(_PyDynArray_GET_ITEM(array, 2) == Py_False); + + // Now, let's make sure that resizing works. + for (int i = 0; i < (_PyDynArray_DEFAULT_SIZE * 2); ++i) + { + APPEND(NULL); + } + + assert(_PyDynArray_PopTop(array) == NULL); + APPEND((void *) 42); + assert(_PyDynArray_PopTop(array) == (void *) 42); + +#undef APPEND + + // Make sure that nothing got corrupted + assert(_PyDynArray_GET_ITEM(array, 0) == Py_None); + assert(_PyDynArray_GET_ITEM(array, 1) == Py_True); + assert(_PyDynArray_GET_ITEM(array, 2) == Py_False); + + // Test removal + _PyDynArray_Remove(array, 0); + assert(_PyDynArray_GET_ITEM(array, 0) == Py_True); + assert(_PyDynArray_GET_ITEM(array, 1) == Py_False); + + Py_ssize_t index = _PyDynArray_DEFAULT_SIZE; + // Test setting values + _PyDynArray_Set(array, index, (void *) 1); + assert(_PyDynArray_GET_ITEM(array, index) == (void *) 1); + + // We already tested PopTop, but it doesn't hurt to test this one. + assert(_PyDynArray_Pop(array, 0) == Py_True); + + // Test insertion + _PyDynArray_Insert(array, 1, (void *) 99); + assert(_PyDynArray_GET_ITEM(array, 1) == (void *) 99); + + // Resizing with insertion + for (Py_ssize_t i = 0; i < 20; ++i) + { + _PyDynArray_Insert(array, 2, (void *) i); + assert(_PyDynArray_GET_ITEM(array, 2) == (void *) i); + } + + assert(_PyDynArray_GET_ITEM(array, 5) == (void *) 16); + assert(_PyDynArray_GET_ITEM(array, 4) == (void *) 17); + assert(_PyDynArray_GET_ITEM(array, 3) == (void *) 18); + assert(_PyDynArray_GET_ITEM(array, 2) == (void *) 19); + + return 0; +} + +static int +test_heap_array(_PyDynArray *array) +{ +#define DO(item, operation) \ + do { \ + if (item == NULL) \ + { \ + _PyDynArray_Free(array); \ + PyErr_NoMemory(); \ + return -1; \ + } \ + if (operation < 0) \ + { \ + PyMem_Free(item); \ + _PyDynArray_Free(array); \ + PyErr_NoMemory(); \ + return -1; \ + }; \ + } while (0) + +#define SILLY_STRING "My hovercraft is full of eels" + // Heap append + char *my_string = PyMem_Malloc(sizeof(SILLY_STRING)); + DO(my_string, _PyDynArray_Append(array, my_string)); + strcpy(my_string, SILLY_STRING); + + // Heap insertion + int *other_ptr = PyMem_Malloc(sizeof(int)); + DO(other_ptr, _PyDynArray_Insert(array, 0, other_ptr)); + *other_ptr = 42; + assert(_PyDynArray_GET_ITEM(array, 0) == other_ptr); + + // Make sure our other allocation is still alive + assert(!strcmp(_PyDynArray_GET_ITEM(array, 1), SILLY_STRING)); + + // Heap pop + char *popped = _PyDynArray_PopTop(array); + assert(!strcmp(popped, my_string)); + PyMem_Free(popped); + + // A lot of heap appends. This is mainly + // so leak tests pick this up if deallocation + // wasn't happening. + for (int i = 0; i < 15; ++i) + { + int *ptr = PyMem_Malloc(sizeof(int)); + DO(ptr, _PyDynArray_Append(array, ptr)); + *ptr = i; + } + + // Heap removal + _PyDynArray_Remove(array, 10); +#undef SILLY_STRING +#undef DO + + return 0; +} + +static PyObject * +test_dynarray(PyObject *self, PyObject *unused) +{ + _PyDynArray array; + // In a loop to make sure that reinitialization works + for (int i = 0; i < 3; ++i) + { + if (_PyDynArray_Init(&array, NULL) < 0) + { + PyErr_NoMemory(); + return NULL; + } + + if (test_dynarray_common(&array) < 0) + { + PyErr_NoMemory(); + _PyDynArray_Clear(&array); + return NULL; + } + + _PyDynArray_Clear(&array); + } + + // Array allocated on the heap + _PyDynArray *heap_array = _PyDynArray_New(NULL); + if (test_dynarray_common(heap_array) < 0) + { + _PyDynArray_Free(heap_array); + PyErr_NoMemory(); + return NULL; + } + _PyDynArray_Free(heap_array); + + // Still on the heap, but now the fields are too + _PyDynArray *array_with_deallocator = _PyDynArray_New(PyMem_Free); + if (array_with_deallocator == NULL) + { + PyErr_NoMemory(); + return NULL; + } + + return test_heap_array(array_with_deallocator) < 0 ? NULL : Py_None; +} static PyMethodDef module_functions[] = { {"get_configs", get_configs, METH_NOARGS}, @@ -2145,6 +2316,7 @@ static PyMethodDef module_functions[] = { GH_119213_GETARGS_METHODDEF {"get_static_builtin_types", get_static_builtin_types, METH_NOARGS}, {"identify_type_slot_wrappers", identify_type_slot_wrappers, METH_NOARGS}, + {"test_dynarray", test_dynarray, METH_NOARGS}, {NULL, NULL} /* sentinel */ }; diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj index a3c2d32c454e04..71433924c80675 100644 --- a/PCbuild/_freeze_module.vcxproj +++ b/PCbuild/_freeze_module.vcxproj @@ -201,6 +201,7 @@ + diff --git a/PCbuild/_freeze_module.vcxproj.filters b/PCbuild/_freeze_module.vcxproj.filters index 91b1d75fb8df5e..9c1e29c9f38bfe 100644 --- a/PCbuild/_freeze_module.vcxproj.filters +++ b/PCbuild/_freeze_module.vcxproj.filters @@ -130,6 +130,9 @@ Source Files + + Source Files + Source Files diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj index 3b33c6bf6bb91d..8caeeaca519ac5 100644 --- a/PCbuild/pythoncore.vcxproj +++ b/PCbuild/pythoncore.vcxproj @@ -231,6 +231,7 @@ + @@ -587,6 +588,7 @@ + diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters index ee2930b10439a9..c6b24d4106617c 100644 --- a/PCbuild/pythoncore.vcxproj.filters +++ b/PCbuild/pythoncore.vcxproj.filters @@ -615,6 +615,9 @@ Include\internal + + Include\internal + Include\internal @@ -1316,6 +1319,9 @@ Python + + Python + Python diff --git a/Python/dynarray.c b/Python/dynarray.c new file mode 100644 index 00000000000000..de5b1517224169 --- /dev/null +++ b/Python/dynarray.c @@ -0,0 +1,150 @@ +/* + * Dynamic array implementation. + */ + +#include "pycore_dynarray.h" +#include "pycore_pymem.h" // _PyMem_RawStrdup + +static inline void +call_deallocator_maybe(_PyDynArray *array, Py_ssize_t index) +{ + if (array->deallocator != NULL && array->items[index] != NULL) + { + array->deallocator(array->items[index]); + } +} + +int +_PyDynArray_InitWithSize(_PyDynArray *array, + _PyDynArray_Deallocator deallocator, + Py_ssize_t initial) +{ + assert(array != NULL); + assert(initial > 0); + void **items = PyMem_RawCalloc(sizeof(void *), initial); + if (items == NULL) + { + return -1; + } + + array->capacity = initial; + array->items = items; + array->length = 0; + array->deallocator = deallocator; + + return 0; +} + +static int +resize_if_needed(_PyDynArray *array) +{ + if (array->length == array->capacity) + { + // Need to resize + array->capacity *= 2; + void **new_items = PyMem_RawRealloc( + array->items, + sizeof(void *) * array->capacity); + if (new_items == NULL) + { + return -1; + } + + array->items = new_items; + } + + return 0; +} + +int +_PyDynArray_Append(_PyDynArray *array, void *item) +{ + _PyDynArray_ASSERT_VALID(array); + array->items[array->length++] = item; + if (resize_if_needed(array) < 0) + { + array->items[--array->length] = NULL; + return -1; + } + return 0; +} + +int +_PyDynArray_Insert(_PyDynArray *array, Py_ssize_t index, void *item) +{ + _PyDynArray_ASSERT_VALID(array); + _PyDynArray_ASSERT_INDEX(array, index); + ++array->length; + if (resize_if_needed(array) < 0) + { + // Grow the array beforehand, otherwise it's + // going to be a mess putting it back together if + // allocation fails. + --array->length; + return -1; + } + + for (Py_ssize_t i = array->length - 1; i > index; --i) + { + array->items[i] = array->items[i - 1]; + } + + array->items[index] = item; + return 0; +} + +void +_PyDynArray_Set(_PyDynArray *array, Py_ssize_t index, void *item) +{ + _PyDynArray_ASSERT_VALID(array); + _PyDynArray_ASSERT_INDEX(array, index); + call_deallocator_maybe(array, index); + array->items[index] = item; +} + +static void +remove_no_dealloc(_PyDynArray *array, Py_ssize_t index) +{ + for (Py_ssize_t i = index; i < array->length - 1; ++i) + { + array->items[i] = array->items[i + 1]; + } + --array->length; +} + +void +_PyDynArray_Remove(_PyDynArray *array, Py_ssize_t index) +{ + _PyDynArray_ASSERT_VALID(array); + _PyDynArray_ASSERT_INDEX(array, index); + call_deallocator_maybe(array, index); + remove_no_dealloc(array, index); +} + +void * +_PyDynArray_Pop(_PyDynArray *array, Py_ssize_t index) +{ + _PyDynArray_ASSERT_VALID(array); + _PyDynArray_ASSERT_INDEX(array, index); + void *item = array->items[index]; + remove_no_dealloc(array, index); + return item; +} + +void +_PyDynArray_Clear(_PyDynArray *array) +{ + _PyDynArray_ASSERT_VALID(array); + for (Py_ssize_t i = 0; i < array->length; ++i) + { + call_deallocator_maybe(array, i); + } + PyMem_RawFree(array->items); + + // It would be nice if others could reuse the allocation for another + // dynarray later, so clear all the fields. + array->items = NULL; + array->length = 0; + array->capacity = 0; + array->deallocator = NULL; +}