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

No comments:

Post a Comment