-2

I am trying to pass different functions which have pointers as arguments to a python function. One example of the input function as input parameter is the given normal function:

Sample.pyx

from cpython cimport array
import cython
import ctypes
cimport numpy as np
cpdef void normal(np.ndarray[ndim=1, dtype=np.float64_t] u, 
                  np.ndarray[ndim=1, dtype=np.float64_t] yu, 
                  np.ndarray[ndim=1, dtype=np.float64_t] ypu):

      cdef int i
      cdef int n=len(u)
      for i in prange(n, nogil=True):
          yu[i]=-u[i]*u[i]*0.5                                                               
          ypu[i]=-u[i]                                                                    
      return                                                     
cdef class _SampleFunc:
     cdef void (*func)(double *, double *, double *)

cdef void sample(int* x, double* hx, double* hxx, void(*func)(double*, double*, double*), int n):
      def int i
      for i from 0 <= i < n:
          func[0](&x[i], &hx[i], &hxx[i])
      return 
cdef class myClass:
     sample_wrapper = _SampleFunc() 
     sample_wrapper.func = Null
     def foo(np.ndarray[ndim=1, dtype=np.float64_t] x,
             np.ndarray[ndim=1, dtype=np.float64_t] hx,
             np.ndarray[ndim=1, dtype=np.float64_t] hxx,
             _SampleFunc sample_func, 
             int k):
         cdef np.ndarray[ndim=1, dtype=np.float64_t] sp
         cdef int num=len(x)
         func = sample_func.func
         assert func is not NULL, "function value is NULL"
         cdef int j
         for j from 0 <= j <k:
             sample(&x[0],&hx[0], &hxx[0], func, num)
             sp[j]=hx[0]
         return sp

test.py

import numpy as np
from sample import *
x = np.zeros(10, float)
hx = np.zeros(10, float)
hpx = np.zeros(10, float)

x[0] = 0
x[1] = 1.0
x[2] = -1.0
def pynormal(x):
    return -x*x*0.5,-x

hx[0], hpx[0] = pynormal(x[0])
hx[1], hpx[1] = pynormal(x[1])
hx[2], hpx[2] = pynormal(x[2])
num=20
ars=myClass()
s=ars.foo( x, hx, hpx, normal, num)

Running the test.py code I am getting this error:

'ars._SampleFunc' object has no attribute 'func'

I am trying to write a wrapper for different C functions which have three pointer arrays as their argument. My conclusion so far was that it can be done with a class, since the class can be accessible in python. I am wondering how I can pass the C functions with pointer arrays as argument to myClass class?

Update: Normal function

cdef void normal(
                 int n,
                 double* u, 
                 double* yu, 
                 double* ypu
                ):          
      cdef int i          
      for i in prange(n, nogil=True):
          yu[i]=-u[i]*u[i]*0.5                                                               
          ypu[i]=-u[i]                                                                    
      return 
