Analyse and test C with Python
I've recently been implementing data structures in C. Writing C is fun and educational, because it's lower level than most other common languages. It's fast, and writing it teaches you to understand what's happening at a machine level.
However, C is also relatively difficult to write, making it harder to analyse and test. It would be helpful to be able to do this with a higher level language, such as Python. Analysis and testing don't affect performance of the actual data structure, so using a slower but easier and more productive language for this seems reasonable.
In this article, we walk through a simple example of doing this with a built-in
Python library for interfacing with C called ctypes
.
Example C code
For this example, I've written a simple (but unsafe) stack data structure, which defines the following API:
// stack.h
typedef struct {
int stk[100];
int top;
} stack;
void stack_push(stack* s, int x);
int stack_pop(stack* s);
The stack's source code is included below1.
Shared library
ctypes
interacts with shared libraries. We can compile our stack to a shared
library with:
$ clang -shared -o stack.so -fPIC stack.c
Python wrapper
Next, we need to define a wrapper module2, which creates the Python objects used to interact with the shared library.
# stack_wrapper.py
import ctypes
class stack(ctypes.Structure):
_fields_ = [
("stk", ctypes.c_int*100),
("top", ctypes.c_int),
]
_lib = ctypes.cdll.LoadLibrary("stack.so")
new = _lib.stack_new
new.restype = ctypes.POINTER(stack)
push = _lib.stack_push
push.argtypes = [ctypes.POINTER(stack), ctypes.c_int]
pop = _lib.stack_pop
pop.restype = ctypes.c_int
pop.argtypes = [ctypes.POINTER(stack)]
Note: It seems idiomatic from examples in the Python documentation to use
the global import from ctypes import *
to reduce code verbosity. I haven't
done this here to make it clear what objects come from ctypes
.
Use
It's now simple to import and use this module:
>>> import stack_wrapper as stack
>>> s = stack.new()
>>> stack.push(s, 5)
>>> stack.push(s, 15)
>>> stack.pop(s)
15
>>> stack.pop(s)
5
We're now fully in Python land, with a set of objects which let us call into our C code. These objects are native Python, and can be used in any way that native Python objects can be used. We can use them with:
unittest
orpytest
for testingtimeit
for benchmarkingbokeh
ormatplotlib
for analyticsAFL
for fuzz testing
-
Warning: This stack implementation was written to be as simple as possible, and isn't really safe to use.
↩︎// stack.c #include <stdlib.h> #include "stack.h" stack* stack_new() { stack* s = malloc(sizeof(stack)); s->top = -1; return s; } void stack_push(stack* s, int x) { s->top++; s->stk[s->top] = x; } int stack_pop(stack* s) { int x = s->stk[s->top]; s->top--; return x; }
-
When using
ctypes
, we end up writing a Python wrapper module, which is basically a Python translation of the C header file. Other libraries for calling into C try to remove this repetition. One example iscffi
, written by the PyPy team. ↩︎