Thursday, October 08, 2009

Interfacing multi-threaded C API's with Python using SWIG ...

Ran into an interesting problem yesterday. There is a third party C library that I need to interface to using Python. Normally, this isn't a problem. Just use SWIG to make a Python Wrapper on the desired API functions and call away. But this library was a little nastier because it spawned other threads and notified the caller via callbacks. Callbacks on their own aren't really a big deal for SWIG, you just have to handle the delegation from C to Python yourself, which is easy to do in the .i file. But when the callback is coming from another thread there are some twists.

CPython is notorious for the Global Interpreter Lock (GIL). You have to release the GIL whenever you do blocking I/O and re-obtain it when you are done. With a callback it's a little different, you want to acquire the lock, call Python and release it again when your back in C-land.

Starting off, my .i file looked something like this:



%module probe
%{
#include "myapi.h"
%}

%include "windows.i"

typedef void __stdcall (*HandleDataCallBack)(char*, int);

int Start(HandleDataCallBack);
int Stop();
int SendInquiry();

%{
PyObject* python_callback = 0;

void __stdcall api_start_callback(char* foo, int blah)
{
PyObject *arglist;
PyObject *result;
PyGILState_STATE gstate;

if (!PyCallable_Check(python_callback))
{
PyErr_SetString(PyExc_TypeError, "Python callback must be callable");
return;
}
arglist = Py_BuildValue("(si)", foo, blah);

gstate = PyGILState_Ensure();

result = PyEval_CallObject(python_callback, arglist);

Py_XDECREF(arglist);
Py_XDECREF(result);

PyGILState_Release(gstate);
}

void alt_start(PyObject *pyfunc)
{
python_callback = pyfunc;
Start(api_start_callback);
Py_INCREF(pyfunc);
}
%}

void alt_start(PyObject*);
Essentially what I'm doing here is defining a new function alt_start() to replace the API Start() function. A well known callback (api_start_callback) is then handed off the the real Start(). api_start_callback() does the heavy lifting of assembling the arguments, acquiring the GIL, calling the Python callback and then giving the GIL back to Python.

For other places where the threading is an issue, SWIG can take of this itself when you run Swig with the -threads option. In my case: swig -python -threads probe.i

The simplified Python code for calling this looks something like this:
import probe
import time

def ProbeCallback(foo, blah):
....print "Foo: %s, Blah: %d" % (foo, blah)

probe.alt_start(ProbeCallback)
probe.SendInquiry()
time.sleep(60)
probe.Stop()
Someday perhaps, I'll be able to call Start() directly and pass my Python callback handler without the need for this special handling code. Until then, the work around isn't too horrible.

4 comments:

Kenny said...

Interesting.

Is it possible PyGILState_Ensure(); should be called higher up though? There's other c-api calls above.

Kenny

MyDarkSecret said...

Good point. Certainly couldn't hurt to move it up, but I don't think it's going to be an issue since it's not running the interpreter, just executing some helper functions.

Thx!

rajashree said...

Hi,
I was impressed with the details here, can you please help me answer below query -

I would like to know hot to pass address reference to ‘c’ function through python. I am using swig for ‘c’ anf python interfacing

Interface file : stud.i
***************************************************
%module stud
%header%{
#include "stud.h"
%}
%include "stud.h"
%array_functions(char *,charp );
%inline %{
extern int parse1(char *,char *,char **);
%}
%inline %{
int print_args(char **argv) {
int i = 0;
for(i=0;i<=180;i++) {
printf("argv[%d] = %c\n", i,*(argv[i]));
i++;
}
return i;
}
%}

**********************************************************

Stud.c
======================================

#include string.h
#include stdio.h
/*the angular braces are removed as here as they were causing issues in posting*/
#include "stud.h"
int parse1(char *s, char *p,char **t)
{
strcat(p,s);
*t=&p[0];
}
**********************************************************
Stud.h
===================================
int parse1(char *,char *,char **);
**************************************************
>>> from stud import *
>>> a='rajashree'
>>> c='thoratf'
>>> d=new_charp(1000)
>>> parse1(a,c,d)
2300188
>>> d
Swig Object of type 'char **' at 0x28aca0

Now I want read value of d in python… How do I do it.

Any type of help is appreciated.

Thanks,
Rajashree.
rthorat@starentnetworks.com

MyDarkSecret said...

Thanks Rajashree,

I suspect you'll need a typemap to tell Swig how to handle the char**. Looks like it doesn't know how to deal with the pointer and is keeping it as a Swig Object.

http://www.swig.org/Doc1.3/Python.html#Python_nn59

Alternatively you could wrap the char * in a simple struct and see if Swig likes that better.

Hope it helps!
-S