I am not sure if you are allowed to change the function pointer type from
ctypedef int (*function_type)(int a, int b)
to
ctypedef int (*function_type)(int a, int b, void *func_d)
but this is usually the way callback functions are implemented in C. void *
parameter func_d
to the function contains the user-provided data in any form. If the answer is yes, then you can have the following solution.
First, create the following definition file in Cython to reveal your C API to other Cython users:
# binary_op.pxd
ctypedef int (*func_t)(int a, int b, void *func_d) except? -1
cdef int func(int a, int b, void *func_d) except? -1
cdef class BinaryOp:
cpdef int eval(self, int a, int b) except? -1
cdef class Add(BinaryOp):
cpdef int eval(self, int a, int b) except? -1
cdef class Multiply(BinaryOp):
cpdef int eval(self, int a, int b) except? -1
This basically allows any Cython user to cimport
these definitions directly into their Cython code and bypass any Python-related function calls. Then, you implement the module in the following pyx
file:
# binary_op.pyx
cdef int func(int a, int b, void *func_d) except? -1:
return (<BinaryOp>func_d).eval(a, b)
cdef class BinaryOp:
cpdef int eval(self, int a, int b) except? -1:
raise NotImplementedError()
cdef class Add(BinaryOp):
cpdef int eval(self, int a, int b) except? -1:
return a + b
cdef class Multiply(BinaryOp):
cpdef int eval(self, int a, int b) except? -1:
return a * b
def call_me(BinaryOp oper not None, c, d):
return func(c, d, <void *>oper)
As you can see, BinaryOp
serves as the base class which raises NotImplementedError
for its users who do not implement eval
properly. cpdef
functions can be overridden by both Cython and Python users, and if they are called from Cython, efficient C calling mechanisms are involved. Otherwise, there is a small overhead when called from Python (well, of course, these functions work on scalars, and hence, the overhead might not be that small).
Then, a Python user might have the following application code:
# app_1.py
import pyximport
pyximport.install()
from binary_op import BinaryOp, Add, Multiply, call_me
print(call_me(Add(), 5, 6))
print(call_me(Multiply(), 5, 6))
class LinearOper(BinaryOp):
def __init__(self, p1, p2):
self.p1 = p1
self.p2 = p2
def eval(self, a, b):
return self.p1 * a + self.p2 * b
print(call_me(LinearOper(3, 4), 5, 6))
As you can see, they can not only create objects from efficient Cython (concrete) classes (i.e., Add
and Multiply
) but also implement their own classes based on BinaryOp
(hopefully by providing the implementation to eval
). When you run python app_1.py
, you will see (after the compilations):
11
30
39
Then, your Cython users can implement their favorite functions as follows:
# sub.pyx
from binary_op cimport BinaryOp
cdef class Sub(BinaryOp):
cpdef int eval(self, int a, int b) except? -1:
return a - b
Well, of course, any application code that uses sub.pyx
can use both libraries as follows:
import pyximport
pyximport.install()
from sub import Sub
from binary_op import call_me
print(call_me(Sub(), 5, 6))
When you run python app_2.py
, you get the expected result: -1
.
EDIT. By the way, provided that you are allowed to have the aforementioned function_type
signature (i.e., the one that has a void *
parameter as the third argument), you can in fact pass an arbitrary Python callable object as a C pointer. For this to happen, you need to have the following changes:
# binary_op.pyx
cdef int func(int a, int b, void *func_d) except? -1:
return (<object>func_d)(a, b)
def call_me(oper not None, c, d):
return func(c, d, <void *>oper)
Note, however, that Python now needs to figure out which object
oper
is. In the former solution, we were constraining oper
to be a valid BinaryOp
object. Note also that __call__
and similar special functions can only be declared def
, which limits your use case. Nevertheless, with these last changes, we can have the following code run without any problems:
print(call_me(lambda x, y: x - y, 5, 6))