[PATCH evemu 15/19] py: Add base class LibraryWrapper

Daniel Martin consume.noise at gmail.com
Tue Jan 7 02:22:11 PST 2014


On Tue, Jan 07, 2014 at 09:46:18AM +1000, Peter Hutterer wrote:
> On Mon, Jan 06, 2014 at 06:38:15PM +0100, Daniel Martin wrote:
> > LibraryWrapper will be a base class for others wrapping a shared
> > library with ctypes.
> > 
> > And add various functions (i.e. expect_ge_zero()), which will be used as
> > callback functions to check the return value of an API call.
> > 
> > Signed-off-by: Daniel Martin <consume.noise at gmail.com>
> > ---
> >  python/evemu/base.py | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++
> >  1 file changed, 128 insertions(+)
> > 
> > diff --git a/python/evemu/base.py b/python/evemu/base.py
> > index 713aec1..3a9de8d 100644
> > --- a/python/evemu/base.py
> > +++ b/python/evemu/base.py
> > @@ -1,11 +1,139 @@
> > +"""
> > +The base module provides classes wrapping shared libraries.
> > +"""
> >  import ctypes
> >  import ctypes.util
> >  import os
> >  
> > +# Import types directly, so they don't have to be prefixed with "ctypes.".
> > +from ctypes import c_char_p, c_int, c_uint, c_void_p
> > +
> >  import evemu.const
> >  import evemu.exception
> >  
> >  
> > +def expected_or_error(result, func, args, is_expected):
> > +    """
> > +    Raise an ExecutionError for an unexpected result (is_expected == True).
> 
> this commment seems off - the error is raised if is_expected == False.

Ups.

> how about calling this "raise_error_if" and reordering the parameters? that
> way the caller code would be
> 
>     def expect_eq_zero(result, func, args):
>         """ Expect 'result' being equal to zero. """
>         return raise_error_if(result != 0, result, func, args)
> 
> I think that's more self-explanatory than the current approach, and you can
> rename "is_expected" to "raise_error". and the code becomes
> if raise_error:
>    ...
> else:
>    return args

Will be changed.

> btw: returning "args" seems a bit odd, is that intended?

Yes:
    http://docs.python.org/3.3/library/ctypes.html
    ...
    If the errcheck function returns the argument tuple it receives
    unchanged, ctypes continues the normal processing it does on the
    output parameters.
    ...
I have added this as a comment to the code.

If you don't return args as it came in, then the foreign function call
returns the modified value instead of the original return value.
Example:
---8<---
#!/usr/bin/python

import ctypes
import ctypes.util

from ctypes import c_char_p, c_size_t

def return_str(result, func, args):
    return "The length of '%s' is %d." % (args[0].decode("utf-8"), result)

libc = ctypes.CDLL(ctypes.util.find_library("c"))

strlen = libc.strlen
strlen.argtypes = (c_char_p,)
strlen.restype = c_size_t

print(strlen(b"Hello World!"))
    # output: 12

strlen.errcheck = return_str # Change the behaviour of strlen()!

print(strlen(b"Hello World!"))
    # output: The length of 'Hello World!' is 12.
---8<---

> > +
> > +    The exception message includes the API call (name) plus arguments, the
> > +    unexpected result and, if errno is not zero, text describing the
> > +    error number.
> > +    """
> > +    def get_call_str():
> > +        """ Returns a str 'function_name(argument_values...)'. """
> > +        strargs = []
> > +        for (num, arg) in enumerate(func.argtypes):
> > +            # convert args to str for readable output
> > +            if arg == c_char_p:
> > +                strargs.append('"%s"' % args[num].decode(evemu.const.ENCODING))
> > +            elif arg == c_void_p:
> > +                strargs.append(hex(int(args[num])))
> > +            else:
> > +                strargs.append(str(args[num]))
> > +        return "%s(%s)" % (func.__name__, ", ".join(strargs))
> > +
> > +    def get_retval_str():
> > +        """ Returns a str with the unexpected return value. """
> > +        return ", Unexpected return value: %s" % result
> > +
> > +    def get_errno_str():
> > +        """ Returns a str describing the error number or an empty string. """
> > +        errno = ctypes.get_errno()
> > +        if errno != 0:
> > +            return ", errno[%d]: %s" % (errno, os.strerror(errno))
> > +        else:
> > +            return ""
> > +
> > +    if is_expected:
> > +        return args
> > +    else:
> > +        msg = "%s%s%s" % (get_call_str(), get_retval_str(), get_errno_str())
> > +        raise evemu.exception.ExecutionError(msg)
> > +
> > +
> > +def expect_eq_zero(result, func, args):
> > +    """ Expect 'result' being equal to zero. """
> > +    return expected_or_error(result, func, args, result == 0)
> > +
> 
> not sure how the passing of objects works exactly but you may be able to
> have expect_eq, expect_ne, expect_gt and the actual expected value as
> attribute on the function somewhere.
> e.g. in the declaration call something like
> 
>         "evemu_new": {
>             "argtypes": (c_char_p,),
>             "restype": c_void_p,
>             "comparison": expect_ne,
>             "retval": None
>         }
> 
> and in the loading call something like
>    api_call.errcheck = attrs["comparison"]
>    api_call.retval = attrs["retval"]
> and then use this here (provided you still have that object, I didn't try)

Yes, that's possible. This would allow us to remove expect_not_null().
But, on the other hand it requires to add the expected value at every
"prototype" next to the comparision function we've to specify anyways.

I'd stick with this solution until more advanced comparisions are
necessary, i.e. range checks.

> having said all this, I like this wrapper approach a lot more than the
> current call() interface, thanks.

Thanks.

> > +
> > +def expect_ge_zero(result, func, args):
> > +    """ Expect 'result' being greater or equal to zero. """
> > +    return expected_or_error(result, func, args, result >= 0)
> > +
> > +
> > +def expect_gt_zero(result, func, args):
> > +    """ Expect 'result' being greater then zero. """
> > +    return expected_or_error(result, func, args, result > 0)
> > +
> > +
> > +def expect_not_none(result, func, args):
> > +    """ Expect 'result' being not None. """
> > +    return expected_or_error(result, func, args, result is not None)
> > +
> > +
> > +class LibraryWrapper(object):
> > +    """
> > +    Base class for wrapping a shared library.
> > +    """
> > +    _loaded_lib = None
> > +        # Class variable containing the instance returned by CDLL(), which
> > +        # represents the shared library.
> > +        # Initialized once, shared between all instances of this class.
> > +
> > +    def __init__(self):
> > +        super(LibraryWrapper, self).__init__()
> > +        self._load()
> > +
> > +    # Prototypes for the API calls to wrap. Needs to be overwritten by sub
> > +    # classes.
> > +    _api_prototypes = {
> > +        #"API_CALL_NAME": {
> > +        #    "argtypes": sequence of ARGUMENT TYPES,
> > +        #    "restype": RETURN TYPE,
> > +        #    "errcheck": callback for return value checking, optional
> > +        #    },
> > +        }
> > +
> > +    @classmethod
> > +    def _load(cls):
> > +        """
> > +        Returns an instance of the wrapped shared library.
> > +
> > +        If not already initialized: set argument and return types on API
> > +        calls and optionally a callback function for return value checking.
> > +        Add the API call as attribute to the class at the end.
> > +        """
> > +        if cls._loaded_lib is not None:
> > +            # Already initialized, just return it.
> > +            return cls._loaded_lib
> > +
> > +        # Get an instance of the wrapped shared library.
> > +        cls._loaded_lib = cls._cdll()
> > +
> > +        # Iterate the API call prototypes.
> > +        for (name, attrs) in cls._api_prototypes.items():
> > +            # Get the API call.
> > +            api_call = getattr(cls._loaded_lib, name)
> > +            # Add argument and return types.
> > +            api_call.argtypes = attrs["argtypes"]
> > +            api_call.restype = attrs["restype"]
> > +            # Optionally, add a callback for return value checking.
> > +            if "errcheck" in attrs:
> > +                api_call.errcheck = attrs["errcheck"]
> > +            # Add the API call as attribute to the class.
> > +            setattr(cls, name, api_call)
> > +
> > +        return cls._loaded_lib
> > +
> > +    @staticmethod
> > +    # @abc.abstractmethod - Would be nice here, but it can't be mixed with
> > +    #                       @staticmethod until Python 3.3.
> > +    def _cdll():
> > +        """ Returns a new instance of the wrapped shared library. """
> > +        raise NotImplementedError
> > +
> > +
> >  class EvEmuBase(object):
> >      """
> >      A base wrapper class for the evemu functions, accessed via ctypes.
> > -- 
> > 1.8.5.2
> > 
> > _______________________________________________
> > Input-tools mailing list
> > Input-tools at lists.freedesktop.org
> > http://lists.freedesktop.org/mailman/listinfo/input-tools


More information about the Input-tools mailing list