Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Loading vs linking in Cython modules

Tags:

python

cython

While exploring Cython compile steps, I found I need to link C libraries like math explicitly in setup.py. However, such step was not needed for numpy. Why so? Is numpy being imported through usual python import mechanism? If that is the case, we need not explicitly link any extension module in Cython?

I tried to rummage through the official documentation, but unfortunately there was no explanation as to when an explicit linking is required and when it will be dealt automatically.

like image 263
Avinash Tripathi Avatar asked Sep 29 '19 13:09

Avinash Tripathi


People also ask

Are Python libraries dynamically linked?

No, loading a pure-Python module is not considered a form of dynamic linking. Traditional dynamic linking loads machine code into a new chunk of memory, and multiple executable processes can be given access (the dynamically linked library only needs to be loaded once, virtual memory takes care of the rest).

Does Cython need to be compiled?

Cython source file names consist of the name of the module followed by a . pyx extension, for example a module called primes would have a source file named primes. pyx . Cython code, unlike Python, must be compiled.

Do Python libraries work with Cython?

Cython Hello World As Cython can accept almost any valid python source file, one of the hardest things in getting started is just figuring out how to compile your extension. Congratulations!


1 Answers

Call of a cdef-function corresponds more or less just to a jump to an address in the memory - the one from which the command should be read/executed. The question is how this address is provided. There are some cases we need to consider:

A. inline functions

The code of those functions is either inlined or the definition of the function is in the same translation unit, thus the address is known to the linker at the link time (or even compiler at compile-time) - no need for additional libraries.

An example are header-only libraries.

Consequences: Only include path(s) should be provided in setup.py.

B. static linking

The definition/functionality we need is in another translation unit/library - the target-address of the jump is calculated at the link-time and cannot be changed anymore afterwards.

An example are additional c/cpp-files or static libraries which are added to extension-definition.

Consequences: Static library should be added to setup.py, i.e. library-path and library name along with include paths.

C. dynamic linking

The necessary functionality is provided in a shared object/dll. The address to jump to is calculated during the runtime from loader and can be replaced at program start by exchanging the loaded shared objects.

An example are stdlibc++ (usually added automatically by g++) or libm, which is not automatically added to linker command by gcc.

Consequences: Dynamic library should be added to setup.py, i.e. library-path and library name, maybe r-path + include paths. Shared object/dll must be provided at the run time. More (than one probably would like to know) information about Cython/Python using dynamic libraries can be found in this SO-post.

D. Calling via a pointer

Linker is needed only when we call a function via its name. If we call it via a function-pointer, we don't need a linker/loader because the address of the function is already known - the value in the function pointer.

Example: Cython-generated modules uses this machinery to enable access to its cdef-functions exported through pxd-file. It creates a data structure (which is stored as variable __pyx_capi__ in the module itself) of function-pointers, which is filled by the loader once the so/dll is loaded via ldopen (or whatever Windows' equivalent). The lookup in the dictionary happens only once when the module is loaded and the addresses of functions are cached, so the calls during the run time have almost no overhead.

We can inspect it, for example via

#foo.pyx:
cdef void doit():
    print("doit")
#foo.pxd
cdef void doit()

>>> cythonize -3 -i foo.pyx
>>> python -c "import foo; print(foo.__pyx_capi__)" 
{'doit': <capsule object "void (void)" at 0x7f7b10bb16c0>}

Now, calling a cdef function from another module is just jumping to the corresponding address.

Consequences: We need to cimport the needed funcionality.


Numpy is a little bit more complicated as it uses a sophisticated combination of A and D in order to postpone the resolution of symbols until the run time, thus not needing shared-object/dlls at link time (but at run time!).

Some functionality in numpy-pxd file can be directly used because they are inlined (or even just defines), for example PyArray_NDIM, basically everything from ndarraytypes.h. This is the reason one can use cython's ndarrays without much ado.

Other functionality (basically everything from ndarrayobject.h) cannot be accessed without calling np.import_array() in an initialization step, for example PyArray_FromAny. Why?

The answer is in the header __multiarray_api.h which is included in ndarrayobject.h, but cannot be found in the git-repository as it is generated during the installation, where the definition of PyArray_FromAny can be looked up:

...
static void **PyArray_API=NULL; //usually...
...
#define PyArray_CheckFromAny \
        (*(PyObject * (*)(PyObject *, PyArray_Descr *, int, int, int, PyObject *)) \
         PyArray_API[108])
...

PyArray_CheckFromAny isn't a name of a function, but a define fo a function pointer saved in PyArray_API, which is not initialized (i.e. is NULL), when module is first loaded! Btw, there is also a (private) function called PyArray_CheckFromAny, which is what the function pointer actually points to - and because the public version is a define there is no name collision when linked...

The last piece of the puzzle - the function _import_array (more or less the working horse behind np.import_array) is an inline function (case A), so only include path is needed, to be able to use it.

_import_array uses a similar approach to Cython's __pyx_capi__ to get the function pointers: The field is called _ARRAY_API and can be inspected via:

>>> import numpy.core._multiarray_umath as macore
>>> macore._ARRAY_API
<capsule object NULL at 0x7f17d85f3810>

More info about how PyArray_API can be initialized can be found in this SO-answer of mine.

However, when using functionality from numpy/math.pxd, one has to staticly link numpy's math-library (see for example this SO-question).

like image 105
ead Avatar answered Sep 19 '22 18:09

ead