ctypes

This example interacts with the shared object file fortran/example.so. This shared object file can be loaded directly

>>> import ctypes
>>> so_file = "fortran/example.so"
>>> lib_example = ctypes.cdll.LoadLibrary(so_file)
>>> lib_example
<CDLL 'fortran/example.so', handle 1b8d1b0 at 0x7f4f1cf0b128>

Once loaded, each function in the ABI can be accessed as an attribute of the CDLL object. For example:

>>> lib_example.make_udf
<_FuncPtr object at 0x7f4f1d15f688>
>>> lib_example.foo
<_FuncPtr object at 0x7f4f1d15f750>

See the ctypes documentation for more information on making foreign calls.

Usage

Each user defined type can be described by subclasses of ctypes.Structure, which are used to describe the fields in a C struct:

class UserDefined(ctypes.Structure):
    _fields_ = [
        ("buzz", ctypes.c_double),
        ("broken", ctypes.c_double),
        ("how_many", ctypes.c_int),
    ]

    def __repr__(self):
        template = (
            "UserDefined(buzz={self.buzz}, "
            "broken={self.broken}, "
            "how_many={self.how_many})"
        )
        return template.format(self=self)


class DataContainer(ctypes.Structure):
    _fields_ = [("data_", ctypes.c_double * 8)]

    @property
    def data(self):
        result = np.ctypeslib.as_array(self.data_)
        return result.reshape((4, 2), order="F")

Note in particular that NumPy provides the numpy.ctypeslib module for ctypes-based interoperability.

To go the other direction, i.e. from a NumPy array to a double* pointer:

def numpy_pointer(array):
    return array.ctypes.data_as(ctypes.POINTER(ctypes.c_double))

In order to call udf_ptr(), a UserDefined instance is created and an opaque intptr_t value is passed by reference:

made_it, ptr_as_int = prepare_udf()
lib_example.udf_ptr(ctypes.byref(ptr_as_int))

In order to convert the pointer to the data held in the ctypes.Structure instance to an intptr_t, the UserDefined* pointer is first converted to a void* pointer:

def prepare_udf():
    made_it = UserDefined()
    raw_pointer = ctypes.cast(ctypes.pointer(made_it), ctypes.c_void_p)
    intptr_t = get_intptr_t()
    ptr_as_int = intptr_t(raw_pointer.value)
    return made_it, ptr_as_int

To call view_knob(), the mangled name must be used:

def view_knob(lib_example):
    return lib_example.__example_MOD_view_knob()

Output

$ python python/check_ctypes.py
------------------------------------------------------------
quux = foo(c_double(1.0), c_double(16.0)) = c_double(61.0)
------------------------------------------------------------
quuz = make_udf(c_double(1.25), c_double(5.0), c_int(1337))
     = UserDefined(buzz=1.25, broken=5.0, how_many=1337)
needsfree(quuz) = True
address(quuz) = 139757150344968  # 0x7f1bbf4d1708
*address(quuz) =
    UserDefined(buzz=1.25, broken=5.0, how_many=1337)
------------------------------------------------------------
val =
[[ 3.    4.5 ]
 [ 1.    1.25]
 [ 9.    0.  ]
 [-1.    4.  ]]
two_val = foo_array(c_int(4), val)
two_val =
[[ 6.   9. ]
 [ 2.   2.5]
 [18.   0. ]
 [-2.   8. ]]
------------------------------------------------------------
ptr_as_int = address(made_it)  # intptr_t / ssize_t / long
ptr_as_int = c_long(139757150344992)  # 0x7f1bbf4d1720
udf_ptr(ptr_as_int)  # Set memory in ``made_it``
made_it = UserDefined(buzz=3.125, broken=-10.5, how_many=101)
needsfree(made_it) = True
*ptr_as_int =
    UserDefined(buzz=3.125, broken=-10.5, how_many=101)
------------------------------------------------------------
contained =
[[0. 4.]
 [1. 9.]
 [1. 2.]
 [3. 1.]]
container = make_container(contained)
container.data =
[[0. 4.]
 [1. 9.]
 [1. 2.]
 [3. 1.]]
address(contained)      = 43439344  # 0x296d4f0
address(container)      = 139757150084784  # 0x7f1bbf491eb0
address(container.data) = 139757150084784  # 0x7f1bbf491eb0
------------------------------------------------------------
just_print()
 ======== BEGIN FORTRAN ========
 just_print() was called
 ========  END  FORTRAN ========
------------------------------------------------------------
view_knob() = 1337
turn_knob(c_int(42))
view_knob() = 42