# -*- coding: utf-8 -*-
"""
Library interfaces
==================
After loading a shared library, its **procedures must be interfaced to make
them available from the Python side**. Shared libraries are loaded using the
:py:func:`load` function, which returns a :py:class:`SharedLibrary` instance.
Procedures can be interfaced using the :py:func:`interface` function.
Now follows a minimal example of C code that we can interface from Python, with
arguments chosen to cover typical cases. Function and class documentation
:ref:`just below <docu>`.
.. code-block:: c
// Sample structure that stores seismic station information
typedef struct {
double longitude;
double latitude;
double elevation;
char network[2];
char station[5];
_Bool operational; // OCD safety (it would get padded anyways)
} t_Station;
// Sample function prototype that will be interfaced
int do_something (t_Station *station, double *data, unsigned int nsamp);
When building an interface, we will assume that :code:`data` is a NumPy array.
.. code-block:: python
import fancytypes as ft
# This ctypes struct is equivalent to the one above
@ft.cstruct
class Station:
longitude : ft.real64
latitude : ft.real64
elevation : ft.real64
network : ft.character[2]
station : ft.character[5]
operational : ft.logical
# We load the library first
library = ft.load(<path_to_library>)
# Now we can interface any procedure we need
library.do_something = ft.interface(ft.pointer(Station),
ft.pointer(ft.real64),
ft.uint32,
returns=ft.int32)
# Create some empty dummy variables
import numpy as np
nsamp = 100
data = np.zeros(100, dtype=np.double)
station = Station()
# Call the procedure
res = library.do_something(station, ft.real64.array(data), nsamp)
# If "data" is always a NumPy array, we can rely on their API instead
library.do_something = ft.interface(ft.pointer(Station),
ft.nparray(ft.real64), # nparray instead of pointer
ft.uint32,
returns=ft.int32)
# The call will now look cleaner but less explicit
res = library.do_something(station, data, nsamp)
It is important to note that **ctypes will always try to make any necessary
type conversions**, such as passing a pointer even if we make the call using
the variable. This happens in the example above when we pass :code:`station`
instead of :code:`ft.cpointer(station)`. NumPy arrays require us to go through
the :code:`array` method of the corresponding type class unless we rely on the
:py:mod:`numpy.ctypeslib` API.
.. _docu:
.. autoclass:: SharedLibrary()
.. autofunction:: load
.. autofunction:: interface
.. autoclass:: LibraryError
:no-index:
"""
from ctypes import CDLL
from pathlib import Path
from os import name as windows_check
[docs]
class SharedLibrary:
'''Class to store loaded shared libraries and their interfaces. Use the
:py:func:`load` function to load them. Any **procedures from the library
must first be explicitly interfaced to make them callable** from Python.
Interfaces can be built by **assigning a two-tuple containing argument and
return types to an attribute with the same name as the procedure**.
.. code-block::
<library>.<procedure> = ((<arg_type_1>, ..., <arg_type_n>), <res_type>)
Use the :py:func:`interface` function to make the right side more readable.
These **assigned attributes are callable** and will run the procedure after
:py:mod:`ctypes` does the corresponding checks and attempts to make any
necessary conversions.
.. code-block::
<res> = <library>.<procedure>(<arg_1>, ..., <arg_n>)
Procedures can have :code:`void` returns if :py:obj:`None` is assigned as
return type on the interface.
.. warning::
Procedure interfaces are not guaranteed to match on both sides, and
there is no way to check for it. Users are responsible of ensuring the
arguments declared are correct for proper functionality. Wrong
interfaces will result in unintended behaviour at best and are likely
to crash the Python interpreter.
'''
def __init__(self, path):
if not isinstance(path, (str, Path)):
errmsg = 'path must be a pathlike object'
raise TypeError(errmsg)
path = Path(path).resolve()
self.__dict__['_path_'] = path
self.__dict__['_name_'] = path.name
try:
if windows_check == 'nt':
from _ctypes import LoadLibrary
self.__dict__['_lib_'] = CDLL(name=str(path),
handle=LoadLibrary(str(path)))
else:
self.__dict__['_lib_'] = CDLL(str(path))
except FileNotFoundError:
errmsg = f'could not load any shared library at "{self._path_}",' \
' make sure the path is correct'
raise LibraryError(errmsg) from None
def __repr__(self):
'''Repr method.
'''
return '<fancytypes.SharedLibrary object for shared library ' \
f'"{self._name_}" at "{self._path_}">'
def __setattr__(self, name, value):
'''Setter method that loads a procedure if it exists and throws an
exception if it does not.
'''
try:
symbol = getattr(self._lib_, name) # self._lib_.__getattr__(name)
except AttributeError:
errmsg = f'procedure {name} not found in {self._name_}'
raise LibraryError(errmsg) from None
args, res = value
res = res._ctype_ if hasattr(res, '_ctype_') else res
symbol.argtypes = args
symbol.restype = res
procedure = FancyProcedure(symbol, args, res, self._path_, name)
self.__dict__[name] = procedure
class FancyProcedure:
'''Class to interface shared object procedures and trivialize invocations.
These instances are not meant to be created "by hand", the shared object
loading class :py:class:`SharedLibrary` will manage that instead.
:param symbol_ptr: Symbol exposed by a :py:class:`ctypes.CDLL` object
:type symbol: :py:class:`ctypes.CDLL._FuncPtr`
:param arguments: Argument types of the procedure
:type arguments: list or tuple
:param result: Result type of the procedure
:type result: :py:mod:`fancytypes` or :py:mod:`ctypes` valid types
:param lib_path: Path to shared library
:type lib_path: str or :py:class:`pathlib.Path`
:param symbol_name: Procedure name
:type symbol_name: str
'''
def __init__(self, symbol_ptr, arguments=(), result=None,
lib_path=None, symbol_name=None):
self._arguments_ = tuple(arguments)
self._result_ = result
self._path_ = lib_path
self._name_ = symbol_name
self._nargs_ = len(self._arguments_)
self.__procedure__ = symbol_ptr
def __repr__(self):
'''Repr method.
'''
return f'<fancytypes.FancyProcedure object for symbol ' \
f'"{self._name_}" at "{self._path_}">'
def __call__(self, *args):
'''Calls the procedure with the arguments provided.
'''
if len(args) != self._nargs_:
errmsg = f'procedure {self._name_} expects {self._nargs_} ' \
f'arguments, got {len(args)} instead'
raise LibraryError(errmsg)
return self.__procedure__(*args)
[docs]
def interface(*args, returns=None):
'''Helper function to declare procedure interfaces on instances of
:py:class:`SharedLibrary` in a more readable way.
.. code-block::
((<arg_type_1>, ..., <arg_type_n>), <res_type>)
|
V
interface(<arg_type_1>, ..., <arg_type_2>, returns=<res_type>)
:param args: Argument types
:param returns: Return type, default is :py:obj:`None`
'''
return args, returns
[docs]
def load(path):
'''Return a :py:class:`SharedLibrary` instance that stores a loaded shared
library.
:param path: Path to shared library
:type path: :py:class:`str` or :py:class:`pathlib.Path`
:return: Loaded shared library
:rtype: :py:class:`SharedLibrary`
'''
return SharedLibrary(path)
[docs]
class LibraryError(Exception):
'''Exception raised by the package for issues related to shared libraries.
'''
pass