diff options
author | Qiming Sun <osirpt.sun@gmail.com> | 2019-09-10 02:22:12 -0700 |
---|---|---|
committer | Charles Harris <charlesr.harris@gmail.com> | 2019-11-07 13:59:54 -0700 |
commit | 88fe4372c9a5cf3dd92431184f165bb25f091490 (patch) | |
tree | 10e147f20da60825e1ccbfb653d66e1fe4ecde37 | |
parent | a8cf76a63d998b0de2cd7826fe8dee89641a945c (diff) | |
download | numpy-88fe4372c9a5cf3dd92431184f165bb25f091490.tar.gz |
BUG: Fix _ctypes class cirular reference. (#13808)
In _ctypes class, ctypes.cast() was called twice. It causes circular reference
for _ctypes._data due to the CPython bug https://bugs.python.org/issue12836.
-rw-r--r-- | numpy/core/_internal.py | 55 | ||||
-rw-r--r-- | numpy/core/tests/test_multiarray.py | 24 |
2 files changed, 34 insertions, 45 deletions
diff --git a/numpy/core/_internal.py b/numpy/core/_internal.py index b0ea603e1..5fd643505 100644 --- a/numpy/core/_internal.py +++ b/numpy/core/_internal.py @@ -247,55 +247,13 @@ class _missing_ctypes(object): self.value = ptr -class _unsafe_first_element_pointer(object): - """ - Helper to allow viewing an array as a ctypes pointer to the first element - - This avoids: - * dealing with strides - * `.view` rejecting object-containing arrays - * `memoryview` not supporting overlapping fields - """ - def __init__(self, arr): - self.base = arr - - @property - def __array_interface__(self): - i = dict( - shape=(), - typestr='|V0', - data=(self.base.__array_interface__['data'][0], False), - strides=(), - version=3, - ) - return i - - -def _get_void_ptr(arr): - """ - Get a `ctypes.c_void_p` to arr.data, that keeps a reference to the array - """ - import numpy as np - # convert to a 0d array that has a data pointer referrign to the start - # of arr. This holds a reference to arr. - simple_arr = np.asarray(_unsafe_first_element_pointer(arr)) - - # create a `char[0]` using the same memory. - c_arr = (ctypes.c_char * 0).from_buffer(simple_arr) - - # finally cast to void* - return ctypes.cast(ctypes.pointer(c_arr), ctypes.c_void_p) - - class _ctypes(object): def __init__(self, array, ptr=None): self._arr = array if ctypes: self._ctypes = ctypes - # get a void pointer to the buffer, which keeps the array alive - self._data = _get_void_ptr(array) - assert self._data.value == ptr + self._data = self._ctypes.c_void_p(ptr) else: # fake a pointer-like object that holds onto the reference self._ctypes = _missing_ctypes() @@ -317,7 +275,14 @@ class _ctypes(object): The returned pointer will keep a reference to the array. """ - return self._ctypes.cast(self._data, obj) + # _ctypes.cast function causes a circular reference of self._data in + # self._data._objects. Attributes of self._data cannot be released + # until gc.collect is called. Make a copy of the pointer first then let + # it hold the array reference. This is a workaround to circumvent the + # CPython bug https://bugs.python.org/issue12836 + ptr = self._ctypes.cast(self._data, obj) + ptr._arr = self._arr + return ptr def shape_as(self, obj): """ @@ -385,7 +350,7 @@ class _ctypes(object): Enables `c_func(some_array.ctypes)` """ - return self._data + return self.data_as(ctypes.c_void_p) # kept for compatibility get_data = data.fget diff --git a/numpy/core/tests/test_multiarray.py b/numpy/core/tests/test_multiarray.py index a0426a808..6fa8548a0 100644 --- a/numpy/core/tests/test_multiarray.py +++ b/numpy/core/tests/test_multiarray.py @@ -7921,6 +7921,8 @@ class TestFormat(object): dst = object.__format__(a, '30') assert_equal(res, dst) +from numpy.testing import IS_PYPY + class TestCTypes(object): def test_ctypes_is_available(self): @@ -7987,7 +7989,29 @@ class TestCTypes(object): # but when the `ctypes_ptr` object dies, so should `arr` del ctypes_ptr + if IS_PYPY: + # Pypy does not recycle arr objects immediately. Trigger gc to + # release arr. Cpython uses refcounts. An explicit call to gc + # should not be needed here. + break_cycles() + assert_(arr_ref() is None, "unknowable whether ctypes pointer holds a reference") + + def test_ctypes_as_parameter_holds_reference(self): + arr = np.array([None]).copy() + + arr_ref = weakref.ref(arr) + + ctypes_ptr = arr.ctypes._as_parameter_ + + # `ctypes_ptr` should hold onto `arr` + del arr break_cycles() + assert_(arr_ref() is not None, "ctypes pointer did not hold onto a reference") + + # but when the `ctypes_ptr` object dies, so should `arr` + del ctypes_ptr + if IS_PYPY: + break_cycles() assert_(arr_ref() is None, "unknowable whether ctypes pointer holds a reference") |