summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorQiming Sun <osirpt.sun@gmail.com>2019-09-10 02:22:12 -0700
committerCharles Harris <charlesr.harris@gmail.com>2019-11-07 13:59:54 -0700
commit88fe4372c9a5cf3dd92431184f165bb25f091490 (patch)
tree10e147f20da60825e1ccbfb653d66e1fe4ecde37
parenta8cf76a63d998b0de2cd7826fe8dee89641a945c (diff)
downloadnumpy-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.py55
-rw-r--r--numpy/core/tests/test_multiarray.py24
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")