C Extensions with flints#

Numpy-flint is can be used as either a python module or a library for another C extension. This page gives the necessary details by giving the structure you would need to build another C extension that uses a custom Numpy Universal Function that works with flints.

When setting up the project, you will need to add the numpy-flint as a build requirement to your pyproject.toml file.

[build-system]
requires = ["setuptools>=61","oldest-supported-numpy","numpy-flint>=0.3.3"]
build-backend = "setuptools.build_meta"

Second, you will need to add the flint include directory to your extension settings in the setup.py file.

from setuptools import setup, Extension
import numpy as np
import flint

setup_args = dict(
    ext_modules = [
        Extension(
            name='new_project.compiled_module_name',
            sources=['src/new_project/c_source_file_name.c'],
            depends=[],
            include_dirs=[
                np.get_include(),
                flint.get_include()
            ],
        )
    ]
)

setup(**setup_args)

Now you can use the numpy-flint in your C project itself. The flint.h header has all the pure C flint functions defined in it, and the numpy_flint.h header has the functions used to interface flints with python. At the top of your C source file you will want to add both header files,

#include <flint.h>
#include <numpy_flint.h>

and inside the PyMODINIT_FUNC module initialization function you will want to import the flint C-API with the import_flint() function.

PyMODINIT_FUNC PyInit_compiled_module_name(void) {
    /* stuff */
    if (import_flint() < 0) {
        PyErr_Print();
        PyErr_SetString(PyExc_SystemError, "Count not load flint c API");
        return NULL;
    }
    /* stuff */
}

In the C source file you can then create the new numpy ufunc.

static void new_ufunc(char** args,
                      npy_intp const* dims,
                      npy_intp const* strides,
                      void* data) {
    /* do the magic with flints here! */
}

It is well beyond the scope of this documentation to cover how to write a ufunc, and there is already a good tutorial in the Numpy docs. However, it does bare mentioning that you can not fully follow that example when implementing a universal function for a custom data-type. The differences are:

  1. In the tutorial, the data-types (and many other variables) are declared static outside of the module initialization function. Because the NPY_FLINT index is not known at compile time, trying to do this will cause the compiler to throw an error. Instead you must declare the data-types inside the PyMODINIT_FUNC module initialization function.

  2. In the example, you only need to make a call to PyUFunc_FromFuncAndData. However when implementing custom data types you can only add functionality to an existing ufunc. If there is not already an existing universal function, you have to declare and empty ufunc, then add the functionality for the new data type.

PyMODINIT_FUNC PyInit_compiled_module_name(void) {
    /* stuff */
    uf = PyUFunc_FromFuncAndData(
        NULL, NULL, NULL, 0, 1, 1, PyUFunc_None,
        "new_ufunc_name", "docstring", 0);
    int new_ufunc_types[] = {NPY_FLINT, NPY_FLINT};
    PyUFunc_RegisterLoopForType(
        (PyUFuncObject*) uf, NPY_FLINT,
        &new_ufunc, new_ufunc_types, NULL);
    d = PyModule_GetDict(m);
    PyDict_SetItemString(d, "new_ufunc_name", uf);
    Py_DECREF(uf);
    /* stuff */
}

All together, making a new universal function that works with the flint dtype would look something like the sketch below.

/* stuff */

#include <Python.h>

#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
#include <numpy/arrayobject.h>
#include <numpy/ufuncobject.h>

#include <flint.h>
#include <numpy_flint.h>

/* stuff */

static void new_ufunc(char** args,
                      npy_intp const* dims,
                      npy_intp const* strides,
                      void* data) {
    /* stuff */
}

/* stuff */

PyMODINIT_FUNC PyInit_compiled_module_name(void) {
    PyObject* m;
    PyObject* d;
    PyObject* uf;
    /* stuff */
    // Import numpys array api
    import_array();
    if (PyErr_Occurred()) {
        PyErr_Print();
        PyErr_SetString(PyExc_SystemError, "Could not initialize NumPy.");
        return NULL;
    }
    // Import numpys ufunc api
    import_ufunc();
    if (PyErr_Occurred()) {
        PyErr_Print();
        PyErr_SetString(PyExc_SystemError, "Could not load NumPy ufunc c API.");
        return NULL;
    }
    // Import flint c API
    if (import_flint() < 0) {
        PyErr_Print();
        PyErr_SetString(PyExc_SystemError, "Count not load flint c API");
        return NULL;
    }
    /* stuff */

    uf = PyUFunc_FromFuncAndData(
        NULL, NULL, NULL, 0, 1, 1, PyUFunc_None,
        "new_ufunc_name", "docstring", 0);
    int new_ufunc_types[] = {NPY_FLINT, NPY_FLINT};
    PyUFunc_RegisterLoopForType(
        (PyUFuncObject*) uf, NPY_FLINT,
        &new_ufunc, new_ufunc_types, NULL);
    d = PyModule_GetDict(m);
    PyDict_SetItemString(d, "new_ufunc_name", uf);
    Py_DECREF(uf);

    /* stuff */

    return m;
}

References#

The following references are used in implementing the C extension to Python and NumPy.

In particular, the following examples were especially helpful.

  • Mobiles quaterions Has been my goto source for understanding the process of extending Python and then Numpy with c.

  • Martin Ling’s numpy_quaternion the previous version of the above quaternion project was also used as an example.

  • Mark Wiebe’s numpy_half which was the first project upon which Martin Ling’s projet was based.