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).