Tutorial: Interfacing Python and C code

Goals

In this section, we will show how to create a new Python function that makes use of C code for computations and that can be later used in a theory or view.

Python offers rapid application development because it easier to write, to read (and therefore to maintain) than C code. However, Python code used for number crunching might be 10 to 100 times slower than C when Python NumPy or SciPy libraries (written in C/C++ or Fortran) cannot be used.

Fortunately, there are many solutions available to write code that will run fast in Python. We can cite Cython or Numba that transform Python code into C executable and require minimal addition to the existing Python code. There is also Ctypes that provides C compatible data types, and allows calling functions from external libraries, e.g. calling pre-compiled C functions. It is a very effective means to communicate with existing C code.

Note

All these solutions require a C compiler installed on your machine.

In RepTate, some theories (most notably the React application theories) are written in C code and interfaced with Python using, Ctypes.

In general, already-written C code will require no modifications to be used by Python. The only work we need to do to integrate C code in Python is on Python’s side.

The steps for interfacing Python with C using Ctypes. are:

  1. write C code functions

  2. compile the C code as a shared library

  3. write some Python lines of code to “extract” the C functions from the library

  4. run!

Our C code

As an example of C function, we write a simple function that takes as input an array of double and return the square. It is evident that such a simple function (that calculates the square of an array) does not justify the use of C, but it is a good place to start.

In a new text file, write the code below and save it as basic_function.c in the folder theories/ for example:

#include <stdlib.h>

void c_square(int n, double *array_in, double *array_out)
{ //return the square of array_in of length n in array_out
    int i;

    for (i = 0; i < n; i++)
    {
        array_out[i] = array_in[i] * array_in[i];
    }
}

Compile the C code into a library

First, make sure you have a C compiler installed on you machine. Mac and Linux machines usually have a C compiler installed by default. On Windows, we suggest to install MinGW via Anaconda.

To compile the above C function into a library that can later be used by Python, open a terminal and change the working directory to the folder where basic_function.c is located. Then compile the library.

Warning

This new library can only be used by the same type of machine is was compiled on, i.e., a Windows machine creates libraries that cannot be used on Linux or Mac, and vice versa. To avoid problem, we name these library differently: we append to the library name

  • _win32 for Windows,

  • _darwin for Max,

  • _linux for Linux.

Compile the C file using either

$ gcc -o basic_function_win32.so -shared -fPIC -O2 basic_function.c # Windows

$ gcc -o basic_function_darwin.so -shared -fPIC -O2 basic_function.c # Mac

$ gcc -o basic_function_linux.so -shared -fPIC -O2 basic_function.c # Linux

This created a new file basic_function_***.so containing our C function in the RepTate folder theories/.

Use a C library in Python

Read the C library

We use the basic_function.so library via Ctypes. In a new file, write the following and save it as, for example, basic_function_helper.py

 1"""
 2Define the C-variables and functions from the C-files that are needed in Python
 3"""
 4from ctypes import c_double, c_int, CDLL
 5import sys
 6
 7lib_path = 'theories/basic_function_%s.so' % (sys.platform)
 8try:
 9    basic_function_lib = CDLL(lib_path)
10except:
11    print('OS %s not recognized' % (sys.platform))
12
13python_c_square = basic_function_lib.c_square
14python_c_square.restype = None

Some explainations:

from ctypes import c_double, c_int, CDLL

imports the Python Ctypes object we will be needing.

lib_path = 'theories/basic_function_%s.so' % (sys.platform)

defines the path of our library file, and sys.platform returns either win32, darwin or linux. Note that the path is relative to the path of RepTate.py.

basic_function_lib = CDLL(lib_path)

defines the Python object square_lib where all the functions and variables from our C file basic_function.c are stored. In particular, the function c_square

python_c_square = basic_function_lib.c_square

defines the Python equivalent of the C function c_square. We name it python_c_square for clarity, but using the same name is acceptable.

python_c_square.restype = None

defines what type of variables the C function returns. In our case, it is void, which translates in Python to None. See fundamental-data-types for a list of equivalence.

Use library functions

Our C function c_square accepts three arguments: an int and two double *. Hence, our Python function python_c_square accepts three arguments too but they must by of type c_int and “array of c_double” defined by the ctypes Python library.

Therefore, to use python_c_square, we have to convert Python integer into c_int type and Python list into an “array of c_double”.

The best way to do so is to write a Python function, in the file basic_function_helper.py

16def do_square_using_c(list_in):
17    """Call C function to calculate squares"""
18    n = len(list_in)
19    c_arr_in = (c_double * n)(*list_in)
20    c_arr_out = (c_double * n)()
21
22    python_c_square(c_int(n), c_arr_in, c_arr_out)
23    return c_arr_out[:]

In details:

c_arr_in = (c_double * n)(*list_in)
c_arr_out = (c_double * n)()

defines two ctypes arrays of double of size n that can be used by the C function. The first one is initialised with the values of list_in. It is equivalent to:

for i in range(n):
    c_arr_in[i] = c_double(list_in[i])

This line:

python_c_square(c_int(n), c_arr_in, c_arr_out)

calls the C function that does the computation of the square of c_arr_in and put the result in c_arr_out. Note the conversion c_int(n) that transforms the Python integer into a ctypes int.

This line:

return c_arr_out[:]

returns a copy of the results as a Python list.

Final comments

Our C function c_square is now wrapped into a Python function do_square_using_c. To use it in a RepTate module, simply import the function by including in the module header.

As an example, the following calculates the square of numbers from 0 to 999:

from basic_function_helper import do_square_using_c
...
my_list = np.arange(1000)
squared_list = do_square_using_c(*my_list)

Bonus: Callback function

We presented above a method to have Python “request something from C”, that is, Python calls a C function and gets an answer. In the previous example, Python requested C to calculate the square of an array. Sometimes, it is convenient to do the other way around too. For instance, if the array is “big”, the C function might take some time to finish the calculations, and we may want to inform Python of the advancement of the computations.

We propose here to modify the above code to include a callback function that lets the C code call a Python function. As a simple example, the C code will request Python to print the advancement of the calculations of the “square” function, previously introduced. It require more steps than what we have seen before, but it is reasonably simple.

The C function will call a Python function with a double argument (the advancement in percent) and Python will return the percentage incremented by 20%.

Modify the C code

We need to define:

  • A proxy function that will be used to call the corresponding Python function

  • A function that Python will initially call to define the proxy function.

    This is similar to what we have done so far.

  • A function type that defines how the proxy function “look” like, i.e.

    arguments and return types (int, double, etc.)

Somewhere before the c_square function definition, we write:

typedef double give_and_take_double(double p); // type definition
give_and_take_double *tell_python;             // pointer to a function of type "give_and_take_double"
void def_python_callback(give_and_take_double *func)
{
    // Function called by Python once
    // Defines what "tell_python" is pointing to
    tell_python = func;
}

The first line defines a new type: a function that takes a single double as argument and returns a double. This allows us to define the second line: a pointer to a function of type give_and_take_double. At this point we do not know that the function will be, but we know it accepts a double as argument, and it returns a double. The last lines consist of the function that Python will have to call to actually define what tell_python is, or rather, define towards what it is pointing to.

Now we can decorate our c_square function with the tell_python function:

void c_square(int n, double *array_in, double *array_out)
{ //return the square of array_in of length n in array_out
    int i;
    double percent = 0.2;
    for (i = 0; i < n; i++)
    {
        if ((double)i / n > percent)
        {
            percent = tell_python(percent);
        }
        array_out[i] = array_in[i] * array_in[i];
    }
}

That is all we need to do on the C side.

Warning

Do not forget to recompile the shared library every time you modify the C code.

Modify the Python code

Last steps, we need to modify the Python code. We make some addition to the file “basic_function_helper.py”. We need to:

  • Define a “classic” Python function

  • Define a C callback function that translates that Python function

  • Call the C function def_python_callback, defined above to setup the callback function

We add to the bottom of the file “basic_function_helper.py”:

# Callback stuff
from ctypes import CFUNCTYPE, POINTER

def get_percent(percent):
    """Print advancement and set the next call when C has advanced a further 20%"""
    self.Qprint("Advancement of C calculations: %f%%" % (percent*100))
    return percent + 0.2

CB_FTYPE_DOUBLE_DOUBLE = CFUNCTYPE(c_double, c_double) # define C pointer to a function type
cb_get_percent = CB_FTYPE_DOUBLE_DOUBLE(get_percent) # define a C function equivalent to the python function "get_percent"
basic_function_lib.def_python_callback(cb_get_percent)  # the the C code about that C function

In these lines:

def get_percent(percent):
    ...

we define a “classic” Python function that take a float as argument, and return a float. It prints the information in the theory text box of RepTate via the Qprint method.

Then, in the line:

CB_FTYPE_DOUBLE_DOUBLE = CFUNCTYPE(c_double, POINTER(c_double))

the first argument of the ctypes function CFUNCTYPE defines the return types (here double) and the other arguments are the function arguments (here only one double). CFUNCTYPE returns a pointer to a C functions:

The following line defines a C function of type CB_FTYPE_DOUBLE_DOUBLE, which is a proxy for the Python function get_percent:

cb_get_percent = CB_FTYPE_DOUBLE_DOUBLE(get_percent)

Then we tell our C code which is our callback function:

basic_function_lib.def_python_callback(cb_get_percent)

Usage

Now we can calculate the square of a “big” list and follow the advancement of the computations in the theory text box.