Thanks to visit codestin.com
Credit goes to github.com

Skip to content

BUG/ENH: Fix use of ndpointer in return values #12431

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 1 commit into from
Nov 22, 2018
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
10 changes: 10 additions & 0 deletions doc/release/1.16.0-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,16 @@ types. As of this release, this caveat is lifted - now:
* Bitfields are no longer interpreted as sub-arrays
* Pointers are no longer replaced with the type that they point to

A new ``ndpointer.contents`` member
-----------------------------------
This matches the ``.contents`` member of normal ctypes arrays, and can be used
to construct an ``np.array`` around the pointers contents.

This replaces ``np.array(some_nd_pointer)``, which stopped working in 1.15.
Copy link
Member Author

Choose a reason for hiding this comment

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

On the one hand, we could try to restore this - on the other, it didn't make a massive amount of sense anyway.


As a side effect of this change, ``ndpointer`` now supports dtypes with
overlapping fields and padding.


Changes
=======
Expand Down
13 changes: 13 additions & 0 deletions numpy/core/src/multiarray/_multiarray_tests.c.src
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
#include "npy_extint128.h"
#include "common.h"


#if defined(MS_WIN32) || defined(__CYGWIN__)
#define EXPORT(x) __declspec(dllexport) x
#else
#define EXPORT(x) x
#endif

#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0]))

/* test PyArray_IsPythonScalar, before including private py3 compat header */
Expand All @@ -31,6 +38,12 @@ IsPythonScalar(PyObject * dummy, PyObject *args)

#include "npy_pycompat.h"

/** Function to test calling via ctypes */
EXPORT(void*) forward_pointer(void *x)
{
return x;
}

/*
* TODO:
* - Handle mode
Expand Down
57 changes: 37 additions & 20 deletions numpy/ctypeslib.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@
'c_intp', 'as_ctypes', 'as_array']

import os
from numpy import integer, ndarray, dtype as _dtype, deprecate, array
from numpy import (
integer, ndarray, dtype as _dtype, deprecate, array, frombuffer
)
from numpy.core.multiarray import _flagdict, flagsobj

try:
Expand Down Expand Up @@ -175,24 +177,6 @@ def _flags_fromnum(num):


class _ndptr(_ndptr_base):

def _check_retval_(self):
"""This method is called when this class is used as the .restype
attribute for a shared-library function. It constructs a numpy
array from a void pointer."""
return array(self)

@property
def __array_interface__(self):
return {'descr': self._dtype_.descr,
'__ref': self,
'strides': None,
'shape': self._shape_,
'version': 3,
'typestr': self._dtype_.descr[0][1],
'data': (self.value, False),
}

@classmethod
def from_param(cls, obj):
if not isinstance(obj, ndarray):
Expand All @@ -213,6 +197,34 @@ def from_param(cls, obj):
return obj.ctypes


class _concrete_ndptr(_ndptr):
"""
Like _ndptr, but with `_shape_` and `_dtype_` specified.

Notably, this means the pointer has enough information to reconstruct
the array, which is not generally true.
"""
def _check_retval_(self):
"""
This method is called when this class is used as the .restype
attribute for a shared-library function, to automatically wrap the
pointer into an array.
"""
return self.contents

@property
def contents(self):
"""
Get an ndarray viewing the data pointed to by this pointer.

This mirrors the `contents` attribute of a normal ctypes pointer
"""
full_dtype = _dtype((self._dtype_, self._shape_))
full_ctype = ctypes.c_char * full_dtype.itemsize
buffer = ctypes.cast(self, ctypes.POINTER(full_ctype)).contents
return frombuffer(buffer, dtype=full_dtype).squeeze(axis=0)
Copy link
Member Author

@eric-wieser eric-wieser Nov 21, 2018

Choose a reason for hiding this comment

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

This is sort of frustrating - there ought to be a better way to construct an array from a pointer without having to go through an intermediate byte array.

Copy link
Member

Choose a reason for hiding this comment

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

That's why the full_ctype ?

Copy link
Member Author

@eric-wieser eric-wieser Nov 21, 2018

Choose a reason for hiding this comment

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

Yeah, the goal of full_ctypes is to get an object supporting the buffer protocol that we can pass to frombuffer. Perhaps ndarray.from_address(addr, shape, strides, dtype) ought to be a (dangerous) part of the C api?

Copy link
Member

Choose a reason for hiding this comment

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

Interesting thought. I'd mostly be concerned if memory or speed was a problem with the current approach.

Copy link
Member Author

@eric-wieser eric-wieser Nov 21, 2018

Choose a reason for hiding this comment

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

Memory shouldn't be a concern - this doesn't make any copies of the underlying data - at most we're churning around a bunch of metadata. _CPointer.contents returns a reference, not a copy.



# Factory for an array-checking class with from_param defined for
# use with ctypes argtypes mechanism
_pointer_type_cache = {}
Expand Down Expand Up @@ -320,7 +332,12 @@ def ndpointer(dtype=None, ndim=None, shape=None, flags=None):
if flags is not None:
name += "_"+"_".join(flags)

klass = type("ndpointer_%s"%name, (_ndptr,),
if dtype is not None and shape is not None:
base = _concrete_ndptr
else:
base = _ndptr

klass = type("ndpointer_%s"%name, (base,),
{"_dtype_": dtype,
"_shape_" : shape,
"_ndim_" : ndim,
Expand Down
75 changes: 70 additions & 5 deletions numpy/tests/test_ctypeslib.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,30 @@
from numpy.testing import assert_, assert_array_equal, assert_raises, assert_equal

try:
import ctypes
except ImportError:
ctypes = None
else:
cdll = None
test_cdll = None
if hasattr(sys, 'gettotalrefcount'):
try:
cdll = load_library('_multiarray_umath_d', np.core._multiarray_umath.__file__)
except OSError:
pass
try:
test_cdll = load_library('_multiarray_tests', np.core._multiarray_tests.__file__)
Copy link
Member

Choose a reason for hiding this comment

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

Long lines here and above.

Copy link
Member Author

Choose a reason for hiding this comment

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

Missed this one. It's not clear to me why load_library is needed here - the tests in cpython just use ctypes.CDLL(np.core._multiarray_tests.__file__), and presumably work just fine.

Copy link
Member

Choose a reason for hiding this comment

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

Are you looking to fix this, or are you going to try the cpython approach and remove an argument?

Copy link
Member

Choose a reason for hiding this comment

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

Oops, load_library is in the public interface, forgot about that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Given the lines in this section were long before this patch, I'd be inclined leave things as they are, and not spend more CI cycles on whitespace changes :).

In a future patch I might try using CDLL directly here and seeing if any CI breaks.

except OSError:
pass
if cdll is None:
cdll = load_library('_multiarray_umath', np.core._multiarray_umath.__file__)
_HAS_CTYPE = True
except ImportError:
_HAS_CTYPE = False
if test_cdll is None:
test_cdll = load_library('_multiarray_tests', np.core._multiarray_tests.__file__)

c_forward_pointer = test_cdll.forward_pointer


@pytest.mark.skipif(not _HAS_CTYPE,
@pytest.mark.skipif(ctypes is None,
reason="ctypes not available in this python")
@pytest.mark.skipif(sys.platform == 'cygwin',
reason="Known to fail on cygwin")
Expand Down Expand Up @@ -117,8 +127,63 @@ def test_cache(self):
assert_(ndpointer(shape=2) is not ndpointer(ndim=2))
assert_(ndpointer(ndim=2) is not ndpointer(shape=2))

@pytest.mark.skipif(ctypes is None,
reason="ctypes not available on this python installation")
class TestNdpointerCFunc(object):
def test_arguments(self):
""" Test that arguments are coerced from arrays """
c_forward_pointer.restype = ctypes.c_void_p
c_forward_pointer.argtypes = (ndpointer(ndim=2),)

c_forward_pointer(np.zeros((2, 3)))
# too many dimensions
assert_raises(
ctypes.ArgumentError, c_forward_pointer, np.zeros((2, 3, 4)))

@pytest.mark.parametrize(
'dt', [
float,
np.dtype(dict(
formats=['<i4', '<i4'],
names=['a', 'b'],
offsets=[0, 2],
itemsize=6
))
], ids=[
'float',
'overlapping-fields'
]
)
def test_return(self, dt):
""" Test that return values are coerced to arrays """
arr = np.zeros((2, 3), dt)
ptr_type = ndpointer(shape=arr.shape, dtype=arr.dtype)

c_forward_pointer.restype = ptr_type
c_forward_pointer.argtypes = (ptr_type,)

# check that the arrays are equivalent views on the same data
arr2 = c_forward_pointer(arr)
assert_equal(arr2.dtype, arr.dtype)
assert_equal(arr2.shape, arr.shape)
assert_equal(
arr2.__array_interface__['data'],
arr.__array_interface__['data']
)

def test_vague_return_value(self):
""" Test that vague ndpointer return values do not promote to arrays """
arr = np.zeros((2, 3))
ptr_type = ndpointer(dtype=arr.dtype)

c_forward_pointer.restype = ptr_type
c_forward_pointer.argtypes = (ptr_type,)

ret = c_forward_pointer(arr)
assert_(isinstance(ret, ptr_type))


@pytest.mark.skipif(not _HAS_CTYPE,
@pytest.mark.skipif(ctypes is None,
reason="ctypes not available on this python installation")
class TestAsArray(object):
def test_array(self):
Expand Down