Saturday, June 19, 2021

Creating Basic Python C Extensions

According https://realpython.com/build-python-c-extension-module/

To write Python modules in C, you’ll need to use the Python API, which defines the various functions, macros, and variables that allow the Python interpreter to call your C code. All of these tools and more are collectively bundled in the <Python.h> header file.

Testing has been done on Fedora Linux 34 . Python version is 3.9.5 

 (.env) [boris@fedora33server ]$ cat fputsmodule.c

#include <Python.h>

static PyObject *method_fputs(PyObject *self, PyObject *args) {

    char *str, *filename = NULL;

    int bytes_copied = -1;

    /* Parse arguments */

    if(!PyArg_ParseTuple(args, "ss", &str, &filename)) {

        return NULL;

    }

    FILE *fp = fopen(filename, "w");

    bytes_copied = fputs(str, fp);

    fclose(fp);

    return PyLong_FromLong(bytes_copied);

}

static PyMethodDef FputsMethods[] = {

    {"fputs", method_fputs, METH_VARARGS, "Python interface for fputs C library function"},

    {NULL, NULL, 0, NULL}

};

static struct PyModuleDef fputsmodule = {

    PyModuleDef_HEAD_INIT,

    "fputs",

    "Python interface for the fputs C library function",

    -1,

    FputsMethods

};

PyMODINIT_FUNC PyInit_fputs(void) {

    return PyModule_Create(&fputsmodule);

}

(.env) [boris@fedora33server ]$ cat setup.py

from distutils.core import setup, Extension

def main():

    setup(name="fputs",

          version="1.0.0",

          description="Python interface for the fputs C library function",

          author="Boris D",

          author_email="BorisD@gmail.com",

          ext_modules=[Extension("fputs", ["fputsmodule.c"])])

if __name__ == "__main__":

    main()

(.env) [boris@fedora33server ]$ python3 setup.py install

(.env) [boris@fedora33server ]$ ll
total 12
drwxrwxr-x. 4 boris boris  63 Jun 19 19:30 build
-rw-rw-r--. 1 boris boris 796 Jun 19 19:08 fputsmodule.c
-rw-rw-r--. 1 boris boris 153 Jun 19 19:24 MyFputs.py
-rw-rw-r--. 1 boris boris 347 Jun 19 19:09 setup.py

(.env) [boris@fedora33server ]$ cat MyFputs.py
import fputs

# Write to an empty file named `output.txt`
fputs.fputs("Python is writing to file!", "./output.txt")
with open("./output.txt", "r") as f:
     print(f.read())

(.env) [boris@fedora33server ]$ python MyFputs.py
Python is writing to file!

Runtime snapshots











































(.env) [boris@fedora33server build]$ ls -CRl
.:
total 0
drwxrwxr-x. 2 boris boris 50 Jun 19 20:01 lib.linux-x86_64-3.9
drwxrwxr-x. 2 boris boris 27 Jun 19 20:01 temp.linux-x86_64-3.9

./lib.linux-x86_64-3.9:
total 28
-rwxrwxr-x. 1 boris boris 26720 Jun 19 20:01 fputs.cpython-39-x86_64-linux-gnu.so

./temp.linux-x86_64-3.9:
total 24
-rw-rw-r--. 1 boris boris 20832 Jun 19 20:01 fputsmodule.o

However, in the link above author doesn't focus your attention on
update required in static PyMethodDef myMethods[]  still having
"METH_NOARGS" instead of "METH_VARARGS", what actually confuses inexperienced learners. Following below is working code follows up link above, but  making  code sample easy to reproduce. Notice also that outside VENV you would have problems to run `python setup install`

(.env) [boris@fedora33server FIBONACHI]$ cat test.c

#include <Python.h>

int Cfib(int n)
{
    if (n < 2)
        return n;
    else
        return Cfib(n-1)+Cfib(n-2);
}

// Our Python binding to our C function
// This will take one and only one non-keyword argument
static PyObject *fib(PyObject* self, PyObject* args)
{
    // instantiate our `n` value
    int n;
    // if our `n` value
    if(!PyArg_ParseTuple(args, "i", &n))
        return NULL;
    // return our computed fib number
    

    int result = Cfib(n);
    return Py_BuildValue("i",result);
}

