4

I am trying to wrap a C++ library for python, using SWIG. The library uses callback functions frequently, by passing callback functions of certain type to class methods.

Now, after wrapping the code, I would like to create the callback logic from python. Is this possible? Here is an experiment I was doing to find it out .. does not work at the moment.

The header and swig files are as follows:

paska.h :

typedef void (handleri)(int code, char* codename);

// handleri is now an alias to a function that eats int, string and returns void

void wannabe_handleri(int i, char* blah);

void handleri_eater(handleri* h);

paska.i :

%module paska

%{ // this section is copied in the front of the wrapper file
#define SWIG_FILE_WITH_INIT
#include "paska.h"
%}

// from now on, what are we going to wrap ..

%inline %{
// helper functions here

void wannabe_handleri(int i, char* blah) {
};

void handleri_eater(handleri* h) {
};

%}

%include "paska.h"

// in this case, we just put the actual .cpp code into the inline block ..

Finally, I test in python ..

import paska

def testfunc(i, st):
  print i
  print st

paska.handleri_eater(paska.wannabe_handleri(1,"eee")) # THIS WORKS!

paska.handleri_eater(testfunc) # THIS DOES NOT WORK!

The last line throws me "TypeError: in method 'handleri_eater', argument 1 of type 'handleri *'"

Is there any way to "cast" the python function to a type accepted by the SWIG wrapper?

El Sampsa
  • 1,673
  • 3
  • 17
  • 33
  • alas, in http://www.swig.org/Doc2.0/SWIGDocumentation.html#SWIG_nn30 it is said that "SWIG provides full support for function pointers provided that the callback functions are defined in C and not in the target language" .. so I guess its impossible. Any ideas how to do the callback logic from python appreciated though.. – El Sampsa Dec 23 '15 at 23:12
  • Definitely not impossible, we can customise everything, but since you're using C++ not C why not use `std::function`? That makes writing a cleaner answer simpler - if you're happy with that change I'll write up a complete solution for you. (Essentially a Python version of http://stackoverflow.com/a/32668302/168175, or a generalised version of http://stackoverflow.com/a/11522655/168175) – Flexo Dec 24 '15 at 21:26
  • Failing that do the real callbacks get a `void*` argument that you get to set when you register them? That makes life much cleaner. – Flexo Dec 24 '15 at 21:31
  • I meet the same problem and could you please show your final solution? – Xingjun Wang May 15 '18 at 12:17
  • In my final solution, I decided not to pass python functions to the cpp level, but to do everything in the cpp level. :) – El Sampsa May 15 '18 at 13:21

2 Answers2

4

Seems to me that a combination of ctypes and a SWIG typemap would be the easiest way to solve the problem. ctypes makes it easy to generate a C function that calls a Python callable. The Python code should be along the lines of:

import example

# python callback
def py_callback(i, s):
    print( 'py_callback(%d, %s)'%(i, s) )

example.use_callback(py_callback)

On the SWIG side we have: (1) a Python function use_callback that wraps the Python callback with a ctypes wrapper, and passes the address the wrapper as an integer to _example.use_callback(), and (2) a SWIG typemap that extracts the address and casts itto the appropriate function pointer.

%module example

// a typemap for the callback, it expects the argument to be an integer
// whose value is the address of an appropriate callback function
%typemap(in) void (*f)(int, const char*) {
    $1 = (void (*)(int i, const char*))PyLong_AsVoidPtr($input);;
}

%{
    void use_callback(void (*f)(int i, const char* str));
%}

%inline
%{

// a C function that accepts a callback
void use_callback(void (*f)(int i, const char* str))
{
    f(100, "callback arg");
}

%}

%pythoncode
%{

import ctypes

# a ctypes callback prototype
py_callback_type = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)

def use_callback(py_callback):

    # wrap the python callback with a ctypes function pointer
    f = py_callback_type(py_callback)

    # get the function pointer of the ctypes wrapper by casting it to void* and taking its value
    f_ptr = ctypes.cast(f, ctypes.c_void_p).value

    _example.use_callback(f_ptr)

%}

You can find this complete example with a CMakeLists.txt file here.

edit: incorporated @Flexo suggestion to move the Python part into the %pythoncode block of the SWIG file.

edit: incorporated @user87746 suggestion for Python 3.6+ compatibility.

sterin
  • 1,898
  • 15
  • 13
  • 1
    I like using ctypes like that. I think you can make the SWIG/Python bit slightly neater by hiding all the ctypes bit inside a `%pythoncode %{ ... %}` block so that from the user of the Pyhton module's perspective they just call `use_callback` with a Python callable and everything else just happens under the hood. – Flexo Dec 24 '15 at 21:45
  • @Flexo, good suggestion, I've incoporated it into the example. – sterin Dec 24 '15 at 21:59
  • A follow-up question: what happens at "f = py_callback_type(py_callback)" .. does the python code get converted to machine code or something like that..? How does this thing work under the hood? – El Sampsa Feb 03 '16 at 09:12
  • The `ctypes` module uses [`libffi`](https://en.wikipedia.org/wiki/Libffi) to do that. From the Wikipedia page: libffi can produce a pointer to a function that can accept and decode any combination of arguments defined at run time. – sterin Feb 03 '16 at 22:08
  • 1
    Just a note, for python 3.6, you need to use the typecast: `PyLong_AsVoidPtr` instead of `PyInt_AsLong` – hmaarrfk Jan 12 '18 at 18:19
  • Thanks @user87746 , I've incorporated your suggestion. – sterin Jan 12 '18 at 21:25
3

You can implement the callback logic in Python by using "directors".

Basically, instead of passing callback functions, you pass callback objects instead. The base object can be defined in C++ and provide a virtual callback member function. This object can then be inherited from and the callback function overwritten in Python. The inherited object can then be passed to a C++ function instead of a callback function. For this to work, you need to enable the director feature for such a callback class.

This does require changing the underlying C++ library, though.

m7thon
  • 3,033
  • 1
  • 11
  • 17
  • Thanks for informing me about the directors.. by now, I have found them very usefull. .. but I still don't have the slightest idea how they work..! Is the python code "compiled" at some stage and passed to C, or does swig embed the python "runtime environment" into the wrapped C-code (i.e. does it execute "PyRun_SimpleString" and the like from C). I am baffled. – El Sampsa Feb 03 '16 at 09:10
  • I also only know as much as it says in the SWIG manual. I am sure no python code is ever compiled. But python code is obviously being called from C. Does this require embedding the python runtime environment? – m7thon Feb 03 '16 at 09:29
  • hmm.. I guess it calls the PyObject_CallMethod of the python C api in the wrapper – El Sampsa Feb 17 '16 at 20:27