Wednesday, March 2, 2011

Behold the power of ctypes

Despite uncertainty over when the University year will resume here, I've been chipping away at a little assignment that was handed out on the first day of term (to be precise, it was the first lecture I had this year, and also the last course I had a lecture for so far, about 1 hour before the quake struck). Anyways, getting back to the main story for today.

I started by coding out some of the more performance-intensive parts in C, with the original intention of doing everything from there too. However, I soon started coding the rest in my favourite Python as more of a "prototype" to see where things were going in the broader sense. Python is good this way: it's simple enough to not have to worry about some low-level details immediately while concentrating on some more fundamental issues, and also having powerful+flexible+readable yet concise syntax to express this with. Perhaps I'm also a bit biased, having effectively learnt to program using Python (technically I started using Visual Basic, but that was mostly just doing heaps of GUI stuff, and most of that wasn't even actually writing code!), especially considering the apparent phenomenon where most people will revert to the native tongue when faced with challenging enough problems (like mental arithmetic IIRC).

Not wanting to recode those C-coded parts again in Python (and risk introducing further errors), I started looking at options for running those C-functions from Python instead. In this case, it was a specific pseudo-random-number-generator (PRNG) which we needed to use.


Evaluating my options, I found that there were basically two possibilities:
1) Code up a set of Python wrappers for the functions, and compile those into a Python extension module
2) Use "ctypes", compiling the existing code into a .dll

Studying the work required for the first option, it was quickly obvious that it was going to be quite a lot of work, which in the end might not be worth much, and/or I'd be facing down some Py-wrapping bugs in my own code. Previous experience playing with the BPy wrapping in Blender suggested that perhaps doing this may not be such a pretty process...

The second option in comparison was quite easy after seeing a simple example or 2 about it. It's as simple as 1, 2, 3:
1) Compile your existing C-code into a .dll or .so
2) Somewhere in your Python code, include a line like
dll = cdll.LoadLibrary(path_to_lib)   # where cdll is ctypes.cdll
3) Everytime you want to run some code from your C-library, just append dll. (or whatever you called the output from the call in step 2), and wrap all arguments you pass in with c_argType, where argType is type of the parameter in the C-code function definition. For example:
If you had a C-function define like so,
int someFunc(int a, float x)
you'd call it in Python like
retval = dll.someFunc(c_int(my_int_val), c_float(my_float_val))

The docs are a good source of reference when comes to this last stage, with regards to how you go about setting up things to access the underlying data.

Now, regarding compiling the code as a .dll, for once I just took an easy-option (since I haven't done this before) and used CodeBlocks to generate a "Shared Library" project, which just compiled the C-code into a .dll without any extra effort or complaints. But perhaps if I spent a bit more time reading compiler docs, I might've found the magic commands to script-up as well.

So, in conclusion, the "best" option for general-purpose access of your own C-functions from within Python scripts is to just compile it into a .dll/.so and use ctypes to access it. However, if you want to do a lot of manipulation of your Python objects (i.e. classes) using C-code for whatever reason, then perhaps the CPython wrapper approach might be better suited in the long run.

I'm just glad to now have a simple wrapper for my C-coded PRNG from within Python now. I'll make this code available on request, if anyone is interested :)


  1. ..and somewhere a Professor is going "Dratt, this should've kept them busy for months!"

  2. Do you know that you can decorate your function to auto cast the argument ?

    dll.someFunc.restype = ctypes.c_int
    dll.someFunc.argtypes = ctypes.c_int, ctypes.c_float

    dll.someFunc(5, 10) (will cast to 5, 10.0f)
    dll.someFunc(5.5, 10.0) (will cast to 5, 10.0f)

    Enjoy ctypes ;)