Dalek
  • 4,168
  • 11
  • 48
  • 100
  • 3
    This cannot be possibly the *shortest* example that you could come up with. – Antti Haapala -- Слава Україні Sep 26 '17 at 10:29
  • to elaborate on @AnttiHaapala, _Questions seeking debugging help (why isn't this code working?) must include the desired behavior, a specific problem or error and the shortest code necessary to reproduce it in the question itself. Questions without a clear problem statement are not useful to other readers. See: How to create a [mcve]._ – Sourav Ghosh Sep 26 '17 at 11:01
  • for example, the function pointer could be `void(*func)(double)`, then you would need much less code to produce a working example. – ead Sep 26 '17 at 11:13
  • @SouravGhosh I have already tried to ask my question in shorter description but no one give a proper answer. I am wondering the purpose of stackoverflow is educational or just giving as short as possible answers to get upvotes for answer providers without giving deep answers. – Dalek Sep 26 '17 at 11:13
  • @ead I want that the pointers which I give as inputs to the `func` carry the updates of their values. If I pass just a normal variable as argument, then defining python function does the job and actually I do not need to suffer a lot of trouble to define a `c` function. – Dalek Sep 26 '17 at 11:17
  • But do you need all 3 pointers for the sake of this example? And your problem is to pass a function pointer, no matter how simple it is, isn't? I would say, which arguments this function has, doesn't really matter. – ead Sep 26 '17 at 11:38
  • My understanding of what you're trying to do is that you have a C function with an interface that you can't change that takes a function pointer (`sample`). The function pointer accepts C arrays (as `double*`). You want to be able to convert a Python callable to this function pointer? Unfortunately this is basically impossible to do in Cython since it involves dynamic code generation (Python callables have quite a bit of state, which you have no way of passing). – DavidW Sep 26 '17 at 11:54
  • You can do it with ctypes or CFFI though (search for "callbacks" in their documentation) - I can give an working example at some point later (if you/nobody else does it first) – DavidW Sep 26 '17 at 11:54
  • @DavidW I want somehow write a python wrapper for `sample` function. Well, my original code has another function encapsulated in the foo function that initialize the value of `x` and `hx` and update them, after that it calls the `sample` function. I want to write a python wrapper for all of these and it would be able to get `normal` function which is a `c` function as an argument. – Dalek Sep 26 '17 at 12:01
  • @Dalek all this information should be in the question, **not** in the comments. – Antti Haapala -- Слава Україні Sep 26 '17 at 12:07
  • @Dalek Do you only want to be able to call it with `normal` (reasonably easy) or do you want to call it with arbitrary functions (more difficult)? The other problem is that `normal` has no way of knowing the length of the arrays from `double*`. – DavidW Sep 26 '17 at 12:28
  • @DavidW I'd like to be able to call any arbitrary function – Dalek Sep 26 '17 at 14:12

1 Answers1

2

The first thing to deal with is that a function of signature cdef void (*func)(double *, double *, double *) does not pass the array length. You can't know how long these arrays are, and thus you can't safely access their elements. The sensible thing is to change the function signature to pass a length too:

cdef void (*func)(double *, double *, double *, int)

What is extra confusing is that you seem to be iterating over the same axis of a 1D array in both normal and sample. I suspect that isn't what you want to do, but I'm not going attempt to fix that.


Essentially your problem is that you want to pass an arbitrary Python callable as a C function pointer. The bad news is that Cython can't do it - a Python callable has a significant amount of information associated with it, while a C function pointer is simply the address of some executable memory. Therefore a C function pointer does not have the space available to hold the information in a Python callable. In order to make this work you need to generate code at runtime, which Python can't do.

I've recommended the ctypes standard library module as a solution to similar problems previously, since it can create a function pointer from a Python callable. There is a simpler but more limited solution if you only want to call cdef Cython functions.

ctypes

Here's a minimal example which demonstrates how to implement the idea:

import numpy as np
import ctypes

ctypedef void (*func_t)(int, double *)

cdef void sample(int n, double* x, func_t f):
    f(n,x)

def call_sample(double[::1] x,
                f):

    def func_wrapper(n, arg1):
        # x is a slightly opaque ctypes type
        # first cast it to a ctypes array of known size
        # and then create a numpy array from that
        arg1_as_ctypes_array = (ctypes.c_double*n).from_address(ctypes.addressof(arg1.contents))
        return f(np.asarray(arg1_as_ctypes_array))


    FTYPE = ctypes.CFUNCTYPE(None, # return type
                             ctypes.c_int, # arguments
                             ctypes.POINTER(ctypes.c_double))
    f_ctypes = FTYPE(func_wrapper) # convert Python callable to ctypes function pointer

    # a rather nasty line to convert to a C function pointer
    cdef func_t f_ptr = (<func_t*><size_t>ctypes.addressof(f_ctypes))[0]

    sample(x.shape[0], &x[0], f_ptr)


def example_function(x):
    # expects a numpy array like object
    print(x)

def test():
    a = np.random.rand(20)
    print(a)
    call_sample(a,example_function)

I realise that there's some slightly messy conversion between ctypes and Cython - this is unavoidable.

A bit of explanation: I'm assuming you want to keep the Python interface simple, hence example_function just takes a numpy array-like object. The function passed by ctypes needs to accept a number of elements and a pointer to match your C interface.

The ctypes pointer type (LP_c_double) can do do indexing (i.e. arg1[5]) so it works fine for simple uses, but it doesn't store its length internally. It's helpful (but not essential) to change it to a numpy array so you can use it more generally and thus we create a wrapper function to do this. We do:

arg1_as_ctypes_array = (ctypes.c_double*n).from_address(ctypes.addressof(arg1.contents))

to convert it to a known length ctypes array and then

np.asarray(arg1_as_ctypes_array)

to convert it to a numpy array. This shares the data rather than makes a copy, so if you change it then your original data will be changed. Because the conversion to a numpy array follows a standard pattern it's easy to generate a wrapper function in call_sample.

(In the comments you ask how to do the conversion if you're just passing a double, not a double*. In this case you don't have to do anything since a ctypes double behaves exactly like a Python type)

Only cdef functions

If you're certain the functions you want to pass will always be cdef functions then you can avoid ctypes and come up with something a bit simpler. You first need to make the function signature match the pointer exactly:

cdef void normal(int N, double *x): # other parameters as necessary
    cdef double[::1] x_as_mview = <double[:N:1]>x # cast to a memoryview
    # ... etc

You should then be able to use your definition of SampleFunc almost as is to create module level objects:

# in Cython
normal_samplefunc = SampleFunc()
normal_samplefunc.func = &normal

# in Python
s=ars.foo( x, hx, hpx, normal_samplefunc, num)

ars.foo is the way you wrote it (no ctypes code):

func = sample_func.func
# ...
sample(..., func,...)

This code will run quicker, but you want be able to call normal from Python.


Python interface

You mention in the comments that you'd also like the be able to access normal from Python. You're likely to need a different interface for the Python function and the one you pass to C, so I'd define a separate function for both uses, but share the implementation:

def normal(double[::1] u, # ... other arguments
           ):
   # or cpdef, if you really want
   implementation goes here

# then, depending on if you're using ctypes or not:
def normal_ctypes(int n, u # other arguments ...
   ):
   u_as_ctypes_array = (ctypes.c_double*n).from_address(ctypes.addressof(x.contents))
   normal(u_as_ctypes_array, # other arguments
                )

# or
cdef void normal_c(int n, double* u # ...
              ):
   normal(<double[:N:1]>x # ...
          )
DavidW
  • 29,336
  • 6
  • 55
  • 86
  • Thanks a lot for the answer and description. I will try to apply your answer to my code. – Dalek Sep 27 '17 at 08:48
  • A comment and question is that I want the `normal` function would be callable from python code. If I make it, in the way you described on the top, I can not access it in python. Well, Do you think the way I coded my `normal` function is wrong in this context? – Dalek Sep 27 '17 at 08:54
  • The issue is that for the normal function to be callable from C it needs to _exactly_ match the C signature. For it to be callable from python it needs to accept Python types (i.e. not C pointers). What you probably want to do is to write a Python/Cython version and C version. The C version convert the pointers to a memoryview or numpy array as shown above, and then call the Python version. – DavidW Sep 27 '17 at 09:08
  • Sorry about my naive questions but I am confused whether the `cpdef` type defining normal function, the way I did above, is right or not? Then can it be used for the pointer function as you defined in your answer? Because I tried to change my code according your comments but kept above definition of the `normal` function, I got this error :`f = FTYPE(f) # convert Python callable to ctypes function pointer TypeError: invalid result type for callback function` – Dalek Sep 27 '17 at 10:10
  • I changed the `cpdef void normal` by adding an extra input argument for the size of vector `u`. Then I compiled the code and ran my `test.py` code. For the `sample` function, I got this error message:`File "_ctypes/callbacks.c", line ..., in 'calling callback function' TypeError: Argument 'u' has incorrect type (expected numpy.ndarray, got LP_c_double)` – Dalek Sep 27 '17 at 13:43
  • @Dalek I've added an illustration of how I'd go about creating a function that can be called from both Python and C (by creating two versions). If I use my minimal example and change `example_function` to `cpdef` it works fine, so I don't know where your type error comes from. I also don't know about your "argument 'u' has incorrect type". – DavidW Sep 27 '17 at 16:51
  • the `normal_ctypes` is the function which I can call from my python and pass as an argument to `call_sample` function, right? – Dalek Sep 27 '17 at 17:37
  • No. `normal` is a function you can call from Python. `normal_ctypes` is a function that you can pass as an argument to `call_sample` but which you aren't really recommended to call from Python. – DavidW Sep 27 '17 at 17:50
  • but My main question is how can I call from my python script the`call_sample` function and pass a python version of `normal` function as an argument. – Dalek Sep 27 '17 at 17:58
  • @DawidW the `call_sample` function is a python function, so it can be imported to my python code but what is the reason behind it that you do not recommend to import `call_sample` and `normal_ctypes` to the python code? – Dalek Sep 28 '17 at 09:26
  • Thanks for the answer. I just have a few final questions. If you would kindly answer, I will be really grateful. In the case that pointer just refers to a double value instead of a pointer array, what is the substitution for this line in your answer :`x_as_ctypes_array = (ctypes.c_double*n).from_address(ctypes.addressof(x.contents))` in `normal_ctypes` function to return the address of the pointer? – Dalek Sep 28 '17 at 09:47
  • One thing that I am still confused is that how to write a python wrapper for `normal_c` function that can be callable from a python script and can be passed to `call_sample` when I will call this function also in my python script? – Dalek Oct 02 '17 at 10:02
  • 1
    To be honest that's hard. There's a choice between writing code that takes Python callables (but might be slower) or writing code that takes C function pointers (which might be quicker but is definitely less easy to use). I don't think there's a good way of doing what you want with `normal_c`. – DavidW Oct 02 '17 at 18:50
  • I just wrote another function in my `cython` script and used the `normal` function and it is working. Thanks a lot. – Dalek Oct 02 '17 at 22:34