Search code examples
pythonccallbackcython

Pass a callback function from python to c using cython


First off I want to say that I do not have the option to modify or even view the c source code so anything that involves modifying the c file will not be helpful.

In VP.h:

typedef enum VPEvent { 
  ...
  EVENT_OBJECT_CLICK,
  ...
}
...
typedef void *VPInstance;
typedef void(*VPEventHandler)(VPInstance);
...
VPSDK_API VPInstance vp_create(void);
...
VPSDK_API int vp_event_set(VPInstance instance, VPEvent eventname, VPEventHandler event);
...

In VP.pyx:

cdef extern from "VP.h":
  ...
  cdef enum VPEvent:
    ...
    VP_EVENT_OBJECT_CLICK,
    ...

  ...
  ctypedef void *VPInstance
  ctypedef void(*VPEventHandler)(VPInstance)
  ...
  VPInstance vp_create()
  ...
  int vp_event_set(VPInstance instance, VPEvent eventname, VPEventHandler event)
  ...

...
EVENT_OBJECT_CLICK = VP_EVENT_OBJECT_CLICK
...

cdef class create:
  cdef VPInstance instance

  def __init__(self):
    self.instance = vp_create()
  ...
  def event_set(self, eventname, event):
    return vp_event_set(self.instance, eventname, event)

What I want to have In Python:

import VP
...
def click(bot):
  bot.say("Someone clicked something!")
...
bot = VP.create()
bot.event_set(VP.EVENT_OBJECT_CLICK, click)

This is how you would do it in c:

#include <VP.h>

void click(VPInstance instance) {
  vp_say(instance, "Someone clicked something!");
}

int main(int argc, char ** argv) {
  ...
  VPInstance instance;
  instance = vp_create();
  ...
  vp_event_set(instance, VP_EVENT_OBJECT_CLICK, click)
}

However the problem is that when compiling VP.pyx I get

Cannot convert Python object to 'VPEventHandler'

As well, by default the callback is given a VPInstance pointer but I want to abstract this value into a class.


Solution

  • As you probably figured out, the problem in the call

    bot.event_set(VP.EVENT_OBJECT_CLICK, click)
    

    Indeed, the third argument click is a Python function object which you are passing in event_set to vp_event_set. Alas vp_event_set is expecting a VPEventHandler that is a C function pointer of type void(*VPEventHandler)(VPInstance);

    I think I would build a dictionary associating to a VPInstance (void * pointer casted as integer) an instance of some PyEvent class which should contains itself the function click. Using that you can ensure that you need on one C function as callback.

    In foo.pxd:

    cdef class PyEvent(object):
         cdef VPInstance instance
         cdef object py_callback
    

    In foo.pyx:

    events = dict()
    
    cdef void EventCallBack(VPInstance instance):
         PyEvent ev = <PyEvent> dict[events[<size_t> self.instance]
         ev.py_callback(ev)
    
    cdef class PyEvent(object):
         def __init__(self, click):
             self.instance = vp_create()
             self.py_callback = click
    
         def event_set(self, eventname):
             global events
             events[<size_t> self.instance] = self
             return vp_event_set(self.instance, eventname, EventCallBack)
    

    I don't have a chance to test this, so I hope it more or less works. Also I would recommend asking on cython-users@googlegroups.com as they are usually really helpful and more expert than me.