home..

Call Python function From C!

CPython is one of the many python runtimes. It contains both a runtime and a shared language specification that all the python runtimes use. Python’s official implementation is in C, which is referred as CPython.

This means that we can make use of the Python features in C and vice-versa using the CPython API . The scope of this blog will be limited to calling python functions from C. For detailed information please refer the docs

Let’s start with a python function that we want to call from C. We have the following area function defined in Rectangle.py which calculates the area of a rectangle.

File: ~/cpython/Rectangle.py
1
2
def area(a, b):
    return a*b

Now our goal is to execute this python function from C by making use of the CPython API. First I will add the C code below, which calls the python function and then I will explain each and every line of the code to make it clear to the reader. The code itself is documented as well.

File: ~/cpython/ctopy.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include<Python.h>

double call_func(PyObject *func, double x, double y)
{
    PyObject *args;
    PyObject *kwargs;
    PyObject *result = 0;
    double retval;
      
    //https://docs.python.org/3/c-api/call.html#call-support-api
    // Verify that func is a callable
    if (!PyCallable_Check(func))
    {
        fprintf(stderr, "call_func: expected a callable\n");
        goto fail;
    }
    
	// https://docs.python.org/3/c-api/arg.html#building-values
	// Build args tuple which will be passed to python func
	args = Py_BuildValue("(dd)", x, y);
	kwargs = NULL;
	
	// https://docs.python.org/3/c-api/call.html#c.PyObject_Call
	// Call the python function
	result = PyObject_Call(func, args, kwargs);

	// https://docs.python.org/3/c-api/refcounting.html#c.Py_DecRef
	// Decrease the referece since Py_BuildValue returns a new ref.
	Py_DECREF(args);

	// https://docs.python.org/3/c-api/refcounting.html#c.Py_XDECREF
	// Decrease the reference using XDECREF because kwargs is NULL
	Py_XDECREF(kwargs);
	
	// https://docs.python.org/3/c-api/exceptions.html#c.PyErr_Occurred
	// Check if python function was executed without any error.
	if (PyErr_Occurred())
	{
		PyErr_Print();
		goto fail;
	}
	
	// https://docs.python.org/3/c-api/float.html#c.PyFloat_Check
	// Verify the result is a float object
	if (!PyFloat_Check(result))
	{
		fprintf(stderr, "call_func: callable didn't return a float\n");
		goto fail;
	}
	
	// https://docs.python.org/3/c-api/float.html#c.PyFloat_AsDouble
	// Convert PyFloat to C double
	retval = PyFloat_AsDouble(result);
	Py_DECREF(result);
	
	return retval;
	fail:
		Py_XDECREF(result);
		abort();
}


/* Load a symbol from a module */
PyObject *import_function(const char *modname, const char *symbol)
{
	PyObject *u_name, *module;

	// https://docs.python.org/3/c-api/unicode.html#c.PyUnicode_FromString
	// Convert Unicode Object from UTF-8 encoded char buffer.
	u_name = PyUnicode_FromString(modname);
	if(u_name == NULL){
		printf("Couldnot convert string to unicode\n");
		return NULL;
	}

	// https://docs.python.org/3/c-api/import.html#c.PyImport_Import
	// Import the module, PyImport_Import performs absolute import
	module = PyImport_Import(u_name);
	
	
	if(module == NULL){
		printf("Couldnot Import Module\n");
		return NULL;
	}
		
	// https://docs.python.org/3/c-api/object.html#c.PyObject_GetAttrString
	// Get the function from module 
	// This is the equivalent of the Python expression o.attr_name.
	return PyObject_GetAttrString(module, symbol);
}


int main()
{
	PyObject *area_func;
	double x,y;

	// https://docs.python.org/3/c-api/init.html#c.Py_Initialize
	// Initialize the Python interpreter and
	// since version 3.7 it also create the GIL explicitly by calling PyEval_InitThreads()
	// so you don’t have to call PyEval_InitThreads() yourself anymore
	Py_Initialize();
	
	// Get a reference to the Area.area function
	area_func = import_function("Rectangle", "area");
	
	if(area_func == NULL){
		return -1;
	}
	// Call it using our call_func() code
	for (x = 0.0,y=10.0; x <= 10.0 && y>=0 ; x += 0.5, y-=0.5)
	{
		printf("x : % 0.2f y : % 0.2f Area : % 0.2f\n", x, y, call_func(area_func, x, y));
	}
		
	Py_DECREF(area_func);

	// Undo all initializations made by Py_Initialize() and subsequent
	Py_Finalize();
	return 0;
}

On line 1 we include the Python.h header which is the entry point of Python C API.

On line 102 inside the main function we Initialize the python interpreter. Since we are executing the python code from C we need to initialize the interpreter where we can execute the python byte code. This line will Initialize the python Interpreter and will create the GIL explicitly by calling PyEval_InitThreads and lock it. Any python operation that we perform on the interpreter should be done only when the GIL is locked. Python interpreter is not fully thread safe hence we need to take care of GIL here as we are performing python operations from C. More information about the GIL can be found here.

The convention that we follow while performing a python operation is as follows:

In the code above the GIL is locked when we initialize the interpreter, so we don’t lock it explicitly anywhere in the whole program because the above program is single threaded.

On line no. 105 we import the area function from Rectangle.py. Inside the import_function on line no 64 takes module name and function name as arguments. It converts module name to PyObject using PyUnicode_FromString on line 70 which is passed as an argument to PyImport_Import on line 78. After we import the module we get the required function on line no 89 using PyObject_GetAttrString, this function is an equivalent of Python expression o.attr_name, where o is the object and attr_name is the attribute that we want to get.

After we import the area function its time to call the function from C. On line no 111-114 we have a for loop that has two variables x and y which we pass as an argument to call_func, we also pass the area_func which is our area function in Rectangle.py as an argument to call_func.

call_func is defined on line no. 3. In this function we first check whether the func that we have passed is callable or not. PyObject_Call on line 25 calls the python function with args and kwargs, in order to make this function call first we prepare these arguments. On line 20 we build a tuple with x and y which are our positional arguments which we pass as args, kwargs is NULL because we don’t have any keyword arguments in our area function. This is where we call our area function and get its output.

Before we further go thorugh the code, We need to know about Python’s Memory management which you can read here. This is a pre-requisite that needs to be understood before we go through the other section of call_func.

Py_BuildValue on line no 20 returns a new reference to a PyObject, everytime a new reference to a PyObject is created in memory, it’s the responsibility of the programmer to decrease its reference count. If the object’s reference count is not decreased then the Python Garbage Collector won’t remove that object from the memory and we will end up having a memory leak.

After the args is used we decrease it’s reference count on line no 29 using Py_DECREF. We use Py_XDECREF to decrease the reference count of kwargs. In this case kwargs is NULL and we need not decrease any refernce count here, but using Py_XDECREF we demonstrate that it is possible to decrease the reference of those objects whose value is NULL. Whenever we are not sure of the value of the PyObject, we should use Py_XDECREF. More information of these functions can be found here

On line 37 we make sure that PyObject_Call ran successfully without any errors, if there is any runtime error on python side, then we catch it and print it on line 39 using PyErr_Print.

On line 45 we check the result’s type and make sure that it is a float object. On line 53 we convert the PyFloat object to C double using PyFloat_AsDouble and then we decrease the reference count of the result and we return the retval on line 56.

After completion of the loop on line 111-114 we decrease the reference count of area_func and the call Py_Finalize, this will undo all the initializations made by Py_Initialize.

How to run the above code

Create the two files add the respective C and Python code and save them in the same dir. In my case both the files are in cpython dir. Then follow the given commands to execute the code successfully.

cd cpython
export PYTHONPATH=.
cc -g ctopy.c -I/usr/include/python3.10 -L /usr/lib/python3.10/config-3.10-x86_64-linux-gnu -lpython3.10

The path to python interpreter might be different in your system, you would have to change that according to your requirements.

Output

./a.out
x :  0.00 y :  10.00 Area :  0.00
x :  0.50 y :  9.50 Area :  4.75
x :  1.00 y :  9.00 Area :  9.00
x :  1.50 y :  8.50 Area :  12.75
x :  2.00 y :  8.00 Area :  16.00
x :  2.50 y :  7.50 Area :  18.75
x :  3.00 y :  7.00 Area :  21.00
x :  3.50 y :  6.50 Area :  22.75
x :  4.00 y :  6.00 Area :  24.00
x :  4.50 y :  5.50 Area :  24.75
x :  5.00 y :  5.00 Area :  25.00
x :  5.50 y :  4.50 Area :  24.75
x :  6.00 y :  4.00 Area :  24.00
x :  6.50 y :  3.50 Area :  22.75
x :  7.00 y :  3.00 Area :  21.00
x :  7.50 y :  2.50 Area :  18.75
x :  8.00 y :  2.00 Area :  16.00
x :  8.50 y :  1.50 Area :  12.75
x :  9.00 y :  1.00 Area :  9.00
x :  9.50 y :  0.50 Area :  4.75
x :  10.00 y :  0.00 Area :  0.00

References:

cpython-docs

GIL

Call Python from C

© 2022    •  Theme  Moonwalk