This blog introduce how to use ctypes to access C code from Python.
1 C code
Here, we use the same C code as my previous blog:
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define PI 3.1415926535
void mycfunc(double *array_d, int *array_i, double d, int i, char *array_c, double *output)
{
if (strcmp(array_c, "cos") == 0)
{
output[0] = cos(array_d[0] + array_i[0] * PI);
output[1] = cos(d + i * PI);
} elif (strcmp(array_c, "sin") == 0) {
output[0] = sin(array_d[0] + array_i[0] * PI);
output[1] = sin(d + i * PI);
}
}
2 Compile
We can use Python to compile the C file into shared library.
import os
# Set the current working directory as your folder
# os.chdir('your folder path')
# on Windows
os.system('gcc -shared -o cfunction.dll cfunction.c')
# on Linux or Mac
os.system('gcc -shared -o cfunction.so cfunction.c')
Or you can use system command achieve it.
# cd to your folder
# on Windows
gcc -shared -o cfunction.dll cfunction.c
# on Linux or Mac
gcc -shared -o cfunction.so cfunction.c
3 Load
Assuming that the py code is located in the same folder, then we can load the shared object file by:
import ctypes
# on Windows
_myfun = ctypes.CDLL('./cfunction.dll')
# on Linux or Mac
_myfun = ctypes.CDLL('./cfunction.so')
4 Specifying the required argument types (function prototypes) and Return types
We can specify the required argument types of functions exported from DLLs by setting the argtypes attribute. In addition, by default functions are assumed to return the C int type. Other return types can be specified by setting the restype attribute of the function object.
# set the argument type
_myfun.mycfunc.argtypes = [ctypes.POINTER(ctypes.c_double),
ctypes.POINTER(ctypes.c_int),
ctypes.c_double,
ctypes.c_int,
ctypes.POINTER(ctypes.c_char),
ctypes.POINTER(ctypes.c_double)]
# set the return type
_myfun.mycfunc.restype = None
Where ctypes.POINTER creates and returns a new ctypes pointer type.
The underlying C code expects to receive a pointer and a length representing an array. However, from Python side, a Python “array” can be a list, a tuple, a numpy array and so on. The following DoubleArrayType class shows how to handle this situation.
class DoubleArrayType:
def from_param(self, param):
typename = type(param).__name__
if hasattr(self, 'from_' + typename):
return getattr(self, 'from_' + typename)(param)
elif isinstance(param, ctypes.Array):
return param
else:
raise TypeError("Can't convert %s" % typename)
# Cast from array.array objects
def from_array(self, param):
if param.typecode != 'd':
raise TypeError('must be an array of doubles')
ptr, _ = param.buffer_info()
return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
# Cast from lists/tuples
def from_list(self, param):
val = ((ctypes.c_double)*len(param))(*param)
return val
from_tuple = from_list
# Cast from a numpy array
def from_ndarray(self, param):
return param.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
Class IntArrayType and CharArrayType can be similarly defined. Then, we can set the required argument types as
_myfun.mycfunc.argtypes = [DoubleArrayType(),
IntArrayType(),
ctypes.c_double,
ctypes.c_int,
CharArrayType(),
DoubleArrayType()]
5 Wrapper function
It is common to write a small wrapper function for our C code.
import numpy as np
def myfun(x, y, a, b, s):
output = np.asarray(range(2)).astype(float)
_myfun.mycfunc(x,
y,
ctypes.c_double(a),
ctypes.c_int(b),
str.encode(s),
output)
return output
One more thing
If you want to sent a matrix from Python to C, make sure it is contiguously stored. For numpy array, we can use following code to convert.
if not x.flags['C_CONTIGUOUS']:
x = np.ascontiguous(x, dtype=x.dtype)