The majority of all firmware is written in C (or C++), but for writing small utility and automation scripts on the host machine, Python is usually the go-to language. I was recently presented with the problem of running some of our firmware algorithms on a set of pre-recorded data, as part of our suite of automation scripts written in Python. Instead of translating the C code to Python – which could quickly become a maintenance nightmare – I opted for creating an interface so the C code could be called directly from a Python script. Luckily, Python has plenty of support for this.
Python bindings
To create Python bindings for C or C++ code, there a several packages to choose from such as ctypes, CFFI, Cython, PyBind11, Boost.Python and more. You can try these out for yourself, but I am going to keep it simple and use the built-in ctypes
package.
The steps required for executing a C function in Python is as follows:
- Load a dynamic-link library (DLL) with the function you need.
- Specify the return type and the argument types.
- Call the function with the correct data types.
So, before writing any Python code, let us start by making a simple C library.
Compiling a C function into a dynamic-link library with CMake
As a basic example, we will create a library that contains just a single function, scale()
, that simply scales an array of numbers by a given scaling factor. It might look something like this:
#ifndef SCALE_H_
#define SCALE_H_
#include <stdint.h>
void scale(float *source, float *destination, uint32_t length, float scaling_factor);
#endif // SCALE_H_
and implemented like this:
#include "scale.h"
void scale(float *source, float *destination, uint32_t length, float scaling_factor)
{
for (uint32_t i = 0; i < length; i++)
{
destination[i] = source[i] * scaling_factor;
}
}
Now we will create a CMakeLists.txt
to build the shared library:
cmake_minimum_required (VERSION 3.24.1)
project(scale)
set(SOURCES
scale.c
)
add_library(scale SHARED ${SOURCES})
Then build the DLL by first running cmake -Bbuild -G"MSYS Makefiles"
(I am using MSYS on Windows, but use whatever generator you have, of course) and then cmake --build build
. We should end up with a libscale.dll
in the build
folder which we are going to use in the Python script.
Calling the C code from Python using ctypes
Now, let us create a Python script scale.py
. Following the steps listed previously, we come up with the following script:
import ctypes
import pathlib
if __name__ == '__main__':
# Import the library
lib_path = pathlib.Path("build/libscale.dll")
dll = ctypes.CDLL(str(lib_path.resolve()))
# Specify return type and argument types
dll.scale.restype = None # Function returns void
dll.scale.argstype = [ctypes.POINTER(ctypes.c_float),
ctypes.POINTER(ctypes.c_float),
ctypes.c_uint32,
ctypes.c_float]
# Create arbitrary input and allocate memory for the output
input = [1,2,3,4,5]
scaling_factor = 10
output = (ctypes.c_float * len(input))()
# Call the function with the arguments correctly casted to ctypes
dll.scale((ctypes.c_float * len(input))(*input),
output,
len(input),
ctypes.c_float(scaling_factor))
# Cast the output back to a Python list and print the results
output = list(output)
print(output)
Now when running the script, you should see the output printed as the input array scaled by 10:
[10.0, 20.0, 30.0, 40.0, 50.0]