File size: 7,087 Bytes
7885a28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
from . import _ccallback_c

import ctypes

PyCFuncPtr = ctypes.CFUNCTYPE(ctypes.c_void_p).__bases__[0]

ffi = None

class CData:
    pass

def _import_cffi():
    global ffi, CData

    if ffi is not None:
        return

    try:
        import cffi
        ffi = cffi.FFI()
        CData = ffi.CData
    except ImportError:
        ffi = False


class LowLevelCallable(tuple):
    """
    Low-level callback function.

    Some functions in SciPy take as arguments callback functions, which
    can either be python callables or low-level compiled functions. Using
    compiled callback functions can improve performance somewhat by
    avoiding wrapping data in Python objects.

    Such low-level functions in SciPy are wrapped in `LowLevelCallable`
    objects, which can be constructed from function pointers obtained from
    ctypes, cffi, Cython, or contained in Python `PyCapsule` objects.

    .. seealso::

       Functions accepting low-level callables:

       `scipy.integrate.quad`, `scipy.ndimage.generic_filter`,
       `scipy.ndimage.generic_filter1d`, `scipy.ndimage.geometric_transform`

       Usage examples:

       :ref:`ndimage-ccallbacks`, :ref:`quad-callbacks`

    Parameters
    ----------
    function : {PyCapsule, ctypes function pointer, cffi function pointer}
        Low-level callback function.
    user_data : {PyCapsule, ctypes void pointer, cffi void pointer}
        User data to pass on to the callback function.
    signature : str, optional
        Signature of the function. If omitted, determined from *function*,
        if possible.

    Attributes
    ----------
    function
        Callback function given.
    user_data
        User data given.
    signature
        Signature of the function.

    Methods
    -------
    from_cython
        Class method for constructing callables from Cython C-exported
        functions.

    Notes
    -----
    The argument ``function`` can be one of:

    - PyCapsule, whose name contains the C function signature
    - ctypes function pointer
    - cffi function pointer

    The signature of the low-level callback must match one of those expected
    by the routine it is passed to.

    If constructing low-level functions from a PyCapsule, the name of the
    capsule must be the corresponding signature, in the format::

        return_type (arg1_type, arg2_type, ...)

    For example::

        "void (double)"
        "double (double, int *, void *)"

    The context of a PyCapsule passed in as ``function`` is used as ``user_data``,
    if an explicit value for ``user_data`` was not given.

    """

    # Make the class immutable
    __slots__ = ()

    def __new__(cls, function, user_data=None, signature=None):
        # We need to hold a reference to the function & user data,
        # to prevent them going out of scope
        item = cls._parse_callback(function, user_data, signature)
        return tuple.__new__(cls, (item, function, user_data))

    def __repr__(self):
        return f"LowLevelCallable({self.function!r}, {self.user_data!r})"

    @property
    def function(self):
        return tuple.__getitem__(self, 1)

    @property
    def user_data(self):
        return tuple.__getitem__(self, 2)

    @property
    def signature(self):
        return _ccallback_c.get_capsule_signature(tuple.__getitem__(self, 0))

    def __getitem__(self, idx):
        raise ValueError()

    @classmethod
    def from_cython(cls, module, name, user_data=None, signature=None):
        """
        Create a low-level callback function from an exported Cython function.

        Parameters
        ----------
        module : module
            Cython module where the exported function resides
        name : str
            Name of the exported function
        user_data : {PyCapsule, ctypes void pointer, cffi void pointer}, optional
            User data to pass on to the callback function.
        signature : str, optional
            Signature of the function. If omitted, determined from *function*.

        """
        try:
            function = module.__pyx_capi__[name]
        except AttributeError as e:
            message = "Given module is not a Cython module with __pyx_capi__ attribute"
            raise ValueError(message) from e
        except KeyError as e:
            message = f"No function {name!r} found in __pyx_capi__ of the module"
            raise ValueError(message) from e
        return cls(function, user_data, signature)

    @classmethod
    def _parse_callback(cls, obj, user_data=None, signature=None):
        _import_cffi()

        if isinstance(obj, LowLevelCallable):
            func = tuple.__getitem__(obj, 0)
        elif isinstance(obj, PyCFuncPtr):
            func, signature = _get_ctypes_func(obj, signature)
        elif isinstance(obj, CData):
            func, signature = _get_cffi_func(obj, signature)
        elif _ccallback_c.check_capsule(obj):
            func = obj
        else:
            raise ValueError("Given input is not a callable or a "
                             "low-level callable (pycapsule/ctypes/cffi)")

        if isinstance(user_data, ctypes.c_void_p):
            context = _get_ctypes_data(user_data)
        elif isinstance(user_data, CData):
            context = _get_cffi_data(user_data)
        elif user_data is None:
            context = 0
        elif _ccallback_c.check_capsule(user_data):
            context = user_data
        else:
            raise ValueError("Given user data is not a valid "
                             "low-level void* pointer (pycapsule/ctypes/cffi)")

        return _ccallback_c.get_raw_capsule(func, signature, context)


#
# ctypes helpers
#

def _get_ctypes_func(func, signature=None):
    # Get function pointer
    func_ptr = ctypes.cast(func, ctypes.c_void_p).value

    # Construct function signature
    if signature is None:
        signature = _typename_from_ctypes(func.restype) + " ("
        for j, arg in enumerate(func.argtypes):
            if j == 0:
                signature += _typename_from_ctypes(arg)
            else:
                signature += ", " + _typename_from_ctypes(arg)
        signature += ")"

    return func_ptr, signature


def _typename_from_ctypes(item):
    if item is None:
        return "void"
    elif item is ctypes.c_void_p:
        return "void *"

    name = item.__name__

    pointer_level = 0
    while name.startswith("LP_"):
        pointer_level += 1
        name = name[3:]

    if name.startswith('c_'):
        name = name[2:]

    if pointer_level > 0:
        name += " " + "*"*pointer_level

    return name


def _get_ctypes_data(data):
    # Get voidp pointer
    return ctypes.cast(data, ctypes.c_void_p).value


#
# CFFI helpers
#

def _get_cffi_func(func, signature=None):
    # Get function pointer
    func_ptr = ffi.cast('uintptr_t', func)

    # Get signature
    if signature is None:
        signature = ffi.getctype(ffi.typeof(func)).replace('(*)', ' ')

    return func_ptr, signature


def _get_cffi_data(data):
    # Get pointer
    return ffi.cast('uintptr_t', data)