diff --git a/doc/TESTS.rst.txt b/doc/TESTS.rst.txt index d048a4569d9c..0d8137f4a7e1 100644 --- a/doc/TESTS.rst.txt +++ b/doc/TESTS.rst.txt @@ -139,6 +139,21 @@ originally written without unit tests, there are still several modules that don't have tests yet. Please feel free to choose one of these modules and develop tests for it. +Using C code in tests +--------------------- + +NumPy exposes a rich :ref:`C-API` . These are tested using c-extension +modules written "as-if" they know nothing about the internals of NumPy, rather +using the official C-API interfaces only. Examples of such modules are tests +for a user-defined ``rational`` dtype in ``_rational_tests`` or the ufunc +machinery tests in ``_umath_tests`` which are part of the binary distribution. +Starting from version 1.21, you can also write snippets of C code in tests that +will be compiled locally into c-extension modules and loaded into python. + +.. currentmodule:: numpy.testing.extbuild + +.. autofunction:: build_and_import_extension + Labeling tests -------------- diff --git a/doc/neps/nep-0049.rst b/doc/neps/nep-0049.rst index 51a3f11b1be6..277351e3b30a 100644 --- a/doc/neps/nep-0049.rst +++ b/doc/neps/nep-0049.rst @@ -93,19 +93,21 @@ High level design Users who wish to change the NumPy data memory management routines will use :c:func:`PyDataMem_SetHandler`, which uses a :c:type:`PyDataMem_Handler` -structure to hold pointers to functions used to manage the data memory. +structure to hold pointers to functions used to manage the data memory. In +order to allow lifetime management of the ``context``, the structure is wrapped +in a ``PyCapsule``. Since a call to ``PyDataMem_SetHandler`` will change the default functions, but that function may be called during the lifetime of an ``ndarray`` object, each -``ndarray`` will carry with it the ``PyDataMem_Handler`` struct used at the -time of its instantiation, and these will be used to reallocate or free the -data memory of the instance. Internally NumPy may use ``memcpy`` or ``memset`` -on the pointer to the data memory. +``ndarray`` will carry with it the ``PyDataMem_Handler``-wrapped PyCapsule used +at the time of its instantiation, and these will be used to reallocate or free +the data memory of the instance. Internally NumPy may use ``memcpy`` or +``memset`` on the pointer to the data memory. The name of the handler will be exposed on the python level via a ``numpy.core.multiarray.get_handler_name(arr)`` function. If called as ``numpy.core.multiarray.get_handler_name()`` it will return the name of the -global handler that will be used to allocate data for the next new `ndarrray`. +handler that will be used to allocate data for the next new `ndarrray`. NumPy C-API functions ===================== @@ -150,20 +152,19 @@ NumPy C-API functions 15780_ and 15788_ but has not yet been resolved. When it is this NEP should be revisited. -.. c:function:: const PyDataMem_Handler * PyDataMem_SetHandler(PyDataMem_Handler *handler) +.. c:function:: PyObject * PyDataMem_SetHandler(PyObject *handler) Sets a new allocation policy. If the input value is ``NULL``, will reset - the policy to the default. Returns the previous policy, ``NULL`` if the - previous policy was the default. We wrap the user-provided functions + the policy to the default. Return the previous policy, or + return NULL if an error has occurred. We wrap the user-provided so they will still call the Python and NumPy memory management callback hooks. All the function pointers must be filled in, ``NULL`` is not accepted. -.. c:function:: const PyDataMem_Handler * PyDataMem_GetHandler(PyArrayObject *obj) +.. c:function:: const PyObject * PyDataMem_GetHandler() - Return the ``PyDataMem_Handler`` used by the - ``PyArrayObject``. If ``NULL``, return the handler - that will be used to allocate data for the next ``PyArrayObject``. + Return the current policy that will be used to allocate data for the + next ``PyArrayObject``. On failure, return ``NULL``. ``PyDataMem_Handler`` thread safety and lifetime ================================================ diff --git a/doc/source/reference/c-api/data_memory.rst b/doc/source/reference/c-api/data_memory.rst new file mode 100644 index 000000000000..8e29894035f3 --- /dev/null +++ b/doc/source/reference/c-api/data_memory.rst @@ -0,0 +1,119 @@ +Memory management in NumPy +========================== + +The `numpy.ndarray` is a python class. It requires additional memory allocations +to hold `numpy.ndarray.strides`, `numpy.ndarray.shape` and +`numpy.ndarray.data` attributes. These attributes are specially allocated +after creating the python object in `__new__`. The ``strides`` and +``shape`` are stored in a piece of memory allocated internally. + +The ``data`` allocation used to store the actual array values (which could be +pointers in the case of ``object`` arrays) can be very large, so NumPy has +provided interfaces to manage its allocation and release. This document details +how those interfaces work. + +Historical overview +------------------- + +Since version 1.7.0, NumPy has exposed a set of ``PyDataMem_*`` functions +(:c:func:`PyDataMem_NEW`, :c:func:`PyDataMem_FREE`, :c:func:`PyDataMem_RENEW`) +which are backed by `alloc`, `free`, `realloc` respectively. In that version +NumPy also exposed the `PyDataMem_EventHook` function described below, which +wrap the OS-level calls. + +Since those early days, Python also improved its memory management +capabilities, and began providing +various :ref:`management policies ` beginning in version +3.4. These routines are divided into a set of domains, each domain has a +:c:type:`PyMemAllocatorEx` structure of routines for memory management. Python also +added a `tracemalloc` module to trace calls to the various routines. These +tracking hooks were added to the NumPy ``PyDataMem_*`` routines. + +NumPy added a small cache of allocated memory in its internal +``npy_alloc_cache``, ``npy_alloc_cache_zero``, and ``npy_free_cache`` +functions. These wrap ``alloc``, ``alloc-and-memset(0)`` and ``free`` +respectively, but when ``npy_free_cache`` is called, it adds the pointer to a +short list of available blocks marked by size. These blocks can be re-used by +subsequent calls to ``npy_alloc*``, avoiding memory thrashing. + +Configurable memory routines in NumPy (NEP 49) +---------------------------------------------- + +Users may wish to override the internal data memory routines with ones of their +own. Since NumPy does not use the Python domain strategy to manage data memory, +it provides an alternative set of C-APIs to change memory routines. There are +no Python domain-wide strategies for large chunks of object data, so those are +less suited to NumPy's needs. User who wish to change the NumPy data memory +management routines can use :c:func:`PyDataMem_SetHandler`, which uses a +:c:type:`PyDataMem_Handler` structure to hold pointers to functions used to +manage the data memory. The calls are still wrapped by internal routines to +call :c:func:`PyTraceMalloc_Track`, :c:func:`PyTraceMalloc_Untrack`, and will +use the :c:func:`PyDataMem_EventHookFunc` mechanism. Since the functions may +change during the lifetime of the process, each ``ndarray`` carries with it the +functions used at the time of its instantiation, and these will be used to +reallocate or free the data memory of the instance. + +.. c:type:: PyDataMem_Handler + + A struct to hold function pointers used to manipulate memory + + .. code-block:: c + + typedef struct { + char name[128]; /* multiple of 64 to keep the struct aligned */ + PyDataMemAllocator allocator; + } PyDataMem_Handler; + + where the allocator structure is + + .. code-block:: c + + /* The declaration of free differs from PyMemAllocatorEx */ + typedef struct { + void *ctx; + void* (*malloc) (void *ctx, size_t size); + void* (*calloc) (void *ctx, size_t nelem, size_t elsize); + void* (*realloc) (void *ctx, void *ptr, size_t new_size); + void (*free) (void *ctx, void *ptr, size_t size); + } PyDataMemAllocator; + +.. c:function:: PyObject * PyDataMem_SetHandler(PyObject *handler) + + Set a new allocation policy. If the input value is ``NULL``, will reset the + policy to the default. Return the previous policy, or + return ``NULL`` if an error has occurred. We wrap the user-provided functions + so they will still call the python and numpy memory management callback + hooks. + +.. c:function:: PyObject * PyDataMem_GetHandler() + + Return the current policy that will be used to allocate data for the + next ``PyArrayObject``. On failure, return ``NULL``. + +For an example of setting up and using the PyDataMem_Handler, see the test in +:file:`numpy/core/tests/test_mem_policy.py` + +.. c:function:: void PyDataMem_EventHookFunc(void *inp, void *outp, size_t size, void *user_data); + + This function will be called during data memory manipulation + +.. c:function:: PyDataMem_EventHookFunc * PyDataMem_SetEventHook(PyDataMem_EventHookFunc *newhook, void *user_data, void **old_data) + + Sets the allocation event hook for numpy array data. + + Returns a pointer to the previous hook or ``NULL``. If old_data is + non-``NULL``, the previous user_data pointer will be copied to it. + + If not ``NULL``, hook will be called at the end of each ``PyDataMem_NEW/FREE/RENEW``: + + .. code-block:: c + + result = PyDataMem_NEW(size) -> (*hook)(NULL, result, size, user_data) + PyDataMem_FREE(ptr) -> (*hook)(ptr, NULL, 0, user_data) + result = PyDataMem_RENEW(ptr, size) -> (*hook)(ptr, result, size, user_data) + + When the hook is called, the GIL will be held by the calling + thread. The hook should be written to be reentrant, if it performs + operations that might cause new allocation events (such as the + creation/destruction numpy objects, or creating/destroying Python + objects which might cause a gc) diff --git a/doc/source/reference/c-api/index.rst b/doc/source/reference/c-api/index.rst index bb1ed154e9b0..6288ff33bc17 100644 --- a/doc/source/reference/c-api/index.rst +++ b/doc/source/reference/c-api/index.rst @@ -49,3 +49,4 @@ code. generalized-ufuncs coremath deprecations + data_memory diff --git a/numpy/core/_add_newdocs.py b/numpy/core/_add_newdocs.py index 7467be80fb36..94a4f472fc1d 100644 --- a/numpy/core/_add_newdocs.py +++ b/numpy/core/_add_newdocs.py @@ -4727,6 +4727,16 @@ and then throwing away the ufunc. """) +add_newdoc('numpy.core.multiarray', 'get_handler_name', + """ + get_handler_name(a: ndarray) -> str,None + + Return the name of the memory handler used by `a`. If not provided, return + the name of the memory handler that will be used to allocate data for the + next `ndarray` in this context. May return None if `a` does not own its + memory, in which case you can traverse ``a.base`` for a memory handler. + """) + add_newdoc('numpy.core.multiarray', '_set_madvise_hugepage', """ _set_madvise_hugepage(enabled: bool) -> bool diff --git a/numpy/core/code_generators/cversions.txt b/numpy/core/code_generators/cversions.txt index a02c7153a5c6..38ee4dac2f6f 100644 --- a/numpy/core/code_generators/cversions.txt +++ b/numpy/core/code_generators/cversions.txt @@ -56,5 +56,7 @@ # DType related API additions. # A new field was added to the end of PyArrayObject_fields. # Version 14 (NumPy 1.21) No change. -# Version 14 (NumPy 1.22) No change. 0x0000000e = 17a0f366e55ec05e5c5c149123478452 + +# Version 15 (NumPy 1.22) Configurable memory allocations +0x0000000f = 0c420aed67010594eb81f23ddfb02a88 diff --git a/numpy/core/code_generators/numpy_api.py b/numpy/core/code_generators/numpy_api.py index fbd3233680fa..3813c6ad7810 100644 --- a/numpy/core/code_generators/numpy_api.py +++ b/numpy/core/code_generators/numpy_api.py @@ -76,9 +76,9 @@ # End 1.6 API } -#define NPY_NUMUSERTYPES (*(int *)PyArray_API[6]) -#define PyBoolArrType_Type (*(PyTypeObject *)PyArray_API[7]) -#define _PyArrayScalar_BoolValues ((PyBoolScalarObject *)PyArray_API[8]) +# define NPY_NUMUSERTYPES (*(int *)PyArray_API[6]) +# define PyBoolArrType_Type (*(PyTypeObject *)PyArray_API[7]) +# define _PyArrayScalar_BoolValues ((PyBoolScalarObject *)PyArray_API[8]) multiarray_funcs_api = { 'PyArray_GetNDArrayCVersion': (0,), @@ -350,6 +350,9 @@ 'PyArray_ResolveWritebackIfCopy': (302,), 'PyArray_SetWritebackIfCopyBase': (303,), # End 1.14 API + 'PyDataMem_SetHandler': (304,), + 'PyDataMem_GetHandler': (305,), + # End 1.21 API } ufunc_types_api = { diff --git a/numpy/core/include/numpy/ndarraytypes.h b/numpy/core/include/numpy/ndarraytypes.h index 8d810fa644d5..80177e2bbfa0 100644 --- a/numpy/core/include/numpy/ndarraytypes.h +++ b/numpy/core/include/numpy/ndarraytypes.h @@ -355,12 +355,10 @@ struct NpyAuxData_tag { #define NPY_ERR(str) fprintf(stderr, #str); fflush(stderr); #define NPY_ERR2(str) fprintf(stderr, str); fflush(stderr); - /* - * Macros to define how array, and dimension/strides data is - * allocated. - */ - - /* Data buffer - PyDataMem_NEW/FREE/RENEW are in multiarraymodule.c */ +/* +* Macros to define how array, and dimension/strides data is +* allocated. These should be made private +*/ #define NPY_USE_PYMEM 1 @@ -666,6 +664,24 @@ typedef struct _arr_descr { PyObject *shape; /* a tuple */ } PyArray_ArrayDescr; +/* + * Memory handler structure for array data. + */ +/* The declaration of free differs from PyMemAllocatorEx */ +typedef struct { + void *ctx; + void* (*malloc) (void *ctx, size_t size); + void* (*calloc) (void *ctx, size_t nelem, size_t elsize); + void* (*realloc) (void *ctx, void *ptr, size_t new_size); + void (*free) (void *ctx, void *ptr, size_t size); +} PyDataMemAllocator; + +typedef struct { + char name[128]; /* multiple of 64 to keep the struct aligned */ + PyDataMemAllocator allocator; +} PyDataMem_Handler; + + /* * The main array object structure. * @@ -716,6 +732,10 @@ typedef struct tagPyArrayObject_fields { /* For weak references */ PyObject *weakreflist; void *_buffer_info; /* private buffer info, tagged to allow warning */ + /* + * For malloc/calloc/realloc/free per object + */ + PyObject *mem_handler; } PyArrayObject_fields; /* @@ -1659,6 +1679,12 @@ PyArray_CLEARFLAGS(PyArrayObject *arr, int flags) ((PyArrayObject_fields *)arr)->flags &= ~flags; } +static NPY_INLINE NPY_RETURNS_BORROWED_REF PyObject * +PyArray_HANDLER(PyArrayObject *arr) +{ + return ((PyArrayObject_fields *)arr)->mem_handler; +} + #define PyTypeNum_ISBOOL(type) ((type) == NPY_BOOL) #define PyTypeNum_ISUNSIGNED(type) (((type) == NPY_UBYTE) || \ diff --git a/numpy/core/multiarray.py b/numpy/core/multiarray.py index 154df6f4d7da..351cd3a1bbb5 100644 --- a/numpy/core/multiarray.py +++ b/numpy/core/multiarray.py @@ -31,8 +31,8 @@ 'count_nonzero', 'c_einsum', 'datetime_as_string', 'datetime_data', 'dot', 'dragon4_positional', 'dragon4_scientific', 'dtype', 'empty', 'empty_like', 'error', 'flagsobj', 'flatiter', 'format_longfloat', - 'frombuffer', 'fromfile', 'fromiter', 'fromstring', 'inner', - 'interp', 'interp_complex', 'is_busday', 'lexsort', + 'frombuffer', 'fromfile', 'fromiter', 'fromstring', 'get_handler_name', + 'inner', 'interp', 'interp_complex', 'is_busday', 'lexsort', 'matmul', 'may_share_memory', 'min_scalar_type', 'ndarray', 'nditer', 'nested_iters', 'normalize_axis_index', 'packbits', 'promote_types', 'putmask', 'ravel_multi_index', 'result_type', 'scalar', diff --git a/numpy/core/setup_common.py b/numpy/core/setup_common.py index 85c8f16d1e73..70e8fc89705a 100644 --- a/numpy/core/setup_common.py +++ b/numpy/core/setup_common.py @@ -43,8 +43,8 @@ # 0x0000000d - 1.19.x # 0x0000000e - 1.20.x # 0x0000000e - 1.21.x -# 0x0000000e - 1.22.x -C_API_VERSION = 0x0000000e +# 0x0000000f - 1.22.x +C_API_VERSION = 0x0000000f class MismatchCAPIWarning(Warning): pass diff --git a/numpy/core/src/multiarray/alloc.c b/numpy/core/src/multiarray/alloc.c index adb4ae1289e7..e4756264d12e 100644 --- a/numpy/core/src/multiarray/alloc.c +++ b/numpy/core/src/multiarray/alloc.c @@ -133,9 +133,10 @@ npy_alloc_cache(npy_uintp sz) /* zero initialized data, sz is number of bytes to allocate */ NPY_NO_EXPORT void * -npy_alloc_cache_zero(npy_uintp sz) +npy_alloc_cache_zero(size_t nmemb, size_t size) { void * p; + size_t sz = nmemb * size; NPY_BEGIN_THREADS_DEF; if (sz < NBUCKETS) { p = _npy_alloc_cache(sz, 1, NBUCKETS, datacache, &PyDataMem_NEW); @@ -145,7 +146,7 @@ npy_alloc_cache_zero(npy_uintp sz) return p; } NPY_BEGIN_THREADS; - p = PyDataMem_NEW_ZEROED(sz, 1); + p = PyDataMem_NEW_ZEROED(nmemb, size); NPY_END_THREADS; return p; } @@ -187,8 +188,8 @@ npy_free_cache_dim(void * p, npy_uintp sz) /* malloc/free/realloc hook */ -NPY_NO_EXPORT PyDataMem_EventHookFunc *_PyDataMem_eventhook; -NPY_NO_EXPORT void *_PyDataMem_eventhook_user_data; +NPY_NO_EXPORT PyDataMem_EventHookFunc *_PyDataMem_eventhook = NULL; +NPY_NO_EXPORT void *_PyDataMem_eventhook_user_data = NULL; /*NUMPY_API * Sets the allocation event hook for numpy array data. @@ -254,21 +255,21 @@ PyDataMem_NEW(size_t size) * Allocates zeroed memory for array data. */ NPY_NO_EXPORT void * -PyDataMem_NEW_ZEROED(size_t size, size_t elsize) +PyDataMem_NEW_ZEROED(size_t nmemb, size_t size) { void *result; - result = calloc(size, elsize); + result = calloc(nmemb, size); if (_PyDataMem_eventhook != NULL) { NPY_ALLOW_C_API_DEF NPY_ALLOW_C_API if (_PyDataMem_eventhook != NULL) { - (*_PyDataMem_eventhook)(NULL, result, size * elsize, + (*_PyDataMem_eventhook)(NULL, result, nmemb * size, _PyDataMem_eventhook_user_data); } NPY_DISABLE_C_API } - PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, size); + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, nmemb * size); return result; } @@ -316,3 +317,325 @@ PyDataMem_RENEW(void *ptr, size_t size) } return result; } + +// The default data mem allocator malloc routine does not make use of a ctx. +// It should be called only through PyDataMem_UserNEW +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void * +default_malloc(void *NPY_UNUSED(ctx), size_t size) +{ + return _npy_alloc_cache(size, 1, NBUCKETS, datacache, &malloc); +} + +// The default data mem allocator calloc routine does not make use of a ctx. +// It should be called only through PyDataMem_UserNEW_ZEROED +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void * +default_calloc(void *NPY_UNUSED(ctx), size_t nelem, size_t elsize) +{ + void * p; + size_t sz = nelem * elsize; + NPY_BEGIN_THREADS_DEF; + if (sz < NBUCKETS) { + p = _npy_alloc_cache(sz, 1, NBUCKETS, datacache, &malloc); + if (p) { + memset(p, 0, sz); + } + return p; + } + NPY_BEGIN_THREADS; + p = calloc(nelem, elsize); + NPY_END_THREADS; + return p; +} + +// The default data mem allocator realloc routine does not make use of a ctx. +// It should be called only through PyDataMem_UserRENEW +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void * +default_realloc(void *NPY_UNUSED(ctx), void *ptr, size_t new_size) +{ + return realloc(ptr, new_size); +} + +// The default data mem allocator free routine does not make use of a ctx. +// It should be called only through PyDataMem_UserFREE +// since itself does not handle eventhook and tracemalloc logic. +static NPY_INLINE void +default_free(void *NPY_UNUSED(ctx), void *ptr, size_t size) +{ + _npy_free_cache(ptr, size, NBUCKETS, datacache, &free); +} + +/* Memory handler global default */ +PyDataMem_Handler default_handler = { + "default_allocator", + { + NULL, /* ctx */ + default_malloc, /* malloc */ + default_calloc, /* calloc */ + default_realloc, /* realloc */ + default_free /* free */ + } +}; + +#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) +PyObject *current_handler; +#endif + +int uo_index=0; /* user_override index */ + +/* Wrappers for the default or any user-assigned PyDataMem_Handler */ + +NPY_NO_EXPORT void * +PyDataMem_UserNEW(size_t size, PyObject *mem_handler) +{ + void *result; + PyDataMem_Handler *handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); + if (handler == NULL) { + return NULL; + } + + assert(size != 0); + result = handler->allocator.malloc(handler->allocator.ctx, size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(NULL, result, size, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, size); + return result; +} + +NPY_NO_EXPORT void * +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyObject *mem_handler) +{ + void *result; + PyDataMem_Handler *handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); + if (handler == NULL) { + return NULL; + } + result = handler->allocator.calloc(handler->allocator.ctx, nmemb, size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(NULL, result, nmemb * size, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, nmemb * size); + return result; +} + +/* Similar to array_dealloc in arrayobject.c */ +static NPY_INLINE void +WARN_IN_FREE(PyObject* warning, const char * msg) { + if (PyErr_WarnEx(warning, msg, 1) < 0) { + PyObject * s; + + s = PyUnicode_FromString("PyDataMem_UserFREE"); + if (s) { + PyErr_WriteUnraisable(s); + Py_DECREF(s); + } + else { + PyErr_WriteUnraisable(Py_None); + } + } +} + + + +NPY_NO_EXPORT void +PyDataMem_UserFREE(void *ptr, size_t size, PyObject *mem_handler) +{ + PyDataMem_Handler *handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); + if (handler == NULL) { + WARN_IN_FREE(PyExc_RuntimeWarning, + "Could not get pointer to 'mem_handler' from PyCapsule"); + PyErr_Clear(); + return; + } + PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); + handler->allocator.free(handler->allocator.ctx, ptr, size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(ptr, NULL, 0, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } +} + +NPY_NO_EXPORT void * +PyDataMem_UserRENEW(void *ptr, size_t size, PyObject *mem_handler) +{ + void *result; + PyDataMem_Handler *handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); + if (handler == NULL) { + return NULL; + } + + assert(size != 0); + result = handler->allocator.realloc(handler->allocator.ctx, ptr, size); + if (result != ptr) { + PyTraceMalloc_Untrack(NPY_TRACE_DOMAIN, (npy_uintp)ptr); + } + PyTraceMalloc_Track(NPY_TRACE_DOMAIN, (npy_uintp)result, size); + if (_PyDataMem_eventhook != NULL) { + NPY_ALLOW_C_API_DEF + NPY_ALLOW_C_API + if (_PyDataMem_eventhook != NULL) { + (*_PyDataMem_eventhook)(ptr, result, size, + _PyDataMem_eventhook_user_data); + } + NPY_DISABLE_C_API + } + return result; +} + +/*NUMPY_API + * Set a new allocation policy. If the input value is NULL, will reset + * the policy to the default. Return the previous policy, or + * return NULL if an error has occurred. We wrap the user-provided + * functions so they will still call the python and numpy + * memory management callback hooks. + */ +NPY_NO_EXPORT PyObject * +PyDataMem_SetHandler(PyObject *handler) +{ + PyObject *old_handler; +#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) + PyObject *token; + if (PyContextVar_Get(current_handler, NULL, &old_handler)) { + return NULL; + } + if (handler == NULL) { + handler = PyCapsule_New(&default_handler, "mem_handler", NULL); + if (handler == NULL) { + return NULL; + } + } + else { + Py_INCREF(handler); + } + token = PyContextVar_Set(current_handler, handler); + Py_DECREF(handler); + if (token == NULL) { + Py_DECREF(old_handler); + return NULL; + } + Py_DECREF(token); + return old_handler; +#else + PyObject *p; + p = PyThreadState_GetDict(); + if (p == NULL) { + return NULL; + } + old_handler = PyDict_GetItemString(p, "current_allocator"); + if (old_handler == NULL) { + old_handler = PyCapsule_New(&default_handler, "mem_handler", NULL); + if (old_handler == NULL) { + return NULL; + } + } + else { + Py_INCREF(old_handler); + } + if (handler == NULL) { + handler = PyCapsule_New(&default_handler, "mem_handler", NULL); + if (handler == NULL) { + Py_DECREF(old_handler); + return NULL; + } + } + else { + Py_INCREF(handler); + } + const int error = PyDict_SetItemString(p, "current_allocator", handler); + Py_DECREF(handler); + if (error) { + Py_DECREF(old_handler); + return NULL; + } + return old_handler; +#endif +} + +/*NUMPY_API + * Return the policy that will be used to allocate data + * for the next PyArrayObject. On failure, return NULL. + */ +NPY_NO_EXPORT PyObject * +PyDataMem_GetHandler() +{ + PyObject *handler; +#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) + if (PyContextVar_Get(current_handler, NULL, &handler)) { + return NULL; + } + return handler; +#else + PyObject *p = PyThreadState_GetDict(); + if (p == NULL) { + return NULL; + } + handler = PyDict_GetItemString(p, "current_allocator"); + if (handler == NULL) { + handler = PyCapsule_New(&default_handler, "mem_handler", NULL); + if (handler == NULL) { + return NULL; + } + } + else { + Py_INCREF(handler); + } + return handler; +#endif +} + +NPY_NO_EXPORT PyObject * +get_handler_name(PyObject *NPY_UNUSED(self), PyObject *args) +{ + PyObject *arr=NULL; + if (!PyArg_ParseTuple(args, "|O:get_handler_name", &arr)) { + return NULL; + } + if (arr != NULL && !PyArray_Check(arr)) { + PyErr_SetString(PyExc_ValueError, "if supplied, argument must be an ndarray"); + return NULL; + } + PyObject *mem_handler; + PyDataMem_Handler *handler; + PyObject *name; + if (arr != NULL) { + mem_handler = PyArray_HANDLER((PyArrayObject *) arr); + if (mem_handler == NULL) { + Py_RETURN_NONE; + } + Py_INCREF(mem_handler); + } + else { + mem_handler = PyDataMem_GetHandler(); + if (mem_handler == NULL) { + return NULL; + } + } + handler = (PyDataMem_Handler *) PyCapsule_GetPointer(mem_handler, "mem_handler"); + if (handler == NULL) { + Py_DECREF(mem_handler); + return NULL; + } + name = PyUnicode_FromString(handler->name); + Py_DECREF(mem_handler); + return name; +} diff --git a/numpy/core/src/multiarray/alloc.h b/numpy/core/src/multiarray/alloc.h index 1259abca5b1b..4f7df1f846b3 100644 --- a/numpy/core/src/multiarray/alloc.h +++ b/numpy/core/src/multiarray/alloc.h @@ -11,13 +11,16 @@ NPY_NO_EXPORT PyObject * _set_madvise_hugepage(PyObject *NPY_UNUSED(self), PyObject *enabled_obj); NPY_NO_EXPORT void * -npy_alloc_cache(npy_uintp sz); +PyDataMem_UserNEW(npy_uintp sz, PyObject *mem_handler); NPY_NO_EXPORT void * -npy_alloc_cache_zero(npy_uintp sz); +PyDataMem_UserNEW_ZEROED(size_t nmemb, size_t size, PyObject *mem_handler); NPY_NO_EXPORT void -npy_free_cache(void * p, npy_uintp sd); +PyDataMem_UserFREE(void * p, npy_uintp sd, PyObject *mem_handler); + +NPY_NO_EXPORT void * +PyDataMem_UserRENEW(void *ptr, size_t size, PyObject *mem_handler); NPY_NO_EXPORT void * npy_alloc_cache_dim(npy_uintp sz); @@ -37,4 +40,12 @@ npy_free_cache_dim_array(PyArrayObject * arr) npy_free_cache_dim(PyArray_DIMS(arr), PyArray_NDIM(arr)); } +#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) +extern PyObject *current_handler; /* PyContextVar/PyCapsule */ +extern PyDataMem_Handler default_handler; +#endif + +NPY_NO_EXPORT PyObject * +get_handler_name(PyObject *NPY_UNUSED(self), PyObject *obj); + #endif /* NUMPY_CORE_SRC_MULTIARRAY_ALLOC_H_ */ diff --git a/numpy/core/src/multiarray/arrayobject.c b/numpy/core/src/multiarray/arrayobject.c index 28aff5d659cb..c3615f95fa99 100644 --- a/numpy/core/src/multiarray/arrayobject.c +++ b/numpy/core/src/multiarray/arrayobject.c @@ -493,7 +493,25 @@ array_dealloc(PyArrayObject *self) if (PyDataType_FLAGCHK(fa->descr, NPY_ITEM_REFCOUNT)) { PyArray_XDECREF(self); } - npy_free_cache(fa->data, PyArray_NBYTES(self)); + /* + * Allocation will never be 0, see comment in ctors.c + * line 820 + */ + size_t nbytes = PyArray_NBYTES(self); + if (nbytes == 0) { + 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); + // Guess at malloc/free ??? + free(fa->data); + } else { + PyDataMem_UserFREE(fa->data, nbytes, fa->mem_handler); + Py_DECREF(fa->mem_handler); + } } /* must match allocation in PyArray_NewFromDescr */ diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 15782a91b74c..9fe76845a3f9 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -3093,6 +3093,10 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (!PyArray_HASFIELDS(ap)) { return STRING_compare(ip1, ip2, ap); } + PyObject *mem_handler = PyDataMem_GetHandler(); + if (mem_handler == NULL) { + goto finish; + } descr = PyArray_DESCR(ap); /* * Compare on the first-field. If equal, then @@ -3107,15 +3111,19 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) if (_unpack_field(tup, &new, &offset) < 0) { goto finish; } - /* descr is the only field checked by compare or copyswap */ + /* Set the fields needed by compare or copyswap */ dummy_struct.descr = new; + swap = PyArray_ISBYTESWAPPED(dummy); nip1 = ip1 + offset; nip2 = ip2 + offset; if (swap || new->alignment > 1) { if (swap || !npy_is_aligned(nip1, new->alignment)) { - /* create buffer and copy */ - nip1 = npy_alloc_cache(new->elsize); + /* + * create temporary buffer and copy, + * always use the current handler for internal allocations + */ + nip1 = PyDataMem_UserNEW(new->elsize, mem_handler); if (nip1 == NULL) { goto finish; } @@ -3124,11 +3132,15 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) new->f->copyswap(nip1, NULL, swap, dummy); } if (swap || !npy_is_aligned(nip2, new->alignment)) { - /* create buffer and copy */ - nip2 = npy_alloc_cache(new->elsize); + /* + * create temporary buffer and copy, + * always use the current handler for internal allocations + */ + nip2 = PyDataMem_UserNEW(new->elsize, mem_handler); if (nip2 == NULL) { if (nip1 != ip1 + offset) { - npy_free_cache(nip1, new->elsize); + /* destroy temporary buffer */ + PyDataMem_UserFREE(nip1, new->elsize, mem_handler); } goto finish; } @@ -3140,10 +3152,12 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) res = new->f->compare(nip1, nip2, dummy); if (swap || new->alignment > 1) { if (nip1 != ip1 + offset) { - npy_free_cache(nip1, new->elsize); + /* destroy temporary buffer */ + PyDataMem_UserFREE(nip1, new->elsize, mem_handler); } if (nip2 != ip2 + offset) { - npy_free_cache(nip2, new->elsize); + /* destroy temporary buffer */ + PyDataMem_UserFREE(nip2, new->elsize, mem_handler); } } if (res != 0) { @@ -3152,6 +3166,7 @@ VOID_compare(char *ip1, char *ip2, PyArrayObject *ap) } finish: + Py_XDECREF(mem_handler); return res; } diff --git a/numpy/core/src/multiarray/convert_datatype.c b/numpy/core/src/multiarray/convert_datatype.c index eeadad374d85..3135d69894b3 100644 --- a/numpy/core/src/multiarray/convert_datatype.c +++ b/numpy/core/src/multiarray/convert_datatype.c @@ -2119,7 +2119,7 @@ PyArray_ObjectType(PyObject *op, int minimum_type) * This function is only used in one place within NumPy and should * generally be avoided. It is provided mainly for backward compatibility. * - * The user of the function has to free the returned array. + * The user of the function has to free the returned array with PyDataMem_FREE. */ NPY_NO_EXPORT PyArrayObject ** PyArray_ConvertToCommonType(PyObject *op, int *retn) diff --git a/numpy/core/src/multiarray/ctors.c b/numpy/core/src/multiarray/ctors.c index 9da75fb8a8c4..bb356991878b 100644 --- a/numpy/core/src/multiarray/ctors.c +++ b/numpy/core/src/multiarray/ctors.c @@ -726,6 +726,7 @@ PyArray_NewFromDescr_int( fa->nd = nd; fa->dimensions = NULL; fa->data = NULL; + fa->mem_handler = NULL; if (data == NULL) { fa->flags = NPY_ARRAY_DEFAULT; @@ -805,12 +806,19 @@ PyArray_NewFromDescr_int( fa->flags |= NPY_ARRAY_C_CONTIGUOUS|NPY_ARRAY_F_CONTIGUOUS; } + if (data == NULL) { + /* Store the handler in case the default is modified */ + fa->mem_handler = PyDataMem_GetHandler(); + if (fa->mem_handler == NULL) { + goto fail; + } /* * Allocate something even for zero-space arrays * e.g. shape=(0,) -- otherwise buffer exposure * (a.data) doesn't work as it should. * Could probably just allocate a few bytes here. -- Chuck + * Note: always sync this with calls to PyDataMem_UserFREE */ if (nbytes == 0) { nbytes = descr->elsize ? descr->elsize : 1; @@ -820,21 +828,23 @@ PyArray_NewFromDescr_int( * which could also be sub-fields of a VOID array */ if (zeroed || PyDataType_FLAGCHK(descr, NPY_NEEDS_INIT)) { - data = npy_alloc_cache_zero(nbytes); + data = PyDataMem_UserNEW_ZEROED(nbytes, 1, fa->mem_handler); } else { - data = npy_alloc_cache(nbytes); + data = PyDataMem_UserNEW(nbytes, fa->mem_handler); } if (data == NULL) { raise_memory_error(fa->nd, fa->dimensions, descr); goto fail; } + fa->flags |= NPY_ARRAY_OWNDATA; } else { + /* The handlers should never be called in this case */ + fa->mem_handler = NULL; /* - * If data is passed in, this object won't own it by default. - * Caller must arrange for this to be reset if truly desired + * If data is passed in, this object won't own it. */ fa->flags &= ~NPY_ARRAY_OWNDATA; } @@ -902,6 +912,7 @@ PyArray_NewFromDescr_int( return (PyObject *)fa; fail: + Py_XDECREF(fa->mem_handler); Py_DECREF(fa); return NULL; } @@ -3409,7 +3420,9 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre dptr += dtype->elsize; if (num < 0 && thisbuf == size) { totalbytes += bytes; - tmp = PyDataMem_RENEW(PyArray_DATA(r), totalbytes); + /* The handler is always valid */ + tmp = PyDataMem_UserRENEW(PyArray_DATA(r), totalbytes, + PyArray_HANDLER(r)); if (tmp == NULL) { err = 1; break; @@ -3431,7 +3444,9 @@ array_from_text(PyArray_Descr *dtype, npy_intp num, char const *sep, size_t *nre const size_t nsize = PyArray_MAX(*nread,1)*dtype->elsize; if (nsize != 0) { - tmp = PyDataMem_RENEW(PyArray_DATA(r), nsize); + /* The handler is always valid */ + tmp = PyDataMem_UserRENEW(PyArray_DATA(r), nsize, + PyArray_HANDLER(r)); if (tmp == NULL) { err = 1; } @@ -3536,7 +3551,9 @@ PyArray_FromFile(FILE *fp, PyArray_Descr *dtype, npy_intp num, char *sep) const size_t nsize = PyArray_MAX(nread,1) * dtype->elsize; char *tmp; - if ((tmp = PyDataMem_RENEW(PyArray_DATA(ret), nsize)) == NULL) { + /* The handler is always valid */ + if((tmp = PyDataMem_UserRENEW(PyArray_DATA(ret), nsize, + PyArray_HANDLER(ret))) == NULL) { Py_DECREF(dtype); Py_DECREF(ret); return PyErr_NoMemory(); @@ -3820,7 +3837,9 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) */ elcount = (i >> 1) + (i < 4 ? 4 : 2) + i; if (!npy_mul_with_overflow_intp(&nbytes, elcount, elsize)) { - new_data = PyDataMem_RENEW(PyArray_DATA(ret), nbytes); + /* The handler is always valid */ + new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), nbytes, + PyArray_HANDLER(ret)); } else { new_data = NULL; @@ -3858,10 +3877,12 @@ PyArray_FromIter(PyObject *obj, PyArray_Descr *dtype, npy_intp count) * (assuming realloc is reasonably good about reusing space...) */ if (i == 0 || elsize == 0) { - /* The size cannot be zero for PyDataMem_RENEW. */ + /* The size cannot be zero for realloc. */ goto done; } - new_data = PyDataMem_RENEW(PyArray_DATA(ret), i * elsize); + /* The handler is always valid */ + new_data = PyDataMem_UserRENEW(PyArray_DATA(ret), i * elsize, + PyArray_HANDLER(ret)); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate array memory"); diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index 2c8d1b3b4968..e81ca29471bc 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -384,7 +384,23 @@ array_data_set(PyArrayObject *self, PyObject *op, void *NPY_UNUSED(ignored)) } if (PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA) { PyArray_XDECREF(self); - PyDataMem_FREE(PyArray_DATA(self)); + size_t nbytes = PyArray_NBYTES(self); + /* + * Allocation will never be 0, see comment in ctors.c + * line 820 + */ + if (nbytes == 0) { + PyArray_Descr *dtype = PyArray_DESCR(self); + nbytes = dtype->elsize ? dtype->elsize : 1; + } + PyObject *handler = PyArray_HANDLER(self); + if (handler == NULL) { + /* This can happen if someone arbitrarily sets NPY_ARRAY_OWNDATA */ + PyErr_SetString(PyExc_RuntimeError, + "no memory handler found but OWNDATA flag set"); + return -1; + } + PyDataMem_UserFREE(PyArray_DATA(self), nbytes, handler); } if (PyArray_BASE(self)) { if ((PyArray_FLAGS(self) & NPY_ARRAY_WRITEBACKIFCOPY) || diff --git a/numpy/core/src/multiarray/item_selection.c b/numpy/core/src/multiarray/item_selection.c index 33d378c2b58f..086b674c809e 100644 --- a/numpy/core/src/multiarray/item_selection.c +++ b/numpy/core/src/multiarray/item_selection.c @@ -776,6 +776,7 @@ PyArray_Repeat(PyArrayObject *aop, PyObject *op, int axis) return NULL; } + /*NUMPY_API */ NPY_NO_EXPORT PyObject * @@ -907,7 +908,7 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out, Py_XDECREF(mps[i]); } Py_DECREF(ap); - npy_free_cache(mps, n * sizeof(mps[0])); + PyDataMem_FREE(mps); if (out != NULL && out != obj) { Py_INCREF(out); PyArray_ResolveWritebackIfCopy(obj); @@ -922,7 +923,7 @@ PyArray_Choose(PyArrayObject *ip, PyObject *op, PyArrayObject *out, Py_XDECREF(mps[i]); } Py_XDECREF(ap); - npy_free_cache(mps, n * sizeof(mps[0])); + PyDataMem_FREE(mps); PyArray_DiscardWritebackIfCopy(obj); Py_XDECREF(obj); return NULL; @@ -962,14 +963,19 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, return 0; } + PyObject *mem_handler = PyDataMem_GetHandler(); + if (mem_handler == NULL) { + return -1; + } it = (PyArrayIterObject *)PyArray_IterAllButAxis((PyObject *)op, &axis); if (it == NULL) { + Py_DECREF(mem_handler); return -1; } size = it->size; if (needcopy) { - buffer = npy_alloc_cache(N * elsize); + buffer = PyDataMem_UserNEW(N * elsize, mem_handler); if (buffer == NULL) { ret = -1; goto fail; @@ -1053,12 +1059,14 @@ _new_sortlike(PyArrayObject *op, int axis, PyArray_SortFunc *sort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - npy_free_cache(buffer, N * elsize); + /* cleanup internal buffer */ + PyDataMem_UserFREE(buffer, N * elsize, mem_handler); if (ret < 0 && !PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ PyErr_NoMemory(); } Py_DECREF(it); + Py_DECREF(mem_handler); return ret; } @@ -1090,11 +1098,16 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, NPY_BEGIN_THREADS_DEF; + PyObject *mem_handler = PyDataMem_GetHandler(); + if (mem_handler == NULL) { + return NULL; + } rop = (PyArrayObject *)PyArray_NewFromDescr( Py_TYPE(op), PyArray_DescrFromType(NPY_INTP), PyArray_NDIM(op), PyArray_DIMS(op), NULL, NULL, 0, (PyObject *)op); if (rop == NULL) { + Py_DECREF(mem_handler); return NULL; } rstride = PyArray_STRIDE(rop, axis); @@ -1102,6 +1115,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, /* Check if there is any argsorting to do */ if (N <= 1 || PyArray_SIZE(op) == 0) { + Py_DECREF(mem_handler); memset(PyArray_DATA(rop), 0, PyArray_NBYTES(rop)); return (PyObject *)rop; } @@ -1115,7 +1129,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, size = it->size; if (needcopy) { - valbuffer = npy_alloc_cache(N * elsize); + valbuffer = PyDataMem_UserNEW(N * elsize, mem_handler); if (valbuffer == NULL) { ret = -1; goto fail; @@ -1123,7 +1137,8 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, } if (needidxbuffer) { - idxbuffer = (npy_intp *)npy_alloc_cache(N * sizeof(npy_intp)); + idxbuffer = (npy_intp *)PyDataMem_UserNEW(N * sizeof(npy_intp), + mem_handler); if (idxbuffer == NULL) { ret = -1; goto fail; @@ -1212,8 +1227,9 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, fail: NPY_END_THREADS_DESCR(PyArray_DESCR(op)); - npy_free_cache(valbuffer, N * elsize); - npy_free_cache(idxbuffer, N * sizeof(npy_intp)); + /* cleanup internal buffers */ + PyDataMem_UserFREE(valbuffer, N * elsize, mem_handler); + PyDataMem_UserFREE(idxbuffer, N * sizeof(npy_intp), mem_handler); if (ret < 0) { if (!PyErr_Occurred()) { /* Out of memory during sorting or buffer creation */ @@ -1224,6 +1240,7 @@ _new_argsortlike(PyArrayObject *op, int axis, PyArray_ArgSortFunc *argsort, } Py_XDECREF(it); Py_XDECREF(rit); + Py_DECREF(mem_handler); return (PyObject *)rop; } diff --git a/numpy/core/src/multiarray/methods.c b/numpy/core/src/multiarray/methods.c index 391e65f6a55a..2d66c77dc4c1 100644 --- a/numpy/core/src/multiarray/methods.c +++ b/numpy/core/src/multiarray/methods.c @@ -1975,6 +1975,16 @@ array_setstate(PyArrayObject *self, PyObject *args) return NULL; } + /* + * Reassigning fa->descr messes with the reallocation strategy, + * since fa could be a 0-d or scalar, and then + * PyDataMem_UserFREE will be confused + */ + size_t n_tofree = PyArray_NBYTES(self); + if (n_tofree == 0) { + PyArray_Descr *dtype = PyArray_DESCR(self); + n_tofree = dtype->elsize ? dtype->elsize : 1; + } Py_XDECREF(PyArray_DESCR(self)); fa->descr = typecode; Py_INCREF(typecode); @@ -2041,7 +2051,18 @@ array_setstate(PyArrayObject *self, PyObject *args) } if ((PyArray_FLAGS(self) & NPY_ARRAY_OWNDATA)) { - PyDataMem_FREE(PyArray_DATA(self)); + /* + * Allocation will never be 0, see comment in ctors.c + * line 820 + */ + PyObject *handler = PyArray_HANDLER(self); + if (handler == NULL) { + /* This can happen if someone arbitrarily sets NPY_ARRAY_OWNDATA */ + PyErr_SetString(PyExc_RuntimeError, + "no memory handler found but OWNDATA flag set"); + return NULL; + } + PyDataMem_UserFREE(PyArray_DATA(self), n_tofree, handler); PyArray_CLEARFLAGS(self, NPY_ARRAY_OWNDATA); } Py_XDECREF(PyArray_BASE(self)); @@ -2077,7 +2098,6 @@ array_setstate(PyArrayObject *self, PyObject *args) if (!PyDataType_FLAGCHK(typecode, NPY_LIST_PICKLE)) { int swap = PyArray_ISBYTESWAPPED(self); - fa->data = datastr; /* Bytes should always be considered immutable, but we just grab the * pointer if they are large, to save memory. */ if (!IsAligned(self) || swap || (len <= 1000)) { @@ -2086,8 +2106,16 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); Py_RETURN_NONE; } - fa->data = PyDataMem_NEW(num); + /* Store the handler in case the default is modified */ + Py_XDECREF(fa->mem_handler); + fa->mem_handler = PyDataMem_GetHandler(); + if (fa->mem_handler == NULL) { + Py_DECREF(rawdata); + return NULL; + } + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(self)); if (PyArray_DATA(self) == NULL) { + Py_DECREF(fa->mem_handler); Py_DECREF(rawdata); return PyErr_NoMemory(); } @@ -2123,7 +2151,12 @@ array_setstate(PyArrayObject *self, PyObject *args) Py_DECREF(rawdata); } else { + /* The handlers should never be called in this case */ + Py_XDECREF(fa->mem_handler); + fa->mem_handler = NULL; + fa->data = datastr; if (PyArray_SetBaseObject(self, rawdata) < 0) { + Py_DECREF(rawdata); return NULL; } } @@ -2134,8 +2167,15 @@ array_setstate(PyArrayObject *self, PyObject *args) if (num == 0 || elsize == 0) { Py_RETURN_NONE; } - fa->data = PyDataMem_NEW(num); + /* Store the functions in case the default handler is modified */ + Py_XDECREF(fa->mem_handler); + fa->mem_handler = PyDataMem_GetHandler(); + if (fa->mem_handler == NULL) { + return NULL; + } + fa->data = PyDataMem_UserNEW(num, PyArray_HANDLER(self)); if (PyArray_DATA(self) == NULL) { + Py_DECREF(fa->mem_handler); return PyErr_NoMemory(); } if (PyDataType_FLAGCHK(PyArray_DESCR(self), NPY_NEEDS_INIT)) { @@ -2144,6 +2184,7 @@ array_setstate(PyArrayObject *self, PyObject *args) PyArray_ENABLEFLAGS(self, NPY_ARRAY_OWNDATA); fa->base = NULL; if (_setlist_pkl(self, rawdata) < 0) { + Py_DECREF(fa->mem_handler); return NULL; } } diff --git a/numpy/core/src/multiarray/multiarraymodule.c b/numpy/core/src/multiarray/multiarraymodule.c index d211f01bcc59..dea828ed95e0 100644 --- a/numpy/core/src/multiarray/multiarraymodule.c +++ b/numpy/core/src/multiarray/multiarraymodule.c @@ -4433,6 +4433,9 @@ static struct PyMethodDef array_module_methods[] = { {"geterrobj", (PyCFunction) ufunc_geterr, METH_VARARGS, NULL}, + {"get_handler_name", + (PyCFunction) get_handler_name, + METH_VARARGS, NULL}, {"_add_newdoc_ufunc", (PyCFunction)add_newdoc_ufunc, METH_VARARGS, NULL}, {"_get_sfloat_dtype", @@ -4910,6 +4913,20 @@ PyMODINIT_FUNC PyInit__multiarray_umath(void) { if (initumath(m) != 0) { goto err; } +#if (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x07030600) + /* + * Initialize the context-local PyDataMem_Handler capsule. + */ + c_api = PyCapsule_New(&default_handler, "mem_handler", NULL); + if (c_api == NULL) { + goto err; + } + current_handler = PyContextVar_New("current_allocator", c_api); + Py_DECREF(c_api); + if (current_handler == NULL) { + goto err; + } +#endif return m; err: diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index 56f17431af01..0d52211a86fa 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -34,6 +34,16 @@ #include "binop_override.h" +/* + * used for allocating a single scalar, so use the default numpy + * memory allocators instead of the (maybe) user overrides + */ +NPY_NO_EXPORT void * +npy_alloc_cache_zero(size_t nmemb, size_t size); + +NPY_NO_EXPORT void +npy_free_cache(void * p, npy_uintp sz); + NPY_NO_EXPORT PyBoolScalarObject _PyArrayScalar_BoolValues[] = { {PyObject_HEAD_INIT(&PyBoolArrType_Type) 0}, {PyObject_HEAD_INIT(&PyBoolArrType_Type) 1}, @@ -1321,7 +1331,7 @@ gentype_imag_get(PyObject *self, void *NPY_UNUSED(ignored)) int elsize; typecode = PyArray_DescrFromScalar(self); elsize = typecode->elsize; - temp = npy_alloc_cache_zero(elsize); + temp = npy_alloc_cache_zero(1, elsize); ret = PyArray_Scalar(temp, typecode, NULL); npy_free_cache(temp, elsize); } @@ -3151,7 +3161,10 @@ void_arrtype_new(PyTypeObject *type, PyObject *args, PyObject *kwds) (int) NPY_MAX_INT); return NULL; } - destptr = npy_alloc_cache_zero(memu); + if (memu == 0) { + memu = 1; + } + destptr = npy_alloc_cache_zero(memu, 1); if (destptr == NULL) { return PyErr_NoMemory(); } diff --git a/numpy/core/src/multiarray/shape.c b/numpy/core/src/multiarray/shape.c index 5a4e8c0f3e6a..162abd6a49c8 100644 --- a/numpy/core/src/multiarray/shape.c +++ b/numpy/core/src/multiarray/shape.c @@ -121,8 +121,16 @@ PyArray_Resize(PyArrayObject *self, PyArray_Dims *newshape, int refcheck, } /* Reallocate space if needed - allocating 0 is forbidden */ - new_data = PyDataMem_RENEW( - PyArray_DATA(self), newnbytes == 0 ? elsize : newnbytes); + PyObject *handler = PyArray_HANDLER(self); + if (handler == NULL) { + /* This can happen if someone arbitrarily sets NPY_ARRAY_OWNDATA */ + PyErr_SetString(PyExc_RuntimeError, + "no memory handler found but OWNDATA flag set"); + return NULL; + } + new_data = PyDataMem_UserRENEW(PyArray_DATA(self), + newnbytes == 0 ? elsize : newnbytes, + handler); if (new_data == NULL) { PyErr_SetString(PyExc_MemoryError, "cannot allocate memory for array"); diff --git a/numpy/core/tests/test_mem_policy.py b/numpy/core/tests/test_mem_policy.py new file mode 100644 index 000000000000..a2ff98ceb781 --- /dev/null +++ b/numpy/core/tests/test_mem_policy.py @@ -0,0 +1,340 @@ +import asyncio +import gc +import pytest +import numpy as np +import threading +import warnings +from numpy.testing import extbuild, assert_warns +import sys + + +@pytest.fixture +def get_module(tmp_path): + """ Add a memory policy that returns a false pointer 64 bytes into the + actual allocation, and fill the prefix with some text. Then check at each + memory manipulation that the prefix exists, to make sure all alloc/realloc/ + free/calloc go via the functions here. + """ + if sys.platform.startswith('cygwin'): + pytest.skip('link fails on cygwin') + functions = [ + ("set_secret_data_policy", "METH_NOARGS", """ + PyObject *secret_data = + PyCapsule_New(&secret_data_handler, "mem_handler", NULL); + if (secret_data == NULL) { + return NULL; + } + PyObject *old = PyDataMem_SetHandler(secret_data); + Py_DECREF(secret_data); + return old; + """), + ("set_old_policy", "METH_O", """ + PyObject *old; + if (args != NULL && PyCapsule_CheckExact(args)) { + old = PyDataMem_SetHandler(args); + } + else { + old = PyDataMem_SetHandler(NULL); + } + if (old == NULL) { + return NULL; + } + Py_DECREF(old); + Py_RETURN_NONE; + """), + ("get_array", "METH_NOARGS", """ + char *buf = (char *)malloc(20); + npy_intp dims[1]; + dims[0] = 20; + PyArray_Descr *descr = PyArray_DescrNewFromType(NPY_UINT8); + return PyArray_NewFromDescr(&PyArray_Type, descr, 1, dims, NULL, + buf, NPY_ARRAY_WRITEABLE, NULL); + """), + ("set_own", "METH_O", """ + if (!PyArray_Check(args)) { + PyErr_SetString(PyExc_ValueError, + "need an ndarray"); + return NULL; + } + PyArray_ENABLEFLAGS((PyArrayObject*)args, NPY_ARRAY_OWNDATA); + // Maybe try this too? + // PyArray_BASE(PyArrayObject *)args) = NULL; + Py_RETURN_NONE; + """), + ] + prologue = ''' + #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION + #include + /* + * This struct allows the dynamic configuration of the allocator funcs + * of the `secret_data_allocator`. It is provided here for + * demonstration purposes, as a valid `ctx` use-case scenario. + */ + typedef struct { + void *(*malloc)(size_t); + void *(*calloc)(size_t, size_t); + void *(*realloc)(void *, size_t); + void (*free)(void *); + } SecretDataAllocatorFuncs; + + NPY_NO_EXPORT void * + shift_alloc(void *ctx, size_t sz) { + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx; + char *real = (char *)funcs->malloc(sz + 64); + if (real == NULL) { + return NULL; + } + snprintf(real, 64, "originally allocated %ld", (unsigned long)sz); + return (void *)(real + 64); + } + NPY_NO_EXPORT void * + shift_zero(void *ctx, size_t sz, size_t cnt) { + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx; + char *real = (char *)funcs->calloc(sz + 64, cnt); + if (real == NULL) { + return NULL; + } + snprintf(real, 64, "originally allocated %ld via zero", + (unsigned long)sz); + return (void *)(real + 64); + } + NPY_NO_EXPORT void + shift_free(void *ctx, void * p, npy_uintp sz) { + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx; + if (p == NULL) { + return ; + } + char *real = (char *)p - 64; + if (strncmp(real, "originally allocated", 20) != 0) { + fprintf(stdout, "uh-oh, unmatched shift_free, " + "no appropriate prefix\\n"); + /* Make C runtime crash by calling free on the wrong address */ + funcs->free((char *)p + 10); + /* funcs->free(real); */ + } + else { + npy_uintp i = (npy_uintp)atoi(real +20); + if (i != sz) { + fprintf(stderr, "uh-oh, unmatched shift_free" + "(ptr, %ld) but allocated %ld\\n", sz, i); + /* This happens in some places, only print */ + funcs->free(real); + } + else { + funcs->free(real); + } + } + } + NPY_NO_EXPORT void * + shift_realloc(void *ctx, void * p, npy_uintp sz) { + SecretDataAllocatorFuncs *funcs = (SecretDataAllocatorFuncs *)ctx; + if (p != NULL) { + char *real = (char *)p - 64; + if (strncmp(real, "originally allocated", 20) != 0) { + fprintf(stdout, "uh-oh, unmatched shift_realloc\\n"); + return realloc(p, sz); + } + return (void *)((char *)funcs->realloc(real, sz + 64) + 64); + } + else { + char *real = (char *)funcs->realloc(p, sz + 64); + if (real == NULL) { + return NULL; + } + snprintf(real, 64, "originally allocated " + "%ld via realloc", (unsigned long)sz); + return (void *)(real + 64); + } + } + /* As an example, we use the standard {m|c|re}alloc/free funcs. */ + static SecretDataAllocatorFuncs secret_data_handler_ctx = { + malloc, + calloc, + realloc, + free + }; + static PyDataMem_Handler secret_data_handler = { + "secret_data_allocator", + { + &secret_data_handler_ctx, /* ctx */ + shift_alloc, /* malloc */ + shift_zero, /* calloc */ + shift_realloc, /* realloc */ + shift_free /* free */ + } + }; + ''' + more_init = "import_array();" + try: + import mem_policy + return mem_policy + except ImportError: + pass + # if it does not exist, build and load it + return extbuild.build_and_import_extension('mem_policy', + functions, + prologue=prologue, + include_dirs=[np.get_include()], + build_dir=tmp_path, + more_init=more_init) + + +def test_set_policy(get_module): + + get_handler_name = np.core.multiarray.get_handler_name + orig_policy_name = get_handler_name() + + a = np.arange(10).reshape((2, 5)) # a doesn't own its own data + assert get_handler_name(a) is None + assert get_handler_name(a.base) == orig_policy_name + + orig_policy = get_module.set_secret_data_policy() + + b = np.arange(10).reshape((2, 5)) # b doesn't own its own data + assert get_handler_name(b) is None + assert get_handler_name(b.base) == 'secret_data_allocator' + + if orig_policy_name == 'default_allocator': + get_module.set_old_policy(None) # tests PyDataMem_SetHandler(NULL) + assert get_handler_name() == 'default_allocator' + else: + get_module.set_old_policy(orig_policy) + assert get_handler_name() == orig_policy_name + + +def test_policy_propagation(get_module): + # The memory policy goes hand-in-hand with flags.owndata + + class MyArr(np.ndarray): + pass + + get_handler_name = np.core.multiarray.get_handler_name + orig_policy_name = get_handler_name() + a = np.arange(10).view(MyArr).reshape((2, 5)) + assert get_handler_name(a) is None + assert a.flags.owndata is False + + assert get_handler_name(a.base) is None + assert a.base.flags.owndata is False + + assert get_handler_name(a.base.base) == orig_policy_name + assert a.base.base.flags.owndata is True + + +async def concurrent_context1(get_module, orig_policy_name, event): + if orig_policy_name == 'default_allocator': + get_module.set_secret_data_policy() + assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' + else: + get_module.set_old_policy(None) + assert np.core.multiarray.get_handler_name() == 'default_allocator' + event.set() + + +async def concurrent_context2(get_module, orig_policy_name, event): + await event.wait() + # the policy is not affected by changes in parallel contexts + assert np.core.multiarray.get_handler_name() == orig_policy_name + # change policy in the child context + if orig_policy_name == 'default_allocator': + get_module.set_secret_data_policy() + assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' + else: + get_module.set_old_policy(None) + assert np.core.multiarray.get_handler_name() == 'default_allocator' + + +async def async_test_context_locality(get_module): + orig_policy_name = np.core.multiarray.get_handler_name() + + event = asyncio.Event() + # the child contexts inherit the parent policy + concurrent_task1 = asyncio.create_task( + concurrent_context1(get_module, orig_policy_name, event)) + concurrent_task2 = asyncio.create_task( + concurrent_context2(get_module, orig_policy_name, event)) + await concurrent_task1 + await concurrent_task2 + + # the parent context is not affected by child policy changes + assert np.core.multiarray.get_handler_name() == orig_policy_name + + +def test_context_locality(get_module): + if (sys.implementation.name == 'pypy' + and sys.pypy_version_info[:3] < (7, 3, 6)): + pytest.skip('no context-locality support in PyPy < 7.3.6') + asyncio.run(async_test_context_locality(get_module)) + + +def concurrent_thread1(get_module, event): + get_module.set_secret_data_policy() + assert np.core.multiarray.get_handler_name() == 'secret_data_allocator' + event.set() + + +def concurrent_thread2(get_module, event): + event.wait() + # the policy is not affected by changes in parallel threads + assert np.core.multiarray.get_handler_name() == 'default_allocator' + # change policy in the child thread + get_module.set_secret_data_policy() + + +def test_thread_locality(get_module): + orig_policy_name = np.core.multiarray.get_handler_name() + + event = threading.Event() + # the child threads do not inherit the parent policy + concurrent_task1 = threading.Thread(target=concurrent_thread1, + args=(get_module, event)) + concurrent_task2 = threading.Thread(target=concurrent_thread2, + args=(get_module, event)) + concurrent_task1.start() + concurrent_task2.start() + concurrent_task1.join() + concurrent_task2.join() + + # the parent thread is not affected by child policy changes + assert np.core.multiarray.get_handler_name() == orig_policy_name + + +@pytest.mark.slow +def test_new_policy(get_module): + a = np.arange(10) + orig_policy_name = np.core.multiarray.get_handler_name(a) + + orig_policy = get_module.set_secret_data_policy() + + b = np.arange(10) + assert np.core.multiarray.get_handler_name(b) == 'secret_data_allocator' + + # test array manipulation. This is slow + if orig_policy_name == 'default_allocator': + # when the np.core.test tests recurse into this test, the + # policy will be set so this "if" will be false, preventing + # infinite recursion + # + # if needed, debug this by + # - running tests with -- -s (to not capture stdout/stderr + # - setting extra_argv=['-vv'] here + assert np.core.test('full', verbose=2, extra_argv=['-vv']) + # also try the ma tests, the pickling test is quite tricky + assert np.ma.test('full', verbose=2, extra_argv=['-vv']) + + get_module.set_old_policy(orig_policy) + + c = np.arange(10) + assert np.core.multiarray.get_handler_name(c) == orig_policy_name + +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" + with assert_warns(RuntimeWarning) as w: + del a + gc.collect() + print(w) diff --git a/numpy/core/tests/test_nditer.py b/numpy/core/tests/test_nditer.py index 79f44ef80c8b..ed775cac628d 100644 --- a/numpy/core/tests/test_nditer.py +++ b/numpy/core/tests/test_nditer.py @@ -9,7 +9,7 @@ from numpy import array, arange, nditer, all from numpy.testing import ( assert_, assert_equal, assert_array_equal, assert_raises, - HAS_REFCOUNT, suppress_warnings + HAS_REFCOUNT, suppress_warnings, break_cycles ) @@ -3150,6 +3150,8 @@ def test_partial_iteration_cleanup(in_dtype, buf_dtype, steps): # Note that resetting does not free references del it + break_cycles() + break_cycles() assert count == sys.getrefcount(value) # Repeat the test with `iternext` @@ -3159,6 +3161,8 @@ def test_partial_iteration_cleanup(in_dtype, buf_dtype, steps): it.iternext() del it # should ensure cleanup + break_cycles() + break_cycles() assert count == sys.getrefcount(value) diff --git a/numpy/ma/mrecords.py b/numpy/ma/mrecords.py index 2ce1f0a2330f..1e8103bcf632 100644 --- a/numpy/ma/mrecords.py +++ b/numpy/ma/mrecords.py @@ -493,7 +493,6 @@ def _mrreconstruct(subtype, baseclass, baseshape, basetype,): _mask = ndarray.__new__(ndarray, baseshape, 'b1') return subtype.__new__(subtype, _data, mask=_mask, dtype=basetype,) - mrecarray = MaskedRecords diff --git a/numpy/testing/__init__.py b/numpy/testing/__init__.py index bca1d3670233..a008f5828e58 100644 --- a/numpy/testing/__init__.py +++ b/numpy/testing/__init__.py @@ -10,7 +10,7 @@ from ._private.utils import * from ._private.utils import (_assert_valid_refcount, _gen_alignment_data, IS_PYSTON) -from ._private import decorators as dec +from ._private import extbuild, decorators as dec from ._private.nosetester import ( run_module_suite, NoseTester as Tester ) diff --git a/numpy/testing/_private/extbuild.py b/numpy/testing/_private/extbuild.py new file mode 100644 index 000000000000..8b3a438dd131 --- /dev/null +++ b/numpy/testing/_private/extbuild.py @@ -0,0 +1,251 @@ +""" +Build a c-extension module on-the-fly in tests. +See build_and_import_extensions for usage hints + +""" + +import os +import pathlib +import sys +import sysconfig +from numpy.distutils.ccompiler import new_compiler +from distutils.errors import CompileError + +__all__ = ['build_and_import_extension', 'compile_extension_module'] + + +def build_and_import_extension( + modname, functions, *, prologue="", build_dir=None, + include_dirs=[], more_init=""): + """ + Build and imports a c-extension module `modname` from a list of function + fragments `functions`. + + + Parameters + ---------- + functions : list of fragments + Each fragment is a sequence of func_name, calling convention, snippet. + prologue : string + Code to preceed the rest, usually extra ``#include`` or ``#define`` + macros. + build_dir : pathlib.Path + Where to build the module, usually a temporary directory + include_dirs : list + Extra directories to find include files when compiling + more_init : string + Code to appear in the module PyMODINIT_FUNC + + Returns + ------- + out: module + The module will have been loaded and is ready for use + + Examples + -------- + >>> functions = [("test_bytes", "METH_O", \"\"\" + if ( !PyBytesCheck(args)) { + Py_RETURN_FALSE; + } + Py_RETURN_TRUE; + \"\"\")] + >>> mod = build_and_import_extension("testme", functions) + >>> assert not mod.test_bytes(u'abc') + >>> assert mod.test_bytes(b'abc') + """ + + body = prologue + _make_methods(functions, modname) + init = """PyObject *mod = PyModule_Create(&moduledef); + """ + if not build_dir: + build_dir = pathlib.Path('.') + if more_init: + init += """#define INITERROR return NULL + """ + init += more_init + init += "\nreturn mod;" + source_string = _make_source(modname, init, body) + try: + mod_so = compile_extension_module( + modname, build_dir, include_dirs, source_string) + except CompileError as e: + # shorten the exception chain + raise RuntimeError(f"could not compile in {build_dir}:") from e + import importlib.util + spec = importlib.util.spec_from_file_location(modname, mod_so) + foo = importlib.util.module_from_spec(spec) + spec.loader.exec_module(foo) + return foo + + +def compile_extension_module( + name, builddir, include_dirs, + source_string, libraries=[], library_dirs=[]): + """ + Build an extension module and return the filename of the resulting + native code file. + + Parameters + ---------- + name : string + name of the module, possibly including dots if it is a module inside a + package. + builddir : pathlib.Path + Where to build the module, usually a temporary directory + include_dirs : list + Extra directories to find include files when compiling + libraries : list + Libraries to link into the extension module + library_dirs: list + Where to find the libraries, ``-L`` passed to the linker + """ + modname = name.split('.')[-1] + dirname = builddir / name + dirname.mkdir(exist_ok=True) + cfile = _convert_str_to_file(source_string, dirname) + include_dirs = [sysconfig.get_config_var('INCLUDEPY')] + include_dirs + + return _c_compile( + cfile, outputfilename=dirname / modname, + include_dirs=include_dirs, libraries=[], library_dirs=[], + ) + + +def _convert_str_to_file(source, dirname): + """Helper function to create a file ``source.c`` in `dirname` that contains + the string in `source`. Returns the file name + """ + filename = dirname / 'source.c' + with filename.open('w') as f: + f.write(str(source)) + return filename + + +def _make_methods(functions, modname): + """ Turns the name, signature, code in functions into complete functions + and lists them in a methods_table. Then turns the methods_table into a + ``PyMethodDef`` structure and returns the resulting code fragment ready + for compilation + """ + methods_table = [] + codes = [] + for funcname, flags, code in functions: + cfuncname = "%s_%s" % (modname, funcname) + if 'METH_KEYWORDS' in flags: + signature = '(PyObject *self, PyObject *args, PyObject *kwargs)' + else: + signature = '(PyObject *self, PyObject *args)' + methods_table.append( + "{\"%s\", (PyCFunction)%s, %s}," % (funcname, cfuncname, flags)) + func_code = """ + static PyObject* {cfuncname}{signature} + {{ + {code} + }} + """.format(cfuncname=cfuncname, signature=signature, code=code) + codes.append(func_code) + + body = "\n".join(codes) + """ + static PyMethodDef methods[] = { + %(methods)s + { NULL } + }; + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "%(modname)s", /* m_name */ + NULL, /* m_doc */ + -1, /* m_size */ + methods, /* m_methods */ + }; + """ % dict(methods='\n'.join(methods_table), modname=modname) + return body + + +def _make_source(name, init, body): + """ Combines the code fragments into source code ready to be compiled + """ + code = """ + #include + + %(body)s + + PyMODINIT_FUNC + PyInit_%(name)s(void) { + %(init)s + } + """ % dict( + name=name, init=init, body=body, + ) + return code + + +def _c_compile(cfile, outputfilename, include_dirs=[], libraries=[], + library_dirs=[]): + if sys.platform == 'win32': + compile_extra = ["/we4013"] + link_extra = ["/LIBPATH:" + os.path.join(sys.exec_prefix, 'libs')] + elif sys.platform.startswith('linux'): + compile_extra = [ + "-O0", "-g", "-Werror=implicit-function-declaration", "-fPIC"] + link_extra = None + else: + compile_extra = link_extra = None + pass + if sys.platform == 'win32': + link_extra = link_extra + ['/DEBUG'] # generate .pdb file + if sys.platform == 'darwin': + # support Fink & Darwinports + for s in ('/sw/', '/opt/local/'): + if (s + 'include' not in include_dirs + and os.path.exists(s + 'include')): + include_dirs.append(s + 'include') + if s + 'lib' not in library_dirs and os.path.exists(s + 'lib'): + library_dirs.append(s + 'lib') + + outputfilename = outputfilename.with_suffix(get_so_suffix()) + saved_environ = os.environ.copy() + try: + build( + cfile, outputfilename, + compile_extra, link_extra, + include_dirs, libraries, library_dirs) + finally: + # workaround for a distutils bugs where some env vars can + # become longer and longer every time it is used + for key, value in saved_environ.items(): + if os.environ.get(key) != value: + os.environ[key] = value + return outputfilename + + +def build(cfile, outputfilename, compile_extra, link_extra, + include_dirs, libraries, library_dirs): + "cd into the directory where the cfile is, use distutils to build" + + compiler = new_compiler(force=1, verbose=2) + compiler.customize('') + objects = [] + + old = os.getcwd() + os.chdir(cfile.parent) + try: + res = compiler.compile( + [str(cfile.name)], + include_dirs=include_dirs, + extra_preargs=compile_extra + ) + objects += [str(cfile.parent / r) for r in res] + finally: + os.chdir(old) + + compiler.link_shared_object( + objects, str(outputfilename), + libraries=libraries, + extra_preargs=link_extra, + library_dirs=library_dirs) + + +def get_so_suffix(): + ret = sysconfig.get_config_var('EXT_SUFFIX') + assert ret + return ret diff --git a/tools/lint_diff.ini b/tools/lint_diff.ini index 3b66d3c3e900..9e31050b78a4 100644 --- a/tools/lint_diff.ini +++ b/tools/lint_diff.ini @@ -1,4 +1,5 @@ [pycodestyle] max_line_length = 79 statistics = True -ignore = E121,E122,E123,E125,E126,E127,E128,E226,E251,E265,E266,E302,E402,E704,E712,E721,E731,E741,W291,W293,W391,W503,W504 +ignore = E121,E122,E123,E125,E126,E127,E128,E226,E241,E251,E265,E266,E302,E402,E704,E712,E721,E731,E741,W291,W293,W391,W503,W504 +exclude = numpy/__config__.py,numpy/typing/tests/data