diff options
| author | Floris Bruynooghe <flub@google.com> | 2019-10-08 23:03:12 +0200 |
|---|---|---|
| committer | David Bremner <david@tethera.net> | 2019-12-03 08:12:30 -0400 |
| commit | 83c2d158983875bf77a9b7662894df585b61741c (patch) | |
| tree | 8443e3ab530a9cbf00b17c395f03e19138d3bae0 /bindings/python-cffi/notdb/_base.py | |
| parent | 5f9ea4d2908a597acaf0b809b6f27fa74b70520b (diff) | |
Introduce CFFI-based python bindings
This introduces CFFI-based Python3-only bindings.
The bindings aim at:
- Better performance on pypy
- Easier to use Python-C interface
- More "pythonic"
- The API should not allow invalid operations
- Use native object protocol where possible
- Memory safety; whatever you do from python, it should not coredump.
Diffstat (limited to 'bindings/python-cffi/notdb/_base.py')
| -rw-r--r-- | bindings/python-cffi/notdb/_base.py | 238 |
1 files changed, 238 insertions, 0 deletions
diff --git a/bindings/python-cffi/notdb/_base.py b/bindings/python-cffi/notdb/_base.py new file mode 100644 index 00000000..acb64413 --- /dev/null +++ b/bindings/python-cffi/notdb/_base.py @@ -0,0 +1,238 @@ +import abc +import collections.abc + +from notdb import _capi as capi +from notdb import _errors as errors + + +__all__ = ['NotmuchObject', 'BinString'] + + +class NotmuchObject(metaclass=abc.ABCMeta): + """Base notmuch object syntax. + + This base class exists to define the memory management handling + required to use the notmuch library. It is meant as an interface + definition rather than a base class, though you can use it as a + base class to ensure you don't forget part of the interface. It + only concerns you if you are implementing this package itself + rather then using it. + + libnotmuch uses a hierarchical memory allocator, where freeing the + memory of a parent object also frees the memory of all child + objects. To make this work seamlessly in Python this package + keeps references to parent objects which makes them stay alive + correctly under normal circumstances. When an object finally gets + deleted the :meth:`__del__` method will be called to free the + memory. + + However during some peculiar situations, e.g. interpreter + shutdown, it is possible for the :meth:`__del__` method to have + been called, whele there are still references to an object. This + could result in child objects asking their memeory to be freed + after the parent has already freed the memory, making things + rather unhappy as double frees are not taken lightly in C. To + handle this case all objects need to follow the same protocol to + destroy themselves, see :meth:`destroy`. + + Once an object has been destroyed trying to use it should raise + the :exc:`ObjectDestroyedError` exception. For this see also the + convenience :class:`MemoryPointer` descriptor in this module which + can be used as a pointer to libnotmuch memory. + """ + + @abc.abstractmethod + def __init__(self, parent, *args, **kwargs): + """Create a new object. + + Other then for the toplevel :class:`Database` object + constructors are only ever called by internal code and not by + the user. Per convention their signature always takes the + parent object as first argument. Feel free to make the rest + of the signature match the object's requirement. The object + needs to keep a reference to the parent, so it can check the + parent is still alive. + """ + + @property + @abc.abstractmethod + def alive(self): + """Whether the object is still alive. + + This indicates whether the object is still alive. The first + thing this needs to check is whether the parent object is + still alive, if it is not then this object can not be alive + either. If the parent is alive then it depends on whether the + memory for this object has been freed yet or not. + """ + + def __del__(self): + self._destroy() + + @abc.abstractmethod + def _destroy(self): + """Destroy the object, freeing all memory. + + This method needs to destory the object on the + libnotmuch-level. It must ensure it's not been destroyed by + it's parent object yet before doing so. It also must be + idempotent. + """ + + +class MemoryPointer: + """Data Descriptor to handle accessing libnotmuch pointers. + + Most :class:`NotmuchObject` instances will have one or more CFFI + pointers to C-objects. Once an object is destroyed this pointer + should no longer be used and a :exc:`ObjectDestroyedError` + exception should be raised on trying to access it. This + descriptor simplifies implementing this, allowing the creation of + an attribute which can be assigned to, but when accessed when the + stored value is *None* it will raise the + :exc:`ObjectDestroyedError` exception:: + + class SomeOjb: + _ptr = MemoryPointer() + + def __init__(self, ptr): + self._ptr = ptr + + def destroy(self): + somehow_free(self._ptr) + self._ptr = None + + def do_something(self): + return some_libnotmuch_call(self._ptr) + """ + + def __get__(self, instance, owner): + try: + val = getattr(instance, self.attr_name, None) + except AttributeError: + # We're not on 3.6+ and self.attr_name does not exist + self.__set_name__(instance, 'dummy') + val = getattr(instance, self.attr_name, None) + if val is None: + raise errors.ObjectDestroyedError() + return val + + def __set__(self, instance, value): + try: + setattr(instance, self.attr_name, value) + except AttributeError: + # We're not on 3.6+ and self.attr_name does not exist + self.__set_name__(instance, 'dummy') + setattr(instance, self.attr_name, value) + + def __set_name__(self, instance, name): + self.attr_name = '_memptr_{}_{:x}'.format(name, id(instance)) + + +class BinString(str): + """A str subclass with binary data. + + Most data in libnotmuch should be valid ASCII or valid UTF-8. + However since it is a C library these are represented as + bytestrings intead which means on an API level we can not + guarantee that decoding this to UTF-8 will both succeed and be + lossless. This string type converts bytes to unicode in a lossy + way, but also makes the raw bytes available. + + This object is a normal unicode string for most intents and + purposes, but you can get the original bytestring back by calling + ``bytes()`` on it. + """ + + def __new__(cls, data, encoding='utf-8', errors='ignore'): + if not isinstance(data, bytes): + data = bytes(data, encoding=encoding) + strdata = str(data, encoding=encoding, errors=errors) + inst = super().__new__(cls, strdata) + inst._bindata = data + return inst + + @classmethod + def from_cffi(cls, cdata): + """Create a new string from a CFFI cdata pointer.""" + return cls(capi.ffi.string(cdata)) + + def __bytes__(self): + return self._bindata + + +class NotmuchIter(NotmuchObject, collections.abc.Iterator): + """An iterator for libnotmuch iterators. + + It is tempting to use a generator function instead, but this would + not correctly respect the :class:`NotmuchObject` memory handling + protocol and in some unsuspecting cornercases cause memory + trouble. You probably want to sublcass this in order to wrap the + value returned by :meth:`__next__`. + + :param parent: The parent object. + :type parent: NotmuchObject + :param iter_p: The CFFI pointer to the C iterator. + :type iter_p: cffi.cdata + :param fn_destory: The CFFI notmuch_*_destroy function. + :param fn_valid: The CFFI notmuch_*_valid function. + :param fn_get: The CFFI notmuch_*_get function. + :param fn_next: The CFFI notmuch_*_move_to_next function. + """ + _iter_p = MemoryPointer() + + def __init__(self, parent, iter_p, + *, fn_destroy, fn_valid, fn_get, fn_next): + self._parent = parent + self._iter_p = iter_p + self._fn_destroy = fn_destroy + self._fn_valid = fn_valid + self._fn_get = fn_get + self._fn_next = fn_next + + def __del__(self): + self._destroy() + + @property + def alive(self): + if not self._parent.alive: + return False + try: + self._iter_p + except errors.ObjectDestroyedError: + return False + else: + return True + + def _destroy(self): + if self.alive: + try: + self._fn_destroy(self._iter_p) + except errors.ObjectDestroyedError: + pass + self._iter_p = None + + def __iter__(self): + """Return the iterator itself. + + Note that as this is an iterator and not a container this will + not return a new iterator. Thus any elements already consumed + will not be yielded by the :meth:`__next__` method anymore. + """ + return self + + def __next__(self): + if not self._fn_valid(self._iter_p): + self._destroy() + raise StopIteration() + obj_p = self._fn_get(self._iter_p) + self._fn_next(self._iter_p) + return obj_p + + def __repr__(self): + try: + self._iter_p + except errors.ObjectDestroyedError: + return '<NotmuchIter (exhausted)>' + else: + return '<NotmuchIter>' |
