# Ctypes reference

https://docs.python.org/3/library/ctypes.html
https://svn.python.org/projects/ctypes/branches/ctypes-1.1/docs/manual/tutorial.html
https://numpy.org/doc/stable/user/c-info.python-as-glue.html#calling-other-compiled-libraries-from-python
https://doc.sagemath.org/html/en/thematic_tutorials/numerical_sage/ctypes.html
https://wiki.python.org/moin/ctypes
https://www.scaler.com/topics/python-ctypes/

# calling C function from Python
Here are the steps to call a C function using Python Ctypes:
1. Creating a C file (with a .c extension) with all the necessary functions.
2. Using the C compiler, create a shared library file (.so extension).
3. Using the shared file, create an instance of ctypes.CDLL in Python.
4. The C function should then be called with the format
    <CDLL_instance>.<function_name>(<function_parameters>)

# make shared object library
$ gcc -c program.c
$ gcc -shared -o program.so program.o
Note that on osx -shared should be replaced by -dynamiclib and program.so should
be called program.dylib


import ctypes
libc = ctypes.cdll.LoadLibrary("libc.so.6")
libc.rand() # returns a pseudo-random integer

* 32-bit platforms where sizeof(int) == sizeof(long), c_int is an alias for c_long,
* 64-bit platforms where sizeof(long) == sizeof(long long), c_long is an alias
  for c_longlong

# fundamental data types
ctypes defines a number of primitive C compatible data types:

ctypes types            C type                              Python type
---------------------------------------------------------------------------------------
c_char                  char                                1-character string
c_wchar                 wchar_t                             1-character unicode string
c_byte                  char                                int/long
c_ubyte                 unsigned char                       int/long
c_short                 short                               int/long
c_ushort                unsigned short                      int/long
c_int                   int                                 int/long
c_uint                  unsigned int                        int/long
c_long                  long                                int/long
c_ulong                 unsigned long                       int/long
c_longlong              __int64 or long long                int/long
c_ulonglong    unsigned __int64 or unsigned long long       int/long
c_float                 float                               float
c_double                double                              float
c_char_p                char *(NUL terminated)              string or None
c_wchar_p               wchar_t *(NUL terminated)           unicode or None
c_void_p                void *                              int/long or None


Python                                              C type
----------------------------------------------------------------
None                                                NULL
ctypes.char_p                                       char*
ctypes.c_int                                        int
ctypes.c_longlong                                   long long
ctypes.c_double                                     double
numpy.ctypeslib.ndpointer(dtype=numpy.int32)        int*
numpy.ctypeslib.ndpointer(dtype=numpy.float64)      double*


By default ctypes assumes the return values are ints. If they are not you need
to tell it by setting restype to the correct return type.




>>> n = ctypes.c_int(42)
>>> print(n)
c_int(42)
>>> n.value
42
>>> n.value = -99
>>> n.value
-99
>>> repr(n.value)
'-99'
>>> ctypes.sizeof(n)
4
>>> ctypes.sizeof(ctypes.c_int)
4
>>> n = ctypes.c_int(10)
>>> arr = (ctypes.c_int * n.value)()
>>> arr
<__main__.c_int_Array_100 object at 0x7fe1a3861fd0>
>>> ctypes.sizeof(arr)
40
>>> for i in range(n.value):
        arr[i] = i
>>> arr[:]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> arr[3:]
[3, 4, 5, 6, 7, 8, 9]
>>> arr[:7]
[0, 1, 2, 3, 4, 5, 6]
>>> arr[3:7]
[3, 4, 5, 6]


>>> s = ctypes.c_wchar_p()
>>> s.value = "institute"
>>> s.value
'institute'

>>> s = ctypes.c_wchar_p("institute")
>>> s.value
'institute'


# pointer
>>> n = ctypes.c_int(42)
>>> n
c_int(42)
>>> n.value
42
>>> nptr = ctypes.pointer(n)
>>> nptr.contents
c_int(42)
>>> nptr.contents.value
42


calling the pointer type without an argument creates a NULL pointer.
NULL pointer have a False boolean value:
>>> null_ptr = ctypes.POINTER(ctypes.c_int)()
>>> bool(null_ptr)
False

>>> n = ctypes.c_double(10)
>>> n.value
10.0
>>> nptr = ctypes.POINTER(ctypes.c_double)(n)
>>> nptr.contents
c_double(10.0)
>>> nptr.contents.value
10.0


Note:
n = ctypes.c_double(10)
nptr = ctypes.pointer(n) is equivalent to nptr = ctypes.POINTER(ctypes.c_double)(n)


num = 10
arr = (ctypes.c_int * num)()
for n in range(num):
    arr[n] = n


# arrays (pure Ctypes)
n = ctypes.c_int(3)

a = (ctypes.c_int * n.value)(1, 2, -5) # default ctypes way of creating arrays
b = (ctypes.c_int * n.value)(-1, 3, 3)

res = (ctypes.c_int * n.value)(0, 0, 0)

math.add_int_array(a, b, res, n.value) # pass by value


# arrays (using Numpy)
import numpy as np

n = ctypes.c_int(3)

a = np.array([1, 2, -5], dtype=ctypes.c_int)
b = np.array([-1, 3, 3], dtype=ctypes.c_int)

res = np.zeros(n.value, dtypes=ctypes.c_int)

intp = ctypes.POINTER(ctypes.c_int) # declare the pointer to int type as an object

i = a.ctypes.data_as(intp)
j = b.ctypes.data_as(intp)

k = res.ctypes.data_as(intp)

math.add_int_array(i, j, k, n.value) # pass by reference

# numpy.ctypeslib.ndpointer
An ndpointer instance is used to describe an ndarray in restype and argtypes
specifications. This approach is more flexible than using, for example,
POINTER(c_double), since several restrictions can be specified, which are
verified upon calling the ctypes function. These include data type, number of
dimensions, shape and flags. If a given array does not satisfy the specified
restrictions, a TypeError is raised.

numpy.ctypeslib.ndpointer(dtype=None, ndim=None, shape=None, flags=None)

Parameters: dtype: data-type, optional
                Array data-type.

            ndim: int, optional
                Number of array dimensions.

            shape: tuple of ints, optional
                Array shape.

            flags: str or tuple of str
                Array flags; may be one or more of:
                    * C_CONTIGUOUS/C/CONTIGUOUS
                    * F_CONTIGUOUS/F/FORTRAN
                    * OWNDATA/O
                    * WRITEABLE/W
                    * ALIGNED/A
                    * WRITEBACKIFCOPY/X

Returns:    klass: ndpointer type object
                A type object, which is an _ndtpr instance containing dtype,
                ndim, shape and flags information.

Raises:     TypeError
                If a given array does not satisfy the specified restrictions.

Reference:
https://numpy.org/doc/stable/reference/routines.ctypeslib.html
https://pydoc.dev/numpy/latest/numpy.ctypeslib.html

# data from ctypes array into numpy array
>>> n = 8
>>> arr = (ctypes.c_double * n)(0, 1, 2, 3, 4, 5, 6, 7)
>>> arr
<c_double_Array_8 object at 0x7f4dccd39d50>
>>> type(arr)
<class 'c_double_Array_8'>
>>> arr[0]
0.0
>>> arr[7]
7.0
>>> arr = np.ctypeslib.as_array(arr)
>>> arr
array([0., 1., 2., 3., 4., 5., 6., 7.])
>>> arr.shape
(8,)
>>> arr.size
8

# data from ctypes pointer into numpy array
>>> n = 8
>>> arr = (ctypes.c_double * n)(0, 1, 2, 3, 4, 5, 6, 7)
>>> ptr = ctypes.POINTER(ctypes.c_double)(arr)
>>> ptr
<__main__.LP_c_double object at 0x7f4dccd39d50>
>>> type(ptr)
<class '__main__.LP_c_double'>
>>> ptr[0]
0.0
>>> ptr[7]
7.0
>>> arr = np.ctypeslib.as_array(ptr, shape=(n,))
>>> arr
array([0., 1., 2., 3., 4., 5., 6., 7.])
>>> arr.shape
(8,)
>>> arr.size
8

# data from numpy array to ctypes array
>>> n = 8
>>> arr = np.arange(n, dtype=np.float64)
>>> arr
array([0., 1., 2., 3., 4., 5., 6., 7.])
>>> type(arr)
<class 'numpy.ndarray'>
>>> arr = np.ctypeslib.as_ctypes(arr)
>>> arr
<c_double_Array_8 object at 0x7f4dc1fe90d0>
>>> type(arr)
<class 'c_double_Array_8'>
>>> arr[0]
0.0
>>> arr[7]
7.0

# data from numpy array to ctypes pointer
>>> n = 8
>>> arr = np.arange(n, dtype=np.float64)
>>> arr
array([0., 1., 2., 3., 4., 5., 6., 7.])
>>> type(arr)
<class 'numpy.ndarray'>
>>> ptr = arr.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
>>> ptr
<__main__.LP_c_double object at 0x7f4dc1fe9750>
>>> type(ptr)
<class '__main__.LP_c_double'>
>>> ptr[0]
0.0
>>> ptr[7]
7.0