// Our Module's Function Definition struct
// We require this `NULL` to signal the end of our method
// definition
static PyMethodDef myMethods[] = {
    { "fib", fib, METH_VARARGS, "Calculate fibonacii value" },
    { NULL, NULL, 0, NULL }
};

// Our Module Definition struct
static struct PyModuleDef myModule = {
    PyModuleDef_HEAD_INIT,
    "myModule",
    "Test Module",
    -1,
    myMethods
};

// Initializes our module using our above struct
PyMODINIT_FUNC PyInit_myModule(void)
{
    return PyModule_Create(&myModule);
}

(.env) [boris@fedora33server FIBONACHI]$ cat setup.py
from distutils.core import setup, Extension
setup(name = 'myModule', version = '1.0',  \
   ext_modules = [Extension('myModule', ['test.c'])])

(.env) [boris@fedora33server FIBONACHI]python3 setup.py install

(.env) [boris@fedora33server FIBONACHI]$ cat MyProg.py
import myModule
a = int(input("Input number:"))
print(myModule.fib(a))

(.env) [boris@fedora33server FIBONACHI]$ python MyProg.py
Input number:11
89


Relatively complicated sample based on
and minor refactoring just to make things clear


(.env) [boris@sever33fedora enhancePython]$ cat demo.h
unsigned long cfactorial_sum(char num_chars[]);
unsigned long ifactorial_sum(long nums[], int size);
unsigned long factorial(long n);

(.env) [boris@sever33fedora enhancePython]$ cat demolib.c
#include <stdio.h>
#include "demo.h"


unsigned long cfactorial_sum(char num_chars[]) {
    unsigned long fact_num;
    unsigned long sum = 0;

    for (int i = 0; num_chars[i]; i++) {
        int ith_num = num_chars[i] - '0';
        fact_num = factorial(ith_num);
        sum = sum + fact_num;
    }
    return sum;
}

unsigned long ifactorial_sum(long nums[], int size) {
    unsigned long fact_num;
    unsigned long sum = 0;
    for (int i = 0; i < size; i++) {
        fact_num = factorial(nums[i]);
        sum += fact_num;
    }
    return sum;
}

unsigned long factorial(long n) {
    if (n == 0)
        return 1;
    return (unsigned)n * factorial(n-1);
}

(.env) [boris@sever33fedora enhancePython]$ cat demo.c   
#include <Python.h>
#include <stdio.h>
#include "demo.h"

// wrapper function for cfactorial_sum
static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    char *char_nums;
    if (!PyArg_ParseTuple(args, "s", &char_nums)) {
        return NULL;
    }

    unsigned long fact_sum;
    fact_sum = cfactorial_sum(char_nums);

    return Py_BuildValue("i", fact_sum);
}

// wrapper function for ifactorial_sum
static PyObject *DemoLib_iFactorialSum(PyObject *self, PyObject *args) {

    PyObject *lst; 

    if (!PyArg_ParseTuple(args, "O", &lst)) {
        return NULL;
    }

    int n = PyObject_Length(lst);
    if (n < 0) {
        return NULL;
    }

    long nums[n];
    for (int i = 0; i < n; i++) {
        PyLongObject *item = PyList_GetItem(lst, i);
        /* long num = PyLong_AsLong(item); */

        nums[i] = PyLong_AsLong(item);
    }

    unsigned long fact_sum;
    fact_sum = ifactorial_sum(nums, n);

    return Py_BuildValue("i", fact_sum);
}

// module's function table
static PyMethodDef DemoLib_FunctionsTable[] = {
    {
        "sfactorial_sum", // name exposed to Python
        DemoLib_cFactorialSum, // C wrapper function
        METH_VARARGS, // received variable args (but really just 1)
        "Calculates factorial sum from digits in string of numbers" // documentation
    }, {
        "ifactorial_sum", // name exposed to Python
        DemoLib_iFactorialSum, // C wrapper function
        METH_VARARGS, // received variable args (but really just 1)
        "Calculates factorial sum from list of ints" // documentation
    }, {
        NULL, NULL, 0, NULL
    }
};

// modules definition
static struct PyModuleDef DemoLib_Module = {
    PyModuleDef_HEAD_INIT,
    "demo",     // name of module exposed to Python
    "Demo Python wrapper for custom C extension library.", // module documentation
    -1,
    DemoLib_FunctionsTable
};

PyMODINIT_FUNC PyInit_demo(void) {
    return PyModule_Create(&DemoLib_Module);
}

(.env) [boris@sever33fedora enhancePython]$ cat setup.py
from setuptools import Extension, setup

module = Extension("demo",
                  sources=[
                    'demo.c',
                    'demolib.c'
                  ])
setup(name='demo',
     version='1.0',
     description='Python wrapper for custom C extension',
     ext_modules=[module])
     
(.env) [boris@sever33fedora enhancePython]$ python setup.py install

(.env) [boris@sever33fedora enhancePython]$ python MyProg.py
Callng sfactorial_sum("1234567") gives 
 5913
Calling ifactorial_sum([1,2,3,4,5,6,7]) gives 
 5913

Friday, June 18, 2021

GCC build of shared library to verify calling C-function from Python 3.9.5 module on Fedora 34 Linux (Server Edition)

 1) We are using "ctype" native Python module to load the shared library.

2) The ctype Python module is available starting with Python 2.5. Make sure you have the most recent Python installed on your system


 (.env) [boris@fedora33server CPYTHON]$ cat arithmatic.h
void connect();
int randNum();
int multNum(int a, int b);

(.env) [boris@fedora33server CPYTHON]$ cat arithmatic.c
#include <stdio.h>
#include <stdlib.h>
#include "arithmatic.h"
void connect()
{
   printf("Connected to C extension\n");
}
//return random value in range of 0-100
int randNum()
{
    int nRand = rand() % 100; 
    return nRand;
}
//Multiply two integer numbers and return value
int multNum(int a, int b)
{
    int nMult = a*b;
    return nMult;
}

That is supposed to run in one line, otherwise you have to place
"\" to let gcc know that the rest of line would go after cartridge return

(.env) [boris@fedora33server CPYTHON]$ gcc -shared -o libcalci.so -fPIC arithmatic.c

(.env) [boris@fedora33server CPYTHON]$ ll
total 28
-rw-rw-r--. 1 boris boris   291 Jun 18 19:23 arithmatic.c
-rw-rw-r--. 1 boris boris    57 Jun 18 18:34 arithmatic.h
-rwxrwxr-x. 1 boris boris 16280 Jun 18 19:31 libcalci.so
-rw-rw-r--. 1 boris boris   374 Jun 18 19:21 MyTest.py

(.env) [boris@fedora33server CPYTHON]$ cat  MyTest.py

from ctypes import *
libCalc = CDLL("./libcalci.so")
 
#call C function to check connection
libCalc.connect() 
 
#calling randNum() C function
#it returns random number
varRand = libCalc.randNum()
print ("Random Number:", varRand, type(varRand))
 
#calling multNum() C function
#it returns multiplication of two numbers
varMult = libCalc.multNum(38,27)
print ("Multiplication : ", varMult)


(.env) [boris@fedora33server CPYTHON]$ python  MyTest.py
Connected to C extension
Random Number: 83 <class 'int'>
Multiplication :  1026











A bit more complicated sample

(.env) [boris@fedora33server CDEVPY]$ cat sum.c
int our_function(int num_numbers, int *numbers) {
    int i;
    int sum = 0;
    for (i = 0; i < num_numbers; i++) {
        sum += numbers[i];
    }
    return sum;
}

(.env) [boris@fedora33server CDEVPY]$ gcc -fPIC -shared -o libsum.so sum.c

(.env) [boris@fedora33server CDEVPY]$ ll
total 28
-rwxrwxr-x. 1 boris boris 16160 Jun 18 21:35 libsum.so
-rw-rw-r--. 1 boris boris   107 Jun 18 21:33 MyRun.py
-rw-rw-r--. 1 boris boris   169 Jun 18 21:29 sum.c
-rw-rw-r--. 1 boris boris   345 Jun 18 21:29 sum.py

(.env) [boris@fedora33server CDEVPY]$ cat sum.py
import ctypes

_sum = ctypes.CDLL('./libsum.so')
_sum.our_function.argtypes = (ctypes.c_int, ctypes.POINTER(ctypes.c_int))

def our_function(numbers):
    global _sum
    num_numbers = len(numbers)
    array_type = ctypes.c_int * num_numbers
    result = _sum.our_function(ctypes.c_int(num_numbers), array_type(*numbers))
    return int(result)

(.env) [boris@fedora33server CDEVPY]$ cat  MyRun.py
import sum
def main():
    print (sum.our_function([11,2,-3,4,-3,6]))

if __name__=="__main__":
    main()
(.env) [boris@fedora33server CDEVPY]$ python  MyRun.py
17

(.env) [boris@fedora33server CDEVPY]$ ll
total 28
-rwxrwxr-x. 1 boris boris 16160 Jun 18 21:35 libsum.so
-rw-rw-r--. 1 boris boris   107 Jun 18 21:33 MyRun.py
drwxrwxr-x. 2 boris boris    32 Jun 18 21:36 __pycache__
-rw-rw-r--. 1 boris boris   169 Jun 18 21:29 sum.c
-rw-rw-r--. 1 boris boris   345 Jun 18 21:29 sum.py

PyCharm project based ctypes wrapper

Pretty short sample in C++

[boris@fedora33server ]$ ll
total 8
-rw-rw-r--. 1 boris boris 126 Jun 19 08:46 dullmath.cpp
-rw-rw-r--. 1 boris boris  79 Jun 19 08:52 MyProg.py

[boris@fedora33server ]$ cat dullmath.cpp
extern "C" int fibonacci(int n) {
  if (n <= 0) return 0;
  if (n == 1) return 1;
  return fibonacci(n-1) + fibonacci(n-2);
}

[boris@fedora33server]$ g++ -shared  dullmath.cpp -o   libdullmath.so

[boris@fedora33server ]$ ll
total 24
-rw-rw-r--. 1 boris boris   126 Jun 19 08:46 dullmath.cpp
-rwxrwxr-x. 1 boris boris 16176 Jun 19 08:52 libdullmath.so
-rw-rw-r--. 1 boris boris    79 Jun 19 08:52 MyProg.py

[boris@fedora33server]$ cat MyProg.py
import ctypes

lib = ctypes.CDLL(f'./libdullmath.so')
print(lib.fibonacci(11))

[boris@fedora33server ]$ python MyProg.py
89

One more sample

[boris@fedora33server ]$ cat function.c
int myFunction(int num)
{
    if (num == 0)
 
        // if number is 0, do not perform any operation.
        return 0;
    else
        // if number divides by 11, return 1 else return 0
          return (num % 11 == 0 ? 1 : 0) ;
 
}
[boris@fedora33server ]$ gcc -fPIC -shared -o libfun.so function.c
[boris@fedora33server ]$ ll libfun.so
-rwxrwxr-x. 1 boris boris 16168 Jun 19 10:31 libfun.so

[boris@fedora33server ]$ cat MyProg.py

import ctypes
  
test_text = input ("Введите число: ")
test_number = int(test_text)

# libfun loaded to the python file
# using fun.myFunction(),
# C function can be accessed
# but type of argument is the problem.
                        
fun = ctypes.CDLL("./libfun.so")  

# Now whenever argument
# will be passed to the function                                                       
# ctypes will check it.
           
fun.myFunction.argtypes = [ctypes.c_int]
 
returnVale = fun.myFunction(test_number)  
print(returnVale)

[boris@fedora33server ]$ python MyProg.py
Введите число: 121
1
[boris@fedora33server ]$ python MyProg.py
Введите число: 555
0
[boris@fedora33server ]$ python MyProg.py
Введите число: 550
1


REFERENCES