Over the past ten years I’ve used Python–with varying amounts of success–to control scientific instruments: scientific CCD and CMOS detectors, frame grabbers, analog and digital I/O cards, GPIB devices, and many others. While I’ve usually been able to make things work, I’ve never developed a complete (or near complete) understanding of how they work. This post is intended to give a decent description of some of the methods/strategies/architectures that have worked, using toy examples based on simulated instrumentation libraries.
I have developed the examples in Linux (#!, specifically, though they should work on any Linux system with the correct tools installed). At some point I may try to port the examples, and tutorial, to Windows. If so, I’ll annotate the tutorial accordingly.
Here’s an outline of what’s going to happen:
- Interacting with shared libraries in C++. First I’ll give a simple introduction to developing a C++ application. I suspect that most users of this tutorial will be scientists who have used the Scipy stack to do simulation and data analysis and who are now interested in using Python to automate data acquisition. If this describes you, you’ve probably learned and forgotton some C++ along the way; to effectively write Python instrumentation code you need to know a tiny bit of C++, but you need to know it very well. This portion of the tutorial will be built around a pretend instrument, a camera or frame grabber, which will have pretend functionality similar to a real frame grabber API.
- ctypes. In this section, I’ll describe how to use Python, in conjunction with cytpes, to call functions in the foreign frame grabber library we created in 1 above.
In future posts I’d like to cover some alternative approaches, viz. Cython and SWIG, and compare them with one another and with ctypes:
- Cython. In this section, I’ll describe how to use Cython, along with distutils, in order to use in-line calls to functions in the toy frame grabber created in 1 above.
- SWIG. In this section I’ll describe how to wrap foreign libraries using the extremely powerful Simplified Wrapper and Interface Generator (SWIG) in order to wrap our frame grabber functions.
- Comparison of approaches. Finally, I’ll describe the advantages and disadvantages of each of the approaches described.
Here’s a list of what you’ll need to have installed in order to do the whole tutorial:
- python (I’m using 2.7–other versions may work, but early versions (<2.5) require separate installation of the ctypes package)
The first part of this article
Shared libraries in C++
Hardware devices often ship with software development kits (SDKs), meant to be used by the programmer to develop control software for the device or integrate the device with existing software. In the simplest case, the SDK may consist of an API to a shared library, usually distributed along with a header file. More often the SDK consists of multiple shared libraries and header files, but we’ll keep things simple in the example below.
Though you won’t normally start with source code for the driver library [^1], we’ll start there in order to develop a somewhat complete picture of how the library is built. You may download all of the source code here and here. Our example consists of a simple frame grabber API, with functions for exposing the camera (
expose), getting the size of the frame (
getSize), and getting the image (
exposeInto), as well as a setter and getter for a dummy float variable
exposureTime. Additionally it has some preprocessor definitions specifying the output of the simulated camera (sensor geometry and pixel bit depth), an array
frame representing the pixel values of the sensor, an array for building strings (
buffer), and a utility funciton for printing the sensor data (
printFrame). Our shared library will be called
libframegrabber, and here is the header file
#include <stdlib.h> #include <time.h> #include <stdio.h> #define MAX_INTENSITY 255 #define SENSOR_ROWS 512 #define SENSOR_COLS 512 static int frame[SENSOR_ROWS*SENSOR_COLS]; static char buffer; static float exposureTime = 1.0; extern "C" void getID(int *f); extern "C" void expose(void); extern "C" int * getFrame(void); extern "C" void copyFrame(int *f); extern "C" void writeInto(int *f); extern "C" int getSize(void); extern "C" void setExposure(float newExposureTime); extern "C" float getExposure(void); extern "C" void printFrame(int *f); float superSecretFunction(void);
Note that we have used the
extern keyword in the declarations of the functions we’ll want access to. This forces the C++ compiler not to mangle the names of the functions in the assembly language symbol table, allowing (among other things) other languages (or other C++ objects) to use the unmangled names. To illustrate the point, we’ve left out the
extern keyword on the declaration of the function
superSecretFunction. The implementation of the framegrabber library (in
libframegrabber.cpp) is simple.
expose fills the array
frame with random numbers, limited by the constant
getSize returns an integer representing the number of pixels in the sensor array.
getFrame returns a pointer to the array
setExposure functions get and set
main.cpp contains a small, executable test program, that calls
libframegrabber functions at run time.
As I mentioned above, if we are given the source code for the driver (
libframegrabber.cpp), we can proceed a couple of ways. The simplest way to proceed would be to compile the executable and library together:
g++ -o myprogram main.cpp libframegrabber.cpp
This would result in a program
myprogram, which when run would print ten frames to stdout. This may be the best way to proceed if you have the source code for a driver, but it’s not the topic of this tutorial.
Instead of compiling an executable program, we want to compile the library
libframegrabber.cpp into a shared object named
libframegrabber.so. (Shared object files have a very nice, albeit involved, standard of naming and linking. For a complete description, read this. Also, the location of
.so files is standard, and managed by the environment variable
LD_LIBRARY_PATH; for the purposes of this example we’re ignoring all of these subtleties–all of our files are in a single directory.)
Compiling a shared object is a two step process: first, the compiler is asked to compile the library without linking, which produces a
.o object file, containing position independent machine code using the
-fPIC option. Position independence is important for shared libraries because there’s no way to know before runtime where in RAM the library will reside. The second step converts the object file into a shared object.
g++ -Wall -fPIC -c libframegrabber.cpp g++ -shared -o libframegrabber.so libframegrabber.o
libframegrabber.so is the shared library containing the functions we’d like to have access to. We can list the symbols in the file using the
nm command. We may invoke it with
-g to show only external symbols, but let’s make sure not to use the commonly used
-C demangling option:
nm -g libframegrabber.so
A portion of the resulting output lists the available functions:
0000000000000b87 T _Z19superSecretFunctionv 00000000000009a1 T copyFrame 0000000000000982 T expose 0000000000000adb T getExposure 0000000000000994 T getFrame 000000000000096c T getID 0000000000000abc T getSize 0000000000000aef T printFrame 0000000000000ac7 T setExposure 0000000000000a14 T writeInto
The first two columns are the hex encoding of the symbol’s address and the symbol type (
T, which indicates that these are from the text of
nm is not necessary, but it can be a useful way to expose the function names of a compiled shared object file. These should correspond to the function names in the provided (usually)
.h header file and/or API documentation. If neither of these are available (or, as is sometimes the case, the documentation contains errors),
nm may be the only way to discover the true function names.
Windows does not have
nmbuilt in, but there is a very nice (and safe/clean) freeware program called dllexp that does much of the same work, with a GUI to boot.
Notice that most of the function names are the same in both the source code and the symbol table of the compiled shared object. This is the intended effect of using
extern "C" in the function declarations. The function for which we did not specify
extern "C" was
superSecretFunction; its name in the shared object’s symbol table is
_Z19superSecretFunctionv–a mangled [^2] version of its source code name. If the names in your shared object are mangled, you can either 1) use the mangled names in your Python; 2) recompile the driver using
extern "C" where appropriate (if you have the driver source); or 3) wrap the shared library with another shared library, using the mangled names on one side of the interface and exposing unmangled names on the other side. The last option is the more roundabout than the first, but will result in more readable Python.
At this point we’re in the shoes of a scientist who has received her new cutting-edge device with a shared object driver and wants to use Python to use the device.
Of the three interface approaches, ctypes is probably the simplest. ctypes is a package that is part of the standard (“pure”) Python distribution, so everyone has it by default. The example code links provided above include a Python script
example_ctypes.py containing all the following content. In this section, though, I give interactive terminal examples (i.e. those beginning with
>>>) meant to illustrate the ideas and show output of small chunks of code.
Loading a library and calling library functions
First, we import
ctypes and use it’s
LoadLibrary function to gain access to the shared library:
>>> import ctypes >>> framegrabber = ctypes.cdll.LoadLibrary('./libframegrabber.so')
Here we’ve supplied the relative path to the share object file, which is fine for our example. Normally, however, you would omit the
./ and allow the OS to choose the library.
In Windows you needn’t give the whole DLL filename; it is customary to omit the
.dllextension and allow Windows to figure it out.
Now, the frame grabber functions are available in Python, and calling one is as easy as:
>>> frameSize = framegrabber.getSize() >>> frameSize
This will cause the Python interpreter to output the product of SENSORROWS and SENSORCOLS, as defined in the C++ function. Notice that those were defined by macro (i.e. preprocessor directives). In other words, the compiler knows nothing about the value of those constants, and the constant names appear nowhere in
libframegrabber.so. In still more other words, there is no way to automatically and programatically get the numbers of rows and columns from Python. This illustrates an important point: any
#define constants that you need in your Python script must be explicitly defined in Python, using the header file (hopefully) supplied with the driver to determine the values.
>>> SENSOR_ROWS = 512 >>> SENSOR_COLS = 512
Now let’s imagine that we looked at the header file and saw the signature for
superSecretFunction. If we tried to call it the same way:
we get an error
AttributeError: libframegrabber.so: undefined symbol: superSecretFunction. Upon receiving this error, we would decide to
nm the shared object file, as above, and we would discover its mangled name
_Z19superSecretFunctionv. If we don’t especially care about our Python readability, we might just use the mangled name:
Argument and return types
From the header file we know that the default exposure time is 1.0, but if we call
>>> framegrabber.getExposure() 1065353216
we do not get 1.0. This is because by default functions in the shared object are assumed to return integers. In order to override this default behavior, we must set the return type of the function, which is an attribute
restype of the function object. We will set it to
c_float. A full list of the ctypes fundamental data types is here.
>>> framegrabber.getExposure.restype = ctypes.c_float
Now if we call the function again, it returns the correct value of 1.0. An analogous issue arises with passing arguments to library functions, which are assumed to require integers. Let’s say we want to set the exposure time to 2.5. If you pass 2.5 (not a Python integer) to
setExposure, for instance, a
ctypes.ArgumentError is returned because ctypes doesn’t know what to do with a non-integer, regardless of what the library function requires. One way to solve this problem is to instantiate a ctypes data object (e.g.
c_float) with the desired value of 2.5, and pass that to the library function:
>>> framegrabber.setExposure(ctypes.c_float(2.5)) >>> framegrabber.getExposure() 2.5
A second alternative is to tell ctypes what to do with any value you pass to the function, by setting the
argtypes attribute of the function.
argtypes must be a list, with one ctypes fundamental type for each argument in the function signature. For example:
>>> framegrabber.setExposure.argtypes = [ctypes.c_float] >>> framegrabber.setExposure(10.0) >>> framegrabber.getExposure() 10.0
In practice I usually set the
argtypes attributes of all the functions I need right after instantiating the library. While you can skip setting
argtypes, we see its value with a quick example:
>>> framegrabber.setExposure.argtypes =  # restoring argtypes to its default value, an empty list >>> framegrabber.setExposure('snafu') 0 >>> framegrabber.setExposure.argtypes = [ctypes.c_float] >>> framegrabber.setExposure('snafu') Traceback (most recent call last): File "<stdin>", line 1, in <module> ctypes.ArgumentError: argument 1: <type 'exceptions.TypeError'>: wrong type
argtypes, we don’t get any usable feedback from calling
setExosure with the wrong argument type. Once
argtypes is set, though, we get a useful exception.
Working with pointers
In my experience, many hardware drivers require the management and passing of pointers, i.e. addresses of data. If you’re unfamiliar with pointers, have a quick look here or read this[^3].
Let’s look at the signature of
void getID(int * f). This kind of signature is very frequently observed in hardware APIs. Such functions are efficient because they avoid data copying–data are written directly into the memory address. This allows both the Python interpreter and the shared object to interact with the same data in RAM without having to manage two separate copies, a crucial capability for real time instrumentation software. Of course these advantages are negligible for funcitons like
getID, but it’s a simple way to illustrate the point. It takes an integer pointer and (presumably) writes the frame grabber ID into that memory location. In order to specify the
argtypes for this function, we must use
ctypes.POINTER, which takes a ctypes type (e.g.
c_int) as an argument, and returns an object representing a C or C++ pointer:
>>> framegrabber.getID.argtypes = [ctypes.POINTER(ctypes.c_int)]
As with the
setExposure example above, we could have gotten by without setting
argtypes, as long as we don’t make any mistakes calling
getID. Those of us who make mistakes probably should set it. Next we have to make a C integer:
>>> cId = ctypes.c_int()
Finally, we have to pass a
cId-pointer to the library function
ctypes.byref() takes ctypes data and returns pointers to those data.
>>> framegrabber.getID(ctypes.byref(cId)) >>> print cId c_int(123) >>> print cId.value 123
I noticed, by mistake, that the following works too:
>>> framegrabber.getID(cId) >>> print cId c_int(123)
In all honesty, I don’t know why it works. Either it’s a bug (unlikely) or an intentional shortcut. I think it’s worth avoiding this shortcut because it’s confusing to non-programmers like me.
Working with pointers and Numpy arrays
Our frame grabber library provides three functions for getting an image:
exposeInto, each of which demonstrates a distinct logic of availing instrument data to Numpy:
The library copies data from one pointer (a C++ pointer) to another (a Numpy pointer):
copyFrame‘s signature is
void copyFrame(int * f). Before proceeding, we must point out the close relationship of a pointer to a datum of a particular type and an array of data of that type.
copyFrame expects a pointer to an integer,
f, which can also be thought of as a pointer to the first member of an array of integers. The two are not technically equivalent in C++–an array declaration causes different memory allocation than a pointer declaration–but it is (probably) safe to equate them in your mind on the Python-ctypes side of things. As with all library functions, the first thing to do is write the
restype attributes. Here we have only
argtypes. As with
getID above, we could specify
argtypes to be a pointer to a ctypes native type[^4], but an easier approach is to utilize Numpy’s own ctypes convenience methods, since (presumably) you want the data to be Numpy-friendly in the end. We will use
numpy.ctypeslib.ndpointer, which allows a specification of the array data type (
dtype), number of dimensions (
shape as keyword arguments. Just as with single-value functions above, specification of the
argtypes (which you can also think of as re-prototyping the function for Python) protects us from passing a pointer to a bad (mistyped or wrong-sized) array, which would normally cause a graceless crash via segmentation fault. Thus:
>>> import numpy as np >>> pixelDataType = np.int32 >>> framegrabber.copyFrame.argtypes = [np.ctypeslib.ndpointer(dtype = pixelDataType, ndim = 2, shape = (SENSOR_ROWS,SENSOR_COLS))]
Notice that we’re reshaping on the fly: the library header file is indifferent to whether the array has 1 or 2 dimensions[^5], so we might as well get the data like we’re going to use it. Unlike the library, Numpy does care about the shape, and once we’ve told it that we’re going to be sending 2D arrays of 512×512, it will complain if we send something else, which is what we want. We needn’t specify
restype. Next, we make a suitable, empty Numpy array, and then call the function:
>>> frameByCopy = np.empty((SENSOR_ROWS,SENSOR_COLS), dtype = pixelDataType) >>> framegrabber.copyFrame(frameByCopy)
Now you have full Numpy access to a frame of data from the camera. This works, but it’s not the most efficient method for getting a frame. Under the hood (in
libframegrabber.cpp) this library call works by copying each pixel from the library’s internal representation (
frame) to the pointer
copyFrame receives (
*f). The copying happens at C-speed (fast), but it’s not the fastest thing we can do.
The library returns a pointer to the data, which Numpy treats as a Numpy array:
getFrame‘s signature is
int * getFrame(void). It returns a pointer to the pixel data. The crucial step in this scenario is to instruct ctypes to treat the returned pointer as a Numpy array, and this is done by properly setting the
restype (return type) for the fuction:
>>> framegrabber.getFrame.restype = np.ctypeslib.ndpointer(dtype = pixelDataType, ndim = 2, shape = (SENSOR_ROWS,SENSOR_COLS))
Again, we could have made the shape
SENSOR_ROWS * SENSOR_COLS instead, more closely following the one-dimensional data structure in the library, but we avoid a later call to Numpy’s
reshape by instructing
getFrame to return a two-dimensional array. Now, getting the data is a simple matter of calling the library function:
>>> frameByReference = np.getFrame()
The library asks for a pointer into which to write the data upon exposure, and we provide a Numpy array:
exposeInto‘s signature is
void copyFrame(int *f), which is identical to
copyFrame‘s, and we use the function just the same way, by setting
argtypes, creating a corresponding Numpy array, and then passing it to the library function:
>>> framegrabber.writeInto.argtypes = [np.ctypeslib.ndpointer(dtype = pixelDataType, ndim=2, shape=(SENSOR_ROWS,SENSOR_COLS))] >>> frameByWrite = np.empty((SENSOR_ROWS,SENSOR_COLS),dtype=pixelDataType) >>> framegrabber.writeInto(frameByWrite)
Efficiency of the above approaches:
The slowest of the three approaches is the first, copying data fromone pointer ot another. The second and third approaches are very similar in efficiency, with neither requiring a copy of the data.
getFrame is slightly slower because it requires the Python interpreter to allocate memory each time it’s called (via redefinition of
writeInto is the fastest because it allocates memory for the array once and repeatedly writes into it. The speedup between
copyFrame and the others is significant (nearly 50% faster on my machine) whereas
writeInto is only 2% faster than
While I’m not including any in-line citations, most of this post was stolen whole-cloth from the articles below.
- A quick and dirty ctypes tutorial
- A ctypes tutorial much clearer and more concise than this one
- Another quick ctypes tutorial
- How to create a shared object file
- Detailed description of shared libraries
- Comparison of three approaches
- Numpy and ctypes exampes – copyFrame works just like the
fibmatrixfunctions in this example.
[^1]: Less commonly, an SDK may consist of an API to source code, intended to be compiled together with the instrumentation application. I have encountered this once, with the manufacturer of a high-speed CCD sensor. Its advantage is that you can (theoretically) compile your application for any machine architecture, provided that you satisfy all the dependencies. Nevertheless, I decided to use Python instead of writing the entire application in C, which is what the manufacturer had intended. In order to use Python I compiled a shared library using the supplied source code, and then called the library functions from Python, making the situation identical to the example described here.
[^2]: Names are mangled in order to prevent functions with the same name but different signatures from being given the same names in the symbol table. Indeed it is not possible to expose two signature-differentiated functions using
extern. This illustrates why it’s important not to use
nm -C to show an object file’s symbol table; it can trick you into thinking that demangled names are available to callers.
[^3]: Briefly, a pointer is a number. The type of the number (i.e. the maximum and minimum allowable values, number of bits used to store the value, whether it can be negative, whether it can be a fractional value, etc.) varies among operating systems, so it’s best to think of its type as ‘pointer’. In spite of this, there’s nothing mysterious about it–it’s just a number. You can do all kinds of things with this number: add to it or subtract from it, print it, etc. Pointers represent locations in the computer’s memory. If you have a pointer you can go to the specified location in memory and see what data are stored there. Of course a location in memory is just a bunch (32 or 64, e.g.) of zeros and ones; to know how to interpret them you have to know what the type of data is. In C and C++, a pointer is declared like this:
int * myPointer, which says
myPointer is an address in memory that stores an integer. If you ever want to know what value is at that address, you dereference the pointer using
int myInteger = *myPointer. Conversely, if you have a variable, say
float myFloat = 3.14159, you can get its address, a pointer, using
float * myFloatPointer = &myFloat. One thing to keep in mind is the two distinct meanings of
*. It can be used to declare a pointer and also to dereference. That’s all you need to know about pointers.
[^4]: There’s a special way to construct an array of ctypes data, e.g.
myArray = (ctypes.c_int * 10)(), which initializes an empty array of C integers, or
myArray = (ctypes.c_int * 10)(range(10)), which populates the array with 0… 9, and additional methods for converting those arrays into Numpy arrays. For our purposes, Numpy’s shortcuts are quicker and easier.
[^5]: Dimensionality in the C++ library is an abstraction anyway; all N-dimensinal arrays can be thought of as 1-d arrays with length (x * y * z * …).