8000 ENH,API: Store exported buffer info on the array by seberg · Pull Request #16938 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content

ENH,API: Store exported buffer info on the array #16938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions doc/release/upcoming_changes/16938.c_api.rst
Original file line number Diff line number Diff line change
@@ -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.

24 changes: 24 additions & 0 deletions doc/source/reference/c-api/types-and-structures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -92,6 +94,7 @@ PyArray_Type and PyArrayObject
PyArray_Descr *descr;
int flags;
PyObject *weakreflist;
/* version dependend private members */
} PyArrayObject;

.. c:macro:: PyObject_HEAD
Expand Down Expand Up @@ -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
-----------------------------------
Expand Down
1 change: 1 addition & 0 deletions numpy/core/include/numpy/arrayscalars.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion numpy/core/include/numpy/ndarraytypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ typedef struct tagPyArrayObject_fields {
int flags;
/* For weak references */
PyObject *weakreflist;
void *_buffer_info; /* private buffer info, tagged to allow warning */
} PyArrayObject_fields;

/*
Expand All @@ -720,7 +721,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.
*/


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was wrong with this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing as such, but it is a trap, and most code that uses it, such as:

struct MySubclass {
    char space_for_ndarray[NPY_SIZEOF_PYARRAYOBJECT];
    int myslot;
}

is probably problematic and most likely not be binary compatible with all numpy versions. So it gives a false sense security?

685C

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why, it is defined at compile time just as before. The API is changed anyway, so apps compiled against 1.20 are not guaranteed to be backwards compatible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And code that extends NumPy internals has never been guaranteed to be forward compatible.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, this is only interesting for forward compatibility with possible future size changes. I would have a faint hope that it may help someone realize that since their module doesn't compile on 1.20+, it has to be modified even if you compile it with an earlier version to run in 1.20+...

I agree, we probably never promised not to modify struct sizes in this way, we have certainly done with many structs (aside from ndarray itself).

The first version here, I replaced the macro with PyArray_Type.tp_basicsize to make it a runtime constan, which is safe and will still fail if used for static sizes. (It is also probably a pretty useless macro, but...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But someone who uses this macro currently will incorrectly believe that their code is forward compatible. They are even avoiding the "deprecated API" to ensure binary compatibility with a potential NumPy release messing with the structs internals.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just rename it then? Put an underscore on it or something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least just comment it out with an explanation of why it is done. The first place people will look for an explanation of trouble will be in the include file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let me make this macro a static assert as Matti posted. Even if a compiler should not support it the compiler error will point to some text regarding to what is going on...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, although, I am not sure that actually compiles well always (i.e. without super confusing things). The stack-overflow posts seem to a be a bit unsure about it. Some people seem to suggest the weird:

static int assert_NPY_SIZEOF_PYARRAYOBJECT_was_removed_because_it_is_misleading_see_1_20_Release_Notes[-1]

instead, which is weird, hmmmpf.

/* Array Flags Object */
typedef struct PyArrayFlagsObject {
Expand Down
37 changes: 37 additions & 0 deletions numpy/core/src/multiarray/_multiarray_tests.c.src
1053D
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ IsPythonScalar(PyObject * dummy, PyObject *args)

#include "npy_pycompat.h"


/** Function to test calling via ctypes */
EXPORT(void*) forward_pointer(void *x)
{
Expand Down Expand Up @@ -684,6 +685,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)
Expand Down Expand Up @@ -2158,6 +2192,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},
Expand Down
6 changes: 4 additions & 2 deletions numpy/core/src/multiarray/arrayobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
Loading
0