Passing Python file objects across a SWIG interface
SWIG works great out of the box when you’re dealing with basic types. Once you start to wrap more complicated APIs the type conversion magic can only go so far.
I have been writing a Python SWIG interface for a library that accepts stdio FILE* pointers for input/output. Here I’ll present a minimal example where a Python file object is passed as a FILE* to a simple C function. All source is available here if you wish to follow along.
char buffer[1024]; char* message(FILE *input) { memset(buffer, 0, sizeof(buffer)); fread(buffer, sizeof(buffer), 1, input); return buffer; }
This simply reads from a file stream until it’s closed and returns what was read. I preemptively apologise for it not being thread safe.
We’ll create a module called test wrapping the C function message such that the following code (example.py) will output “Hello World”.
import os import test # Create and open pipe as file objects r,w = os.pipe() fr, fw = os.fdopen(r, 'r'), os.fdopen(w, 'w') # Write and close fw.write('Hello World') fw.close() # Read data from pipe using test module print test.message(fr)
The first cut at a SWIG interface file yields
%module test %{ #include "test.h" %} char *message(FILE *input);
Unfortunately when we build and run example.py we get a TypeError indicating the argument we passed couldn’t be converted to a FILE*.
$ python setup.py build_ext --inplace $ python example.py Traceback (most recent call last): File "example.py", line 17, in <module> print test.message(fr) TypeError: in method 'message', argument 1 of type 'FILE *'
How do we make SWIG correctly convert the Python file type to a FILE*? The solution lies in a SWIG feature called a typemap.
Typemaps allow you to write short stubs in C to convert objects from Python to C and vice-versa. Writing them requires you to have some knowledge of the Python API, for simple conversions however you can make good progress simply by digging through the Python include directory.
Adding the following typemap to our SWIG interface file tells squid how to convert incoming Python objects into FILE* pointers. Conveniently the Python C API provides the PyFile_Check function that checks if a PyObject* is of the PyFile type and PyFile_AsFile functions that returns a FILE* given a PyFile instance.
/* Converts a PyFile instance to a stdio FILE* */ %typemap(in) FILE* { if ( PyFile_Check($input) ){ $1 = PyFile_AsFile($input); } else { PyErr_SetString(PyExc_TypeError, "$1_name must be a file type."); return NULL; } }
Now when we build test and run example.py we get the desired result.
$ python example.py
Hello WorldNotice that if we pass a non-file type to message a TypeError will be raised.
>>> import test >>> test.message('not a file') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: input must be a file type.
This simple example demonstrates one powerful feature of SWIG that can be expanded to wrap complex C and even C++ APIs.
lol @ “I preemptively apologise for it not being thread safe.”