3

I am working on a ctypes drop-in-replacement / extension and ran into an issue I do not fully understand.

I am trying to build a class factory for call-back function decorators similar to CFUNCTYPE and WINFUNCTYPE. Both factories produce classes derived from ctypes._CFuncPtr. Like every ctypes function interface, they have properties like argtypes and restype. I want to extend the classes allowing an additional property named some_param and I thought, why not, let's try this with "getter" and "setter" methods - how hard can it be ...

Because I am trying to use "getter" and "setter" methods (@property) on a property of a class (NOT a property of objects), I ended up writing a metaclass. Because my class is derived from ctypes._CFuncPtr, I think my metaclass must be derived from ctypes._CFuncPtr.__class__ (I could be wrong here).

The example below works, sort of:

import ctypes

class a_class:

    def b_function(self, some_param_parg):

        class c_class_meta(ctypes._CFuncPtr.__class__):
            def __init__(cls, *args):
                super().__init__(*args) # no idea if this is good ...
                cls._some_param_ = some_param_parg
            @property
            def some_param(cls):
                return cls._some_param_
            @some_param.setter
            def some_param(cls, value):
                if not isinstance(value, list):
                    raise TypeError('some_param must be a list')
                cls._some_param_ = value

        class c_class(ctypes._CFuncPtr, metaclass = c_class_meta):
            _argtypes_ = ()
            _restype_ = None
            _flags_ = ctypes._FUNCFLAG_STDCALL # change for CFUNCTYPE or WINFUNCTYPE etc ...

        return c_class

d_class = a_class().b_function([1, 2, 3])
print(d_class.some_param)
d_class.some_param = [2, 6]
print(d_class.some_param)
d_class.some_param = {} # Raises an error - as expected

So far so good - using the above any further does NOT work anymore. The following pseudo-code (if used on an actual function from a DLL or shared object) will fail - in fact, it will cause the CPython interpreter to segfault ...

some_routine = ctypes.windll.LoadLibrary('some.dll').some_routine
func_type = d_class(ctypes.c_int16, ctypes.c_int16) # similar to CFUNCTYPE/WINFUNCTYPE
func_type.some_param = [4, 5, 6] # my "special" property
some_routine.argtypes = (ctypes.c_int16, func_type)
@func_type
def demo(x):
    return x - 1
some_routine(4, demo) # segfaults HERE!

I am not entirely sure what goes wrong. ctypes._CFuncPtr is implemented in C, which could be a relevant limitation ... I could also have made a mistake in the implementation of the metaclass. Can someone enlighten me?

(For additional context, I am working on this function.)

s-m-e
  • 3,433
  • 2
  • 34
  • 71

1 Answers1

1

Maybe ctypes metaclasses simply won't work nicely being subclasses - since it is itself written in C, it may bypass the routes inheritance imposes for some shortcuts and end up in failures.

Ideally this "bad behavior" would have to be properly documented, filled as bugs against CPython's ctypes and fixed - to my knowledge there are not many people who can fix ctypes bugs.

On the other hand, having a metaclass just because you want a property-like attribute at class level is overkill.

Python's property itself is just pre-made, very useful builtin class that implements the descriptor protocol. Any class you create yourself that implements proper __get__ and __set__ methods can replace "property" (and often, when logic is shared across property-attributes, leads to shorter, non duplicated code)

On a second though, unfortunately, descriptor setters will only work for instances, not for classes (which makes sense, since doing cls.attr will already get you the special code-guarded value, and there is no way a __set__ method could be called on it)

So, if you could work with "manually" setting the values in the cls.__dict__ and putting your logic in the __get__ attribute, you could do:

PREFIX = "_cls_prop_"

class ClsProperty:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        value = owner.__dict__.get(PREFIX + self.name)
        # Logic to transform/check value goes here:
        if not isinstance(value, list):
            raise TypeError('some_param must be a list')
        return value


def b_function(some_param_arg):

    class c_class(ctypes._CFuncPtr):
        _argtypes_ = ()
        _restype_ = None
        _flags_ = 0 # ctypes._FUNCFLAG_STDCALL # change for CFUNCTYPE or WINFUNCTYPE etc ...

        _some_param_ = ClsProperty()

    setattr(c_class, PREFIX + "_some_param_", some_param_arg) 

    return c_class


d_class = b_function([1, 2, 3])
print(d_class._some_param_)
d_class._some_param_ = [1, 2]
print(d_class._some_param_)

If that does not work, I don't think other approaches trying to extend CTypes metaclass will work anyway, but if you want a try, instead of a "meta-property", you might try to customize the metaclass' __setitem__ instead, to do your parameter checking, instead of using property.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Thanks a lot for the answer. I thought I would not get one at all. Actually, it was an [answer of yours](https://stackoverflow.com/a/34984506/1672565) to a question of mine that inspired the whole mad project, so thanks for that too ;) I (mildly) edited the code in your answer to reflect how it *should* look like AFAIK (?), but it still does not work. The line `c_class.__dict__[PREFIX + '_some_param_'] = some_param_arg` fails with `TypeError: 'mappingproxy' object does not support item assignment`. I have not yet managed to make some sense of it. What do I have to do to complete the example? – s-m-e Mar 28 '19 at 21:10
  • Yes - sorry for that - `setattr(c_class, PREFIX + "_some_param_", some_param_arg)` should work there. – jsbueno Mar 29 '19 at 19:13