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;
+}