ctypes: Minimal type checking in object oriented C API

Viewed 137

I am trying to wrap to python3 a very simple Object Oriented C-API. This basically follow the same pattern as:

So basically my issue is as follow, I have the following input API:

$ cat api.h
/* ns::a */
typedef struct ns_a ns_a;
ns_a* ns_a_create();
void ns_a_delete(ns_a* a);
bool ns_a_execute(ns_a* a);
/* ns::b */
typedef struct ns_b ns_b;
ns_b* ns_b_create();
void ns_b_delete(ns_b* b);
bool ns_b_run(ns_b* b);

Which I then wrap using the following steps:

class _TypeSafeHandle:
    def __init__(self, ptr):
        self._as_parameter_ = ptr
    @classmethod
    def from_param(cls, obj):
        if obj.__class__ != cls:
            raise ValueError(f"Not a {obj.__class__.__name__} reference")
        if not obj._as_parameter_:
            raise ValueError("Passing undefined instance")
        return obj

@final
class _A(_TypeSafeHandle):
    """ for library author, not intended to be exposed publicly """
    def __init__(self, ptr):
        super().__init__(ptr)

@final
class _B(_TypeSafeHandle):
    def __init__(self, ptr):
        super().__init__(ptr)

Given the type definition I can now expose the C-API as:

# a
ns_a_create = _func('ns_a_create', c_void_p, None, _check_ns_a)
ns_a_delete = _func('ns_a_delete', None, [_A])
ns_a_execute = _func('ns_a_execute', c_bool, [_A])
# b
ns_b_create = _func('ns_b_create', c_void_p, None, _check_ns_b)
ns_b_delete = _func('ns_b_delete', None, [_B])
ns_b_run = _func('ns_b_run', c_bool, [_B])

Using:

def _func(name, restype, argtypes, errcheck):
    func = getattr(_lib, name)
    func.restype = restype
    func.argtypes = argtypes
    func.errcheck = errcheck
    return func

and:

def _check_ns_a(result, _func, _args):
    if result is None:
        raise MemoryError("internal memory allocation failure")
    if type(result) != int:
        raise TypeError("Expecting int value for pointer")
    return = _A(c_void_p(result))

and

def _check_ns_b(result, _func, _args):
    if result is None:
        raise MemoryError("internal memory allocation failure")
    if type(result) != int:
        raise TypeError("Expecting int value for pointer")
    return = _B(c_void_p(result))

At this step, I can now expose the class A to my user:

class A:
    def __init__(self):
        self._a = ns_a_create()
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        ns_a_delete(self._a)
    def execute(self):
        ret = ns_a_execute(self._a)
        if not ret:
            raise ValueError("Could not execute")

I'd like to keep the intermediate _A and _B class as implementation detail for the library author (avoid shooting in the foot passing wrong instance to wrong delete function). I find it more readable than declaring a forward declared class A:

class A:  # forward declaration
    pass
# _func declaration with type `A` instead of `_A`
class A:
  # actual implementation detail

So my question is: is there a way to reduce code redundancy in between _A and _B ? Is there a way to reduce code redundancy in between _check_ns_a and _check_ns_b ?

0 Answers
Related