summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/release/upcoming_changes/17582.new_feature.rst2
-rw-r--r--doc/source/reference/c-api/array.rst7
-rw-r--r--doc/source/reference/c-api/data_memory.rst37
-rw-r--r--doc/source/reference/global_state.rst10
-rw-r--r--numpy/core/src/multiarray/arrayobject.c11
-rw-r--r--numpy/core/tests/test_mem_policy.py49
6 files changed, 105 insertions, 11 deletions
diff --git a/doc/release/upcoming_changes/17582.new_feature.rst b/doc/release/upcoming_changes/17582.new_feature.rst
index be5997dda..c2426330c 100644
--- a/doc/release/upcoming_changes/17582.new_feature.rst
+++ b/doc/release/upcoming_changes/17582.new_feature.rst
@@ -3,6 +3,8 @@ NEP 49 configurable allocators
As detailed in `NEP 49`_, the function used for allocation of the data segment
of a ndarray can be changed. The policy can be set globally or in a context.
For more information see the NEP and the :ref:`data_memory` reference docs.
+Also add a ``NUMPY_WARN_IF_NO_MEM_POLICY`` override to warn on dangerous use
+of transfering ownership by setting ``NPY_ARRAY_OWNDATA``.
.. _`NEP 49`: https://numpy.org/neps/nep-0049.html
diff --git a/doc/source/reference/c-api/array.rst b/doc/source/reference/c-api/array.rst
index 6a135fd71..232690486 100644
--- a/doc/source/reference/c-api/array.rst
+++ b/doc/source/reference/c-api/array.rst
@@ -325,8 +325,7 @@ From scratch
should be increased after the pointer is passed in, and the base member
of the returned ndarray should point to the Python object that owns
the data. This will ensure that the provided memory is not
- freed while the returned array is in existence. To free memory as soon
- as the ndarray is deallocated, set the OWNDATA flag on the returned ndarray.
+ freed while the returned array is in existence.
.. c:function:: PyObject* PyArray_SimpleNewFromDescr( \
int nd, npy_int const* dims, PyArray_Descr* descr)
@@ -1463,7 +1462,9 @@ of the constant names is deprecated in 1.7.
.. c:macro:: NPY_ARRAY_OWNDATA
- The data area is owned by this array.
+ The data area is owned by this array. Should never be set manually, instead
+ create a ``PyObject`` wrapping the data and set the array's base to that
+ object. For an example, see the test in ``test_mem_policy``.
.. c:macro:: NPY_ARRAY_ALIGNED
diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst
index c17f98a2c..11a37adc4 100644
--- a/doc/source/reference/c-api/data_memory.rst
+++ b/doc/source/reference/c-api/data_memory.rst
@@ -119,3 +119,40 @@ For an example of setting up and using the PyDataMem_Handler, see the test in
operations that might cause new allocation events (such as the
creation/destruction numpy objects, or creating/destroying Python
objects which might cause a gc)
+
+What happens when deallocating if there is no policy set
+--------------------------------------------------------
+
+A rare but useful technique is to allocate a buffer outside NumPy, use
+:c:func:`PyArray_NewFromDescr` to wrap the buffer in a ``ndarray``, then switch
+the ``OWNDATA`` flag to true. When the ``ndarray`` is released, the
+appropriate function from the ``ndarray``'s ``PyDataMem_Handler`` should be
+called to free the buffer. But the ``PyDataMem_Handler`` field was never set,
+it will be ``NULL``. For backward compatibility, NumPy will call ``free()`` to
+release the buffer. If ``NUMPY_WARN_IF_NO_MEM_POLICY`` is set to ``1``, a
+warning will be emitted. The current default is not to emit a warning, this may
+change in a future version of NumPy.
+
+A better technique would be to use a ``PyCapsule`` as a base object:
+
+.. code-block:: c
+
+ /* define a PyCapsule_Destructor, using the correct deallocator for buff */
+ void free_wrap(void *capsule){
+ void * obj = PyCapsule_GetPointer(capsule, PyCapsule_GetName(capsule));
+ free(obj);
+ };
+
+ /* then inside the function that creates arr from buff */
+ ...
+ arr = PyArray_NewFromDescr(... buf, ...);
+ if (arr == NULL) {
+ return NULL;
+ }
+ capsule = PyCapsule_New(buf, "my_wrapped_buffer",
+ (PyCapsule_Destructor)&free_wrap);
+ if (PyArray_SetBaseObject(arr, capsule) == -1) {
+ Py_DECREF(arr);
+ return NULL;
+ }
+ ...
diff --git a/doc/source/reference/global_state.rst b/doc/source/reference/global_state.rst
index f18481235..20874ceaa 100644
--- a/doc/source/reference/global_state.rst
+++ b/doc/source/reference/global_state.rst
@@ -84,3 +84,13 @@ contiguous in memory.
Most users will have no reason to change these; for details
see the :ref:`memory layout <memory-layout>` documentation.
+
+Warn if no memory allocation policy when deallocating data
+----------------------------------------------------------
+
+Some users might pass ownership of the data pointer to the ``ndarray`` by
+setting the ``OWNDATA`` flag. If they do this without setting (manually) a
+memory allocation policy, the default will be to call ``free``. If
+``NUMPY_WARN_IF_NO_MEM_POLICY`` is set to ``"1"``, a ``RuntimeWarning`` will
+be emitted. A better alternative is to use a ``PyCapsule`` with a deallocator
+and set the ``ndarray.base``.
diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c
index c3615f95f..237fb9b72 100644
--- a/numpy/core/src/multiarray/arrayobject.c
+++ b/numpy/core/src/multiarray/arrayobject.c
@@ -502,10 +502,13 @@ array_dealloc(PyArrayObject *self)
nbytes = fa->descr->elsize ? fa->descr->elsize : 1;
}
if (fa->mem_handler == NULL) {
- char const * msg = "Trying to dealloc data, but a memory policy "
- "is not set. If you take ownership of the data, you must "
- "also set a memory policy.";
- WARN_IN_DEALLOC(PyExc_RuntimeWarning, msg);
+ char *env = getenv("NUMPY_WARN_IF_NO_MEM_POLICY");
+ if ((env == NULL) || (strncmp(env, "1", 1) == 0)) {
+ char const * msg = "Trying to dealloc data, but a memory policy "
+ "is not set. If you take ownership of the data, you must "
+ "set a base owning the data (e.g. a PyCapsule).";
+ WARN_IN_DEALLOC(PyExc_RuntimeWarning, msg);
+ }
// Guess at malloc/free ???
free(fa->data);
} else {
diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py
index a2ff98ceb..4ec9b3b11 100644
--- a/numpy/core/tests/test_mem_policy.py
+++ b/numpy/core/tests/test_mem_policy.py
@@ -1,5 +1,6 @@
import asyncio
import gc
+import os
import pytest
import numpy as np
import threading
@@ -61,6 +62,29 @@ def get_module(tmp_path):
// PyArray_BASE(PyArrayObject *)args) = NULL;
Py_RETURN_NONE;
"""),
+ ("get_array_with_base", "METH_NOARGS", """
+ char *buf = (char *)malloc(20);
+ npy_intp dims[1];
+ dims[0] = 20;
+ PyArray_Descr *descr = PyArray_DescrNewFromType(NPY_UINT8);
+ PyObject *arr = PyArray_NewFromDescr(&PyArray_Type, descr, 1, dims,
+ NULL, buf,
+ NPY_ARRAY_WRITEABLE, NULL);
+ if (arr == NULL) return NULL;
+ PyObject *obj = PyCapsule_New(buf, "buf capsule",
+ (PyCapsule_Destructor)&warn_on_free);
+ if (obj == NULL) {
+ Py_DECREF(arr);
+ return NULL;
+ }
+ if (PyArray_SetBaseObject((PyArrayObject *)arr, obj) < 0) {
+ Py_DECREF(arr);
+ Py_DECREF(obj);
+ return NULL;
+ }
+ return arr;
+
+ """),
]
prologue = '''
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
@@ -163,6 +187,12 @@ def get_module(tmp_path):
shift_free /* free */
}
};
+ void warn_on_free(void *capsule) {
+ PyErr_WarnEx(PyExc_UserWarning, "in warn_on_free", 1);
+ void * obj = PyCapsule_GetPointer(capsule,
+ PyCapsule_GetName(capsule));
+ free(obj);
+ };
'''
more_init = "import_array();"
try:
@@ -331,10 +361,21 @@ def test_switch_owner(get_module):
a = get_module.get_array()
assert np.core.multiarray.get_handler_name(a) is None
get_module.set_own(a)
- with warnings.catch_warnings():
- warnings.filterwarnings('always')
- # The policy should be NULL, so we have to assume we can call "free"
+ oldval = os.environ.get('NUMPY_WARN_IF_NO_MEM_POLICY', None)
+ os.environ['NUMPY_WARN_IF_NO_MEM_POLICY'] = "1"
+ try:
+ # The policy should be NULL, so we have to assume we can call
+ # "free"
with assert_warns(RuntimeWarning) as w:
del a
gc.collect()
- print(w)
+ finally:
+ if oldval is None:
+ os.environ.pop('NUMPY_WARN_IF_NO_MEM_POLICY')
+ else:
+ os.environ['NUMPY_WARN_IF_NO_MEM_POLICY'] = oldval
+
+ a = get_module.get_array_with_base()
+ with pytest.warns(UserWarning, match='warn_on_free'):
+ del a
+ gc.collect()