summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian Berg <sebastian@sipsolutions.net>2020-11-26 14:43:58 -0600
committerGitHub <noreply@github.com>2020-11-26 13:43:58 -0700
commitaf4b110273346e5969fba04365bc477747529fe2 (patch)
tree66d192f2ea9b56521716bc5cab0f48dd89f1a248
parentd4ba8edb36eb8f272274d835294533eb00b0e57a (diff)
downloadnumpy-af4b110273346e5969fba04365bc477747529fe2.tar.gz
ENH,API: Store exported buffer info on the array (#16938)
* ENH,API: Store exported buffer info on the array This speeds up array deallocation and buffer exports, since it removes the need to global dictionary lookups. It also somewhat simplifies the logic. The main advantage is prossibly less the speedup itself (which is not large compared to most things that happen in the livetime of an array), but rather that no unnecessary work is done for shortlived arrays, which never export a buffer. The downside of this approach is that the ABI changes for anyone who would be subclassing ndarray in C. * MAINT: Do not tag the NULL (no buffers exported) The allocation is not the right place to initialize to anything but NULL, so take the easy path and do not tag the NULL default. * TST: Add test for best try RuntimeError on corrupt buffer-info * Remove NPY_SIZEOF_PYARRAYOBJECT and add some documentation * Use 3 to tag the pointer and object for a "bad" one in the test * DEP: deprecate the NPY_SIZEOF_PYARRAYOBJECT macro * Tune down matti's deprecation to write the error instead. * Tweak macro, so that clang hopefully doesn't complain. * Use None instead of NULL in PyErr_WriteUnraisable, pypy seems to have a bug with it * Just comment it out... * Apply suggestions from code review Co-authored-by: Matti Picus <matti.picus@gmail.com> Co-authored-by: mattip <matti.picus@gmail.com>
-rw-r--r--doc/release/upcoming_changes/16938.c_api.rst19
-rw-r--r--doc/source/reference/c-api/types-and-structures.rst24
-rw-r--r--numpy/core/include/numpy/arrayscalars.h1
-rw-r--r--numpy/core/include/numpy/ndarraytypes.h14
-rw-r--r--numpy/core/src/multiarray/_multiarray_tests.c.src37
-rw-r--r--numpy/core/src/multiarray/arrayobject.c6
-rw-r--r--numpy/core/src/multiarray/buffer.c364
-rw-r--r--numpy/core/src/multiarray/ctors.c2
-rw-r--r--numpy/core/src/multiarray/methods.c2
-rw-r--r--numpy/core/src/multiarray/npy_buffer.h4
-rw-r--r--numpy/core/src/multiarray/scalartypes.c.src11
-rw-r--r--numpy/core/tests/test_multiarray.py19
12 files changed, 296 insertions, 207 deletions
diff --git a/doc/release/upcoming_changes/16938.c_api.rst b/doc/release/upcoming_changes/16938.c_api.rst
new file mode 100644
index 000000000..aff72c8e5
--- /dev/null
+++ b/doc/release/upcoming_changes/16938.c_api.rst
@@ -0,0 +1,19 @@
+Size of ``np.ndarray`` and ``np.void_`` changed
+-----------------------------------------------
+The size of the ``PyArrayObject`` and ``PyVoidScalarObject``
+structures have changed. The following header definition has been
+removed::
+
+ #define NPY_SIZEOF_PYARRAYOBJECT (sizeof(PyArrayObject_fields))
+
+since the size must not be considered a compile time constant: it will
+change for different runtime versions of NumPy.
+
+The most likely relevant use are potential subclasses written in C which
+will have to be recompiled and should be updated. Please see the
+documentation for :c:type:`PyArrayObject` for more details and contact
+the NumPy developers if you are affected by this change.
+
+NumPy will attempt to give a graceful error but a program expecting a
+fixed structure size may have undefined behaviour and likely crash.
+
diff --git a/doc/source/reference/c-api/types-and-structures.rst b/doc/source/reference/c-api/types-and-structures.rst
index 6a9c4a9cf..763f985a6 100644
--- a/doc/source/reference/c-api/types-and-structures.rst
+++ b/doc/source/reference/c-api/types-and-structures.rst
@@ -79,6 +79,8 @@ PyArray_Type and PyArrayObject
of :c:type:`NPY_AO` (deprecated) which is defined to be equivalent to
:c:type:`PyArrayObject`. Direct access to the struct fields are
deprecated. Use the ``PyArray_*(arr)`` form instead.
+ As of NumPy 1.20, the size of this struct is not considered part of
+ the NumPy ABI (see note at the end of the member list).
.. code-block:: c
@@ -92,6 +94,7 @@ PyArray_Type and PyArrayObject
PyArray_Descr *descr;
int flags;
PyObject *weakreflist;
+ /* version dependend private members */
} PyArrayObject;
.. c:macro:: PyObject_HEAD
@@ -173,6 +176,27 @@ PyArray_Type and PyArrayObject
This member allows array objects to have weak references (using the
weakref module).
+ .. note::
+
+ Further members are considered private and version dependend. If the size
+ of the struct is important for your code, special care must be taken.
+ A possible use-case when this is relevant is subclassing in C.
+ If your code relies on ``sizeof(PyArrayObject)`` to be constant,
+ you must add the following check at import time:
+
+ .. code-block:: c
+
+ if (sizeof(PyArrayObject) < PyArray_Type.tp_basicsize) {
+ PyErr_SetString(PyExc_ImportError,
+ "Binary incompatibility with NumPy, must recompile/update X.");
+ return NULL;
+ }
+
+ To ensure that your code does not have to be compiled for a specific
+ NumPy version, you may add a constant, leaving room for changes in NumPy.
+ A solution guaranteed to be compatible with any future NumPy version
+ requires the use of a runtime calculate offset and allocation size.
+
PyArrayDescr_Type and PyArray_Descr
-----------------------------------
diff --git a/numpy/core/include/numpy/arrayscalars.h b/numpy/core/include/numpy/arrayscalars.h
index b282a2cd4..14a31988f 100644
--- a/numpy/core/include/numpy/arrayscalars.h
+++ b/numpy/core/include/numpy/arrayscalars.h
@@ -149,6 +149,7 @@ typedef struct {
PyArray_Descr *descr;
int flags;
PyObject *base;
+ void *_buffer_info; /* private buffer info, tagged to allow warning */
} PyVoidScalarObject;
/* Macros
diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h
index 75e9519fe..63e8bf974 100644
--- a/numpy/core/include/numpy/ndarraytypes.h
+++ b/numpy/core/include/numpy/ndarraytypes.h
@@ -709,6 +709,7 @@ typedef struct tagPyArrayObject_fields {
int flags;
/* For weak references */
PyObject *weakreflist;
+ void *_buffer_info; /* private buffer info, tagged to allow warning */
} PyArrayObject_fields;
/*
@@ -728,7 +729,18 @@ typedef struct tagPyArrayObject {
} PyArrayObject;
#endif
-#define NPY_SIZEOF_PYARRAYOBJECT (sizeof(PyArrayObject_fields))
+/*
+ * Removed 2020-Nov-25, NumPy 1.20
+ * #define NPY_SIZEOF_PYARRAYOBJECT (sizeof(PyArrayObject_fields))
+ *
+ * The above macro was removed as it gave a false sense of a stable ABI
+ * with respect to the structures size. If you require a runtime constant,
+ * you can use `PyArray_Type.tp_basicsize` instead. Otherwise, please
+ * see the PyArrayObject documentation or ask the NumPy developers for
+ * information on how to correctly replace the macro in a way that is
+ * compatible with multiple NumPy versions.
+ */
+
/* Array Flags Object */
typedef struct PyArrayFlagsObject {
diff --git a/numpy/core/src/multiarray/_multiarray_tests.c.src b/numpy/core/src/multiarray/_multiarray_tests.c.src
index 58bb76950..3811e87a8 100644
--- a/numpy/core/src/multiarray/_multiarray_tests.c.src
+++ b/numpy/core/src/multiarray/_multiarray_tests.c.src
@@ -37,6 +37,7 @@ IsPythonScalar(PyObject * dummy, PyObject *args)
#include "npy_pycompat.h"
+
/** Function to test calling via ctypes */
EXPORT(void*) forward_pointer(void *x)
{
@@ -685,6 +686,39 @@ create_custom_field_dtype(PyObject *NPY_UNUSED(mod), PyObject *args)
}
+PyObject *
+corrupt_or_fix_bufferinfo(PyObject *dummy, PyObject *obj)
+{
+ void **buffer_info_ptr;
+ if (PyArray_Check(obj)) {
+ buffer_info_ptr = &((PyArrayObject_fields *)obj)->_buffer_info;
+ }
+ else if (PyArray_IsScalar(obj, Void)) {
+ buffer_info_ptr = &((PyVoidScalarObject *)obj)->_buffer_info;
+ }
+ else {
+ PyErr_SetString(PyExc_TypeError,
+ "argument must be an array or void scalar");
+ return NULL;
+ }
+ if (*buffer_info_ptr == NULL) {
+ /* set to an invalid value (as a subclass might accidentally) */
+ *buffer_info_ptr = obj;
+ assert(((uintptr_t)obj & 7) == 0);
+ }
+ else if (*buffer_info_ptr == obj) {
+ /* Reset to a NULL (good value) */
+ *buffer_info_ptr = NULL;
+ }
+ else {
+ PyErr_SetString(PyExc_TypeError,
+ "buffer was already exported, this test doesn't support that");
+ return NULL;
+ }
+ Py_RETURN_NONE;
+}
+
+
/* check no elison for avoided increfs */
static PyObject *
incref_elide(PyObject *dummy, PyObject *args)
@@ -2244,6 +2278,9 @@ static PyMethodDef Multiarray_TestsMethods[] = {
{"create_custom_field_dtype",
create_custom_field_dtype,
METH_VARARGS, NULL},
+ {"corrupt_or_fix_bufferinfo",
+ corrupt_or_fix_bufferinfo,
+ METH_O, NULL},
{"incref_elide",
incref_elide,
METH_VARARGS, NULL},
diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c
index 5da1b5f29..a2474d79f 100644
--- a/numpy/core/src/multiarray/arrayobject.c
+++ b/numpy/core/src/multiarray/arrayobject.c
@@ -434,7 +434,9 @@ array_dealloc(PyArrayObject *self)
{
PyArrayObject_fields *fa = (PyArrayObject_fields *)self;
- _dealloc_cached_buffer_info((PyObject*)self);
+ if (_buffer_info_free(fa->_buffer_info, (PyObject *)self) < 0) {
+ PyErr_WriteUnraisable(NULL);
+ }
if (fa->weakreflist != NULL) {
PyObject_ClearWeakRefs((PyObject *)self);
@@ -1745,7 +1747,7 @@ array_free(PyObject * v)
NPY_NO_EXPORT PyTypeObject PyArray_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "numpy.ndarray",
- .tp_basicsize = NPY_SIZEOF_PYARRAYOBJECT,
+ .tp_basicsize = sizeof(PyArrayObject_fields),
/* methods */
.tp_dealloc = (destructor)array_dealloc,
.tp_repr = (reprfunc)array_repr,
diff --git a/numpy/core/src/multiarray/buffer.c b/numpy/core/src/multiarray/buffer.c
index 1f4f676ba..813850224 100644
--- a/numpy/core/src/multiarray/buffer.c
+++ b/numpy/core/src/multiarray/buffer.c
@@ -428,31 +428,23 @@ _buffer_format_string(PyArray_Descr *descr, _tmp_string_t *str,
/*
- * Global information about all active buffers
+ * Information about all active buffers is stored as a linked list on
+ * the ndarray. The initial pointer is currently tagged to have a chance of
+ * detecting incompatible subclasses.
*
* Note: because for backward compatibility we cannot define bf_releasebuffer,
* we must manually keep track of the additional data required by the buffers.
*/
/* Additional per-array data required for providing the buffer interface */
-typedef struct {
+typedef struct _buffer_info_t_tag {
char *format;
int ndim;
Py_ssize_t *strides;
Py_ssize_t *shape;
+ struct _buffer_info_t_tag *next;
} _buffer_info_t;
-/*
- * { id(array): [list of pointers to _buffer_info_t, the last one is latest] }
- *
- * Because shape, strides, and format can be different for different buffers,
- * we may need to keep track of multiple buffer infos for each array.
- *
- * However, when none of them has changed, the same buffer info may be reused.
- *
- * Thread-safety is provided by GIL.
- */
-static PyObject *_buffer_info_cache = NULL;
/* Fill in the info structure */
static _buffer_info_t*
@@ -564,6 +556,7 @@ _buffer_info_new(PyObject *obj, int flags)
Py_DECREF(descr);
info->format = NULL;
}
+ info->next = NULL;
return info;
fail:
@@ -596,145 +589,161 @@ _buffer_info_cmp(_buffer_info_t *a, _buffer_info_t *b)
return 0;
}
-static void
-_buffer_info_free(_buffer_info_t *info)
-{
- if (info->format) {
- PyObject_Free(info->format);
- }
- PyObject_Free(info);
-}
-/* Get buffer info from the global dictionary */
-static _buffer_info_t*
-_buffer_get_info(PyObject *obj, int flags)
+/*
+ * Tag the buffer info pointer by adding 2 (unless it is NULL to simplify
+ * object initialization).
+ * The linked list of buffer-infos was appended to the array struct in
+ * NumPy 1.20. Tagging the pointer gives us a chance to raise/print
+ * a useful error message instead of crashing hard if a C-subclass uses
+ * the same field.
+ */
+static NPY_INLINE void *
+buffer_info_tag(void *buffer_info)
{
- PyObject *key = NULL, *item_list = NULL, *item = NULL;
- _buffer_info_t *info = NULL, *old_info = NULL;
-
- if (_buffer_info_cache == NULL) {
- _buffer_info_cache = PyDict_New();
- if (_buffer_info_cache == NULL) {
- return NULL;
- }
+ if (buffer_info == NULL) {
+ return buffer_info;
}
-
- /* Compute information */
- info = _buffer_info_new(obj, flags);
- if (info == NULL) {
- return NULL;
+ else {
+ return (void *)((uintptr_t)buffer_info + 3);
}
+}
- /* Check if it is identical with an old one; reuse old one, if yes */
- key = PyLong_FromVoidPtr((void*)obj);
- if (key == NULL) {
- goto fail;
- }
- item_list = PyDict_GetItem(_buffer_info_cache, key);
-
- if (item_list != NULL) {
- Py_ssize_t item_list_length = PyList_GET_SIZE(item_list);
- Py_INCREF(item_list);
- if (item_list_length > 0) {
- item = PyList_GetItem(item_list, item_list_length - 1);
- old_info = (_buffer_info_t*)PyLong_AsVoidPtr(item);
- if (_buffer_info_cmp(info, old_info) != 0) {
- old_info = NULL; /* Can't use this one, but possibly next */
-
- if (item_list_length > 1 && info->ndim > 1) {
- /*
- * Some arrays are C- and F-contiguous and if they have more
- * than one dimension, the buffer-info may differ between
- * the two due to RELAXED_STRIDES_CHECKING.
- * If we export both buffers, the first stored one may be
- * the one for the other contiguity, so check both.
- * This is generally very unlikely in all other cases, since
- * in all other cases the first one will match unless array
- * metadata was modified in-place (which is discouraged).
- */
- item = PyList_GetItem(item_list, item_list_length - 2);
- old_info = (_buffer_info_t*)PyLong_AsVoidPtr(item);
- if (_buffer_info_cmp(info, old_info) != 0) {
- old_info = NULL;
- }
- }
- }
- if (old_info != NULL) {
- /*
- * The two info->format are considered equal if one of them
- * has no format set (meaning the format is arbitrary and can
- * be modified). If the new info has a format, but we reuse
- * the old one, this transfers the ownership to the old one.
- */
- if (old_info->format == NULL) {
- old_info->format = info->format;
- info->format = NULL;
- }
- _buffer_info_free(info);
- info = old_info;
- }
- }
+static NPY_INLINE int
+_buffer_info_untag(
+ void *tagged_buffer_info, _buffer_info_t **buffer_info, PyObject *obj)
+{
+ if (tagged_buffer_info == NULL) {
+ *buffer_info = NULL;
+ return 0;
}
- else {
- item_list = PyList_New(0);
- if (item_list == NULL) {
- goto fail;
- }
- if (PyDict_SetItem(_buffer_info_cache, key, item_list) != 0) {
- goto fail;
- }
+ if (NPY_UNLIKELY(((uintptr_t)tagged_buffer_info & 0x7) != 3)) {
+ PyErr_Format(PyExc_RuntimeError,
+ "Object of type %S appears to be C subclassed NumPy array, "
+ "void scalar, or allocated in a non-standard way."
+ "NumPy reserves the right to change the size of these "
+ "structures. Projects are required to take this into account "
+ "by either recompiling against a specific NumPy version or "
+ "padding the struct and enforcing a maximum NumPy version.",
+ Py_TYPE(obj));
+ return -1;
}
+ *buffer_info = (void *)((uintptr_t)tagged_buffer_info - 3);
+ return 0;
+}
- if (info != old_info) {
- /* Needs insertion */
- item = PyLong_FromVoidPtr((void*)info);
- if (item == NULL) {
- goto fail;
+
+/*
+ * NOTE: for backward compatibility (esp. with PyArg_ParseTuple("s#", ...))
+ * we do *not* define bf_releasebuffer at all.
+ *
+ * Instead, any extra data allocated with the buffer is released only in
+ * array_dealloc.
+ *
+ * Ensuring that the buffer stays in place is taken care by refcounting;
+ * ndarrays do not reallocate if there are references to them, and a buffer
+ * view holds one reference.
+ *
+ * This is stored in the array's _buffer_info slot (currently as a void *).
+ */
+static void
+_buffer_info_free_untagged(void *_buffer_info)
+{
+ _buffer_info_t *next = _buffer_info;
+ while (next != NULL) {
+ _buffer_info_t *curr = next;
+ next = curr->next;
+ if (curr->format) {
+ PyObject_Free(curr->format);
}
- PyList_Append(item_list, item);
- Py_DECREF(item);
+ /* Shape is allocated as part of info */
+ PyObject_Free(curr);
}
+}
- Py_DECREF(item_list);
- Py_DECREF(key);
- return info;
-fail:
- if (info != NULL && info != old_info) {
- _buffer_info_free(info);
+/*
+ * Checks whether the pointer is tagged, and then frees the cache list.
+ * (The tag check is only for transition due to changed structure size in 1.20)
+ */
+NPY_NO_EXPORT int
+_buffer_info_free(void *buffer_info, PyObject *obj)
+{
+ _buffer_info_t *untagged_buffer_info;
+ if (_buffer_info_untag(buffer_info, &untagged_buffer_info, obj) < 0) {
+ return -1;
}
- Py_XDECREF(item_list);
- Py_XDECREF(key);
- return NULL;
+ _buffer_info_free_untagged(untagged_buffer_info);
+ return 0;
}
-/* Clear buffer info from the global dictionary */
-static void
-_buffer_clear_info(PyObject *arr)
+
+/*
+ * Get the buffer info returning either the old one (passed in) or a new
+ * buffer info which adds holds on to (and thus replaces) the old one.
+ */
+static _buffer_info_t*
+_buffer_get_info(void **buffer_info_cache_ptr, PyObject *obj, int flags)
{
- PyObject *key, *item_list, *item;
- _buffer_info_t *info;
- int k;
+ _buffer_info_t *info = NULL;
+ _buffer_info_t *stored_info; /* First currently stored buffer info */
+
+ if (_buffer_info_untag(*buffer_info_cache_ptr, &stored_info, obj) < 0) {
+ return NULL;
+ }
+ _buffer_info_t *old_info = stored_info;
- if (_buffer_info_cache == NULL) {
- return;
+ /* Compute information (it would be nice to skip this in simple cases) */
+ info = _buffer_info_new(obj, flags);
+ if (info == NULL) {
+ return NULL;
}
- key = PyLong_FromVoidPtr((void*)arr);
- item_list = PyDict_GetItem(_buffer_info_cache, key);
- if (item_list != NULL) {
- for (k = 0; k < PyList_GET_SIZE(item_list); ++k) {
- item = PyList_GET_ITEM(item_list, k);
- info = (_buffer_info_t*)PyLong_AsVoidPtr(item);
- _buffer_info_free(info);
+ if (old_info != NULL && _buffer_info_cmp(info, old_info) != 0) {
+ _buffer_info_t *next_info = old_info->next;
+ old_info = NULL; /* Can't use this one, but possibly next */
+
+ if (info->ndim > 1 && next_info != NULL) {
+ /*
+ * Some arrays are C- and F-contiguous and if they have more
+ * than one dimension, the buffer-info may differ between
+ * the two due to RELAXED_STRIDES_CHECKING.
+ * If we export both buffers, the first stored one may be
+ * the one for the other contiguity, so check both.
+ * This is generally very unlikely in all other cases, since
+ * in all other cases the first one will match unless array
+ * metadata was modified in-place (which is discouraged).
+ */
+ if (_buffer_info_cmp(info, next_info) == 0) {
+ old_info = next_info;
+ }
+ }
+ }
+ if (old_info != NULL) {
+ /*
+ * The two info->format are considered equal if one of them
+ * has no format set (meaning the format is arbitrary and can
+ * be modified). If the new info has a format, but we reuse
+ * the old one, this transfers the ownership to the old one.
+ */
+ if (old_info->format == NULL) {
+ old_info->format = info->format;
+ info->format = NULL;
}
- PyDict_DelItem(_buffer_info_cache, key);
+ _buffer_info_free_untagged(info);
+ info = old_info;
+ }
+ else {
+ /* Insert new info as first item in the linked buffer-info list. */
+ info->next = stored_info;
+ *buffer_info_cache_ptr = buffer_info_tag(info);
}
- Py_DECREF(key);
+ return info;
}
+
/*
* Retrieving buffers for ndarray
*/
@@ -779,8 +788,9 @@ array_getbuffer(PyObject *obj, Py_buffer *view, int flags)
goto fail;
}
- /* Fill in information */
- info = _buffer_get_info(obj, flags);
+ /* Fill in information (and add it to _buffer_info if necessary) */
+ info = _buffer_get_info(
+ &((PyArrayObject_fields *)self)->_buffer_info, obj, flags);
if (info == NULL) {
goto fail;
}
@@ -830,90 +840,48 @@ fail:
}
/*
- * Retrieving buffers for scalars
+ * Retrieving buffers for void scalar (which can contain any complex types),
+ * defined in buffer.c since it requires the complex format building logic.
*/
-int
+NPY_NO_EXPORT int
void_getbuffer(PyObject *self, Py_buffer *view, int flags)
{
- _buffer_info_t *info = NULL;
- PyArray_Descr *descr = NULL;
- int elsize;
+ PyVoidScalarObject *scalar = (PyVoidScalarObject *)self;
if (flags & PyBUF_WRITABLE) {
PyErr_SetString(PyExc_BufferError, "scalar buffer is readonly");
- goto fail;
- }
-
- /* Fill in information */
- info = _buffer_get_info(self, flags);
- if (info == NULL) {
- goto fail;
- }
-
- view->ndim = info->ndim;
- view->shape = info->shape;
- view->strides = info->strides;
-
- if ((flags & PyBUF_FORMAT) == PyBUF_FORMAT) {
- view->format = info->format;
- } else {
- view->format = NULL;
- }
-
- descr = PyArray_DescrFromScalar(self);
- view->buf = (void *)scalar_value(self, descr);
- elsize = descr->elsize;
- view->len = elsize;
- if (PyArray_IsScalar(self, Datetime) || PyArray_IsScalar(self, Timedelta)) {
- elsize = 1; /* descr->elsize,char is 8,'M', but we return 1,'B' */
+ return -1;
}
- view->itemsize = elsize;
-
- Py_DECREF(descr);
+ view->ndim = 0;
+ view->shape = NULL;
+ view->strides = NULL;
+ view->suboffsets = NULL;
+ view->len = scalar->descr->elsize;
+ view->itemsize = scalar->descr->elsize;
view->readonly = 1;
view->suboffsets = NULL;
- view->obj = self;
Py_INCREF(self);
- return 0;
-
-fail:
- view->obj = NULL;
- return -1;
-}
-
-/*
- * NOTE: for backward compatibility (esp. with PyArg_ParseTuple("s#", ...))
- * we do *not* define bf_releasebuffer at all.
- *
- * Instead, any extra data allocated with the buffer is released only in
- * array_dealloc.
- *
- * Ensuring that the buffer stays in place is taken care by refcounting;
- * ndarrays do not reallocate if there are references to them, and a buffer
- * view holds one reference.
- */
-
-NPY_NO_EXPORT void
-_dealloc_cached_buffer_info(PyObject *self)
-{
- int reset_error_state = 0;
- PyObject *ptype, *pvalue, *ptraceback;
-
- /* This function may be called when processing an exception --
- * we need to stash the error state to avoid confusing PyDict
- */
+ view->obj = self;
+ view->buf = scalar->obval;
- if (PyErr_Occurred()) {
- reset_error_state = 1;
- PyErr_Fetch(&ptype, &pvalue, &ptraceback);
+ if (((flags & PyBUF_FORMAT) != PyBUF_FORMAT)) {
+ /* It is unnecessary to find the correct format */
+ view->format = NULL;
+ return 0;
}
- _buffer_clear_info(self);
-
- if (reset_error_state) {
- PyErr_Restore(ptype, pvalue, ptraceback);
+ /*
+ * If a format is being exported, we need to use _buffer_get_info
+ * to find the correct format. This format must also be stored, since
+ * at least in theory it can change (in practice it should never change).
+ */
+ _buffer_info_t *info = _buffer_get_info(&scalar->_buffer_info, self, flags);
+ if (info == NULL) {
+ return -1;
}
+ view->format = info->format;
+ return 0;
}
diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c
index 2426076b9..f6031e370 100644
--- a/numpy/core/src/multiarray/ctors.c
+++ b/numpy/core/src/multiarray/ctors.c
@@ -756,9 +756,11 @@ PyArray_NewFromDescr_int(
Py_DECREF(descr);
return NULL;
}
+ fa->_buffer_info = NULL;
fa->nd = nd;
fa->dimensions = NULL;
fa->data = NULL;
+
if (data == NULL) {
fa->flags = NPY_ARRAY_DEFAULT;
if (flags) {
diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c
index 76df2337b..9c8bb4135 100644
--- a/numpy/core/src/multiarray/methods.c
+++ b/numpy/core/src/multiarray/methods.c
@@ -2180,7 +2180,7 @@ static PyObject *
array_sizeof(PyArrayObject *self)
{
/* object + dimension and strides */
- Py_ssize_t nbytes = NPY_SIZEOF_PYARRAYOBJECT +
+ Py_ssize_t nbytes = Py_TYPE(self)->tp_basicsize +
PyArray_NDIM(self) * sizeof(npy_intp) * 2;
if (PyArray_CHKFLAGS(self, NPY_ARRAY_OWNDATA)) {
nbytes += PyArray_NBYTES(self);
diff --git a/numpy/core/src/multiarray/npy_buffer.h b/numpy/core/src/multiarray/npy_buffer.h
index 5ff8b6c2c..d10f1a020 100644
--- a/numpy/core/src/multiarray/npy_buffer.h
+++ b/numpy/core/src/multiarray/npy_buffer.h
@@ -3,8 +3,8 @@
extern NPY_NO_EXPORT PyBufferProcs array_as_buffer;
-NPY_NO_EXPORT void
-_dealloc_cached_buffer_info(PyObject *self);
+NPY_NO_EXPORT int
+_buffer_info_free(void *buffer_info, PyObject *obj);
NPY_NO_EXPORT PyArray_Descr*
_descriptor_from_pep3118_format(char const *s);
diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src
index b8976d08f..d018fccbb 100644
--- a/numpy/core/src/multiarray/scalartypes.c.src
+++ b/numpy/core/src/multiarray/scalartypes.c.src
@@ -67,8 +67,11 @@ gentype_alloc(PyTypeObject *type, Py_ssize_t nitems)
const size_t size = _PyObject_VAR_SIZE(type, nitems + 1);
obj = (PyObject *)PyObject_Malloc(size);
+ if (obj == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
/*
- * Fixme. Need to check for no memory.
* If we don't need to zero memory, we could use
* PyObject_{New, NewVar} for this whole function.
*/
@@ -2596,16 +2599,18 @@ NPY_NO_EXPORT PyTypeObject PyGenericArrType_Type = {
.tp_basicsize = sizeof(PyObject),
};
+
static void
void_dealloc(PyVoidScalarObject *v)
{
- _dealloc_cached_buffer_info((PyObject *)v);
-
if (v->flags & NPY_ARRAY_OWNDATA) {
npy_free_cache(v->obval, Py_SIZE(v));
}
Py_XDECREF(v->descr);
Py_XDECREF(v->base);
+ if (_buffer_info_free(v->_buffer_info, (PyObject *)v) < 0) {
+ PyErr_WriteUnraisable(NULL);
+ }
Py_TYPE(v)->tp_free(v);
}
diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py
index 61806f99f..12306cbb8 100644
--- a/numpy/core/tests/test_multiarray.py
+++ b/numpy/core/tests/test_multiarray.py
@@ -7526,6 +7526,25 @@ class TestNewBufferProtocol:
f.a = 3
assert_equal(arr['a'], 3)
+ @pytest.mark.parametrize("obj", [np.ones(3), np.ones(1, dtype="i,i")[()]])
+ def test_error_if_stored_buffer_info_is_corrupted(self, obj):
+ """
+ If a user extends a NumPy array before 1.20 and then runs it
+ on NumPy 1.20+. A C-subclassed array might in theory modify
+ the new buffer-info field. This checks that an error is raised
+ if this happens (for buffer export), an error is written on delete.
+ This is a sanity check to help users transition to safe code, it
+ may be deleted at any point.
+ """
+ # corrupt buffer info:
+ _multiarray_tests.corrupt_or_fix_bufferinfo(obj)
+ name = type(obj)
+ with pytest.raises(RuntimeError,
+ match=f".*{name} appears to be C subclassed"):
+ memoryview(obj)
+ # Fix buffer info again before we delete (or we lose the memory)
+ _multiarray_tests.corrupt_or_fix_bufferinfo(obj)
+
class TestArrayAttributeDeletion: