python-libei 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
libei/_capi/loader.py ADDED
@@ -0,0 +1,112 @@
1
+ """Deferred loading of the native libei/libeis/liboeffis shared libraries.
2
+
3
+ ``ctypes.CDLL(soname)`` raises ``OSError`` immediately if the library isn't
4
+ installed. Binding that call to module import (as a naive ctypes wrapper
5
+ would) means ``import libei.ei`` fails hard on any system that hasn't
6
+ installed libei -- even for code that only wants to construct dataclasses or
7
+ introspect enums. :class:`LazyLibrary` defers the ``dlopen`` until the first
8
+ call actually made through it, so importing this package is always safe and
9
+ callers can check :meth:`LazyLibrary.is_available` before depending on it.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import ctypes
15
+ import threading
16
+ from collections.abc import Callable, Sequence
17
+ from typing import Any
18
+
19
+
20
+ class LibraryNotFoundError(RuntimeError):
21
+ """The native shared library could not be loaded."""
22
+
23
+
24
+ class LazyLibrary:
25
+ """A ctypes.CDLL that only opens the library on first real use."""
26
+
27
+ def __init__(self, soname: str) -> None:
28
+ self._soname = soname
29
+ self._lib: ctypes.CDLL | None = None
30
+ self._load_error: OSError | None = None
31
+ self._lock = threading.Lock()
32
+
33
+ def _ensure_loaded(self) -> ctypes.CDLL:
34
+ # Double-checked: the unlocked read is the fast path taken by every
35
+ # call after the first, and the repeated check inside the lock is
36
+ # what makes it safe -- two threads can both fall through the first
37
+ # check, and the second one must not dlopen() again after the first
38
+ # has finished. The repetition is deliberate, not redundant.
39
+ if self._lib is not None:
40
+ return self._lib
41
+ with self._lock:
42
+ if self._lib is not None:
43
+ return self._lib
44
+ # A failed load is cached and re-raised rather than retried:
45
+ # a missing library does not appear mid-process, and retrying
46
+ # would pay the dlopen() cost on every call for a caller that
47
+ # ignores the exception in a loop.
48
+ if self._load_error is not None:
49
+ raise LibraryNotFoundError(
50
+ f"{self._soname} is not available"
51
+ ) from self._load_error
52
+ try:
53
+ self._lib = ctypes.CDLL(self._soname, use_errno=True)
54
+ except OSError as exc:
55
+ self._load_error = exc
56
+ raise LibraryNotFoundError(f"{self._soname} is not available") from exc
57
+ return self._lib
58
+
59
+ def is_available(self) -> bool:
60
+ """Whether the library can be loaded on this system.
61
+
62
+ Attempts the load if it hasn't been tried yet, then caches the
63
+ result -- callers can use this to skip integration tests or fall
64
+ back to another backend without triggering an exception.
65
+ """
66
+ try:
67
+ self._ensure_loaded()
68
+ except LibraryNotFoundError:
69
+ return False
70
+ return True
71
+
72
+ def function(
73
+ self,
74
+ name: str,
75
+ argtypes: Sequence[type],
76
+ restype: type | None,
77
+ ) -> Callable[..., Any]:
78
+ """Bind a single C function, resolved and typed on first call.
79
+
80
+ ``argtypes``/``restype`` are applied the first time the function is
81
+ actually invoked, not when this method runs -- so declaring a full
82
+ set of bindings at module scope never touches the filesystem.
83
+ """
84
+ # A one-slot dict used as a mutable cell. `call` below has to write
85
+ # the resolved function back somewhere the *next* call can see, and
86
+ # a plain `bound = ...` inside `call` would just create a local. A
87
+ # `nonlocal` on a variable declared here would work equally well;
88
+ # the dict is chosen only because "absent from the dict" already
89
+ # means "not resolved yet", with no None sentinel to confuse with a
90
+ # legitimately-None value.
91
+ cache: dict[str, Any] = {}
92
+
93
+ def call(*args: Any) -> Any:
94
+ # Resolution happens here, on first call, not at bind time --
95
+ # that is the whole point of this module (see its docstring).
96
+ bound = cache.get("f")
97
+ if bound is None:
98
+ lib = self._ensure_loaded()
99
+ try:
100
+ bound = getattr(lib, name)
101
+ except AttributeError as exc:
102
+ raise LibraryNotFoundError(
103
+ f"{self._soname} does not export {name!r} "
104
+ "(installed version may be too old)"
105
+ ) from exc
106
+ bound.argtypes = list(argtypes)
107
+ bound.restype = restype
108
+ cache["f"] = bound
109
+ return bound(*args)
110
+
111
+ call.__name__ = name
112
+ return call
libei/_cobject.py ADDED
@@ -0,0 +1,252 @@
1
+ """Base class for Python wrappers around refcounted opaque C pointers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import weakref
7
+ from typing import Any, ClassVar, TypeVar
8
+
9
+ T = TypeVar("T", bound="CObject")
10
+
11
+
12
+ class CObject:
13
+ """Base class for a Python wrapper around a refcounted C pointer.
14
+
15
+ Subclasses pick up three things:
16
+
17
+ * ``_as_parameter_``, which ctypes consults automatically, so a
18
+ wrapper instance can be passed straight to any bound C function
19
+ expecting the underlying pointer.
20
+ * Automatic ref/unref, driven by ``_ref_func``/``_unref_func`` (each
21
+ a ``staticmethod``-wrapped bound C function, or ``None``).
22
+ * An identity cache, so the same pointer yields the same Python
23
+ object for as long as that object stays alive. Use :meth:`wrap`
24
+ for a borrowed pointer and :meth:`adopt` for an owned one. The
25
+ cache is shared by a class and all its subclasses, so one pointer
26
+ has one wrapper no matter which class in the hierarchy wraps it --
27
+ see :meth:`__init_subclass__` for why that matters.
28
+
29
+ Subclasses that define ``__init__`` must accept and forward the
30
+ keyword-only ``_adopt`` flag, or :meth:`wrap`/:meth:`adopt` will fail
31
+ on them.
32
+
33
+ .. note::
34
+ The identity cache is keyed by raw address, and C libraries reuse
35
+ addresses. If the library frees an object and allocates a new one
36
+ at the same address while a wrapper for the old one is still
37
+ alive, :meth:`wrap` returns the stale wrapper. :meth:`release`
38
+ closes this for the short-lived objects where it actually bites
39
+ (events, allocated and freed in a tight cycle); for longer-lived
40
+ objects such as seats and devices, hold the wrapper for as long as
41
+ you hold the C object and the question doesn't arise.
42
+ """
43
+
44
+ # These must be wrapped in staticmethod(). A plain function stored as
45
+ # a class attribute is a descriptor: `self._ref_func` would hand back
46
+ # a *bound* method, passing `self` to the C call as an extra leading
47
+ # argument, and giving weakref.finalize a strong reference to `self`
48
+ # that stops the object ever being collected.
49
+ _ref_func: ClassVar[staticmethod[[int], Any] | None] = None
50
+ _unref_func: ClassVar[staticmethod[[int], Any] | None] = None
51
+ # False on classes that the library only ever hands out as brand-new,
52
+ # freshly-allocated roots (ei.Context/Sender/Receiver, eis.Eis) rather
53
+ # than as a sub-object reachable from some other wrapped object. There
54
+ # is no legitimate pointer to wrap() or adopt() for those -- callers
55
+ # get them from create_for_fd(), never from a getter -- so wrap()ing an
56
+ # arbitrary int would construct a real wrapper around it and hand that
57
+ # pointer straight to a C call (e.g. Context.__init__'s
58
+ # log_set_handler()) with no way for this package or ctypes to tell a
59
+ # garbage address from a real `struct ei *`. Blocking it here turns
60
+ # that from a process-killing segfault into a catchable TypeError.
61
+ _wrappable: ClassVar[bool] = True
62
+ _instances: ClassVar[weakref.WeakValueDictionary[int, CObject]]
63
+ # Reentrant: _get_or_create holds this while running __init__, which
64
+ # registers the new object under the same lock, and which for some
65
+ # subclasses also makes C calls that can re-enter Python.
66
+ _instances_lock: ClassVar[threading.RLock]
67
+
68
+ def __init_subclass__(cls, **kwargs: Any) -> None:
69
+ super().__init_subclass__(**kwargs)
70
+ # One cache per wrapper *hierarchy*, not per class. Only a root
71
+ # wrapper class -- one whose only CObject ancestor is CObject
72
+ # itself -- gets a fresh cache; deeper subclasses share their
73
+ # root's.
74
+ #
75
+ # This is load-bearing for the unref-only classes. ei.Sender and
76
+ # ei.Receiver subclass ei.Context, which has an _unref_func and no
77
+ # _ref_func. With a cache per class, Sender.create_for_fd() would
78
+ # register its wrapper under Sender, and a later Context.wrap() on
79
+ # the same pointer would miss it, build a *second* wrapper with a
80
+ # *second* finalizer, and take no ref -- two ei_unref() calls for
81
+ # one reference, i.e. a use-after-free. (Context/Eis now also set
82
+ # _wrappable = False, which closes this specific path a second,
83
+ # earlier way -- Context.wrap() never gets far enough to look at
84
+ # the cache at all. The shared cache stays as the general-purpose
85
+ # invariant for any other unref-only hierarchy shaped like this
86
+ # one, wrappable or not.)
87
+ #
88
+ # Sharing the cache also gives _get_or_create's isinstance() check
89
+ # something real to do: Context.wrap() on a cached Sender now
90
+ # returns that Sender, while Sender.wrap() on a cached plain
91
+ # Context raises instead of silently double-wrapping.
92
+ root = next(
93
+ (b for b in cls.__mro__[1:] if b is not CObject and issubclass(b, CObject)),
94
+ None,
95
+ )
96
+ if root is None:
97
+ cls._instances = weakref.WeakValueDictionary()
98
+ cls._instances_lock = threading.RLock()
99
+
100
+ def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
101
+ if not pointer:
102
+ raise ValueError(f"{type(self).__name__} cannot wrap a NULL pointer")
103
+ self._pointer = pointer
104
+ # Kept even after release() zeroes _pointer, so __hash__ stays
105
+ # stable for the object's whole lifetime -- a hash that changes
106
+ # would silently lose the object from any set or dict holding it.
107
+ self._hash_key = pointer
108
+ self._finalizer: Any = None # weakref.finalize's generics need a ParamSpec
109
+ if self._ref_func is not None and not _adopt:
110
+ self._ref_func(pointer)
111
+ if self._unref_func is not None:
112
+ self._finalizer = weakref.finalize(self, self._unref_func, pointer)
113
+ # Self-register, so a wrapper built by direct construction (as the
114
+ # create_for_*() constructors do) is still found by a later
115
+ # wrap()/adopt(). Without this, an unref-only class -- one with an
116
+ # _unref_func but no _ref_func, such as Context/Eis/Event/Touch --
117
+ # would get a *second* wrapper with a *second* finalizer but no
118
+ # extra reference, and the C object would be unref'd twice.
119
+ with type(self)._instances_lock:
120
+ type(self)._instances[pointer] = self
121
+
122
+ @property
123
+ def _as_parameter_(self) -> int:
124
+ if self._pointer == 0:
125
+ raise RuntimeError(
126
+ f"{type(self).__name__} has already been released; "
127
+ "the underlying C object no longer exists"
128
+ )
129
+ return self._pointer
130
+
131
+ def release(self) -> None:
132
+ """Drop this object's C reference now rather than at GC time.
133
+
134
+ Also evicts the wrapper from the identity cache and invalidates
135
+ it, so any later use raises rather than reading through a pointer
136
+ the C library may already have reused. Idempotent, and optional --
137
+ skipping it just leaves the unref to the garbage collector.
138
+
139
+ Worth calling explicitly wherever the *timing* of the unref is
140
+ load-bearing. libei sends a SYNC event's pong reply exactly when
141
+ that event is unref'd, so a caller waiting on the reply can stall
142
+ indefinitely if the unref is left to the GC; the ``events``
143
+ generators in :mod:`libei.ei` and :mod:`libei.eis` call this after
144
+ each event for that reason.
145
+ """
146
+ # Invalidate and evict under one lock hold. Zeroing _pointer first
147
+ # and locking afterwards leaves a window where a concurrent
148
+ # _get_or_create can find this object still cached and hand back a
149
+ # wrapper that is already invalid.
150
+ with type(self)._instances_lock:
151
+ pointer = self._pointer
152
+ if pointer == 0:
153
+ return
154
+ self._pointer = 0
155
+ if type(self)._instances.get(pointer) is self:
156
+ type(self)._instances.pop(pointer, None)
157
+ if self._finalizer is not None:
158
+ self._finalizer()
159
+
160
+ @classmethod
161
+ def _get_or_create(cls: type[T], pointer: int | None, *, adopt: bool) -> T | None:
162
+ if not pointer:
163
+ return None
164
+ if not cls._wrappable:
165
+ # Checked before the lock, and well before the pointer ever
166
+ # reaches a C call: cls(pointer, ...) below is what would
167
+ # dereference it.
168
+ raise TypeError(
169
+ f"{cls.__name__} cannot be constructed from a raw pointer via "
170
+ "wrap()/adopt() -- use its own create_for_*() classmethod instead"
171
+ )
172
+ # Locked so two threads racing to wrap the same new pointer can't
173
+ # both pass the "not cached yet" check and construct (and ref)
174
+ # two separate wrappers for it.
175
+ with cls._instances_lock:
176
+ existing = cls._instances.get(pointer)
177
+ if existing is not None:
178
+ if not isinstance(existing, cls):
179
+ # A real exception, not `assert`: this guards against
180
+ # a genuine cross-class pointer collision (or a bug
181
+ # letting one through), and assertions disappear
182
+ # under `python -O` -- silently returning the wrong
183
+ # wrapper type is worse than the check never running.
184
+ #
185
+ # ei.py/eis.py do use bare `assert x is not None` after
186
+ # wrap(), which is a different job: narrowing wrap()'s
187
+ # `T | None` for mypy at a getter the C API documents as
188
+ # never returning NULL. Losing one of those under -O
189
+ # costs an AttributeError on None; losing this one costs
190
+ # a wrapper of the wrong type over live memory.
191
+ raise TypeError(
192
+ f"pointer {pointer:#x} already wrapped as "
193
+ f"{type(existing).__name__}, not {cls.__name__}"
194
+ )
195
+ return existing
196
+ # __init__ self-registers, so no insert is needed here.
197
+ return cls(pointer, _adopt=adopt)
198
+
199
+ @classmethod
200
+ def wrap(cls: type[T], pointer: int | None) -> T | None:
201
+ """Return the cached wrapper for a *borrowed* pointer, or a new one.
202
+
203
+ Use this for getters (e.g. ``event.device``), which hand back a
204
+ reference the callee still owns -- an extra ref is taken to keep
205
+ the C object alive for as long as this Python wrapper is.
206
+
207
+ Returns ``None`` for a NULL pointer, matching the C API's own
208
+ "may return NULL" contract instead of raising.
209
+
210
+ Raises ``TypeError`` immediately, without touching the pointer, if
211
+ ``cls`` has ``_wrappable = False`` (``ei.Context``/``Sender``/
212
+ ``Receiver``, ``eis.Eis``) -- those are only ever handed out
213
+ freshly-created by their own ``create_for_*()``, never as a
214
+ sub-object, so there is no valid pointer to pass here.
215
+ """
216
+ return cls._get_or_create(pointer, adopt=False)
217
+
218
+ @classmethod
219
+ def adopt(cls: type[T], pointer: int | None) -> T | None:
220
+ """Return the cached wrapper for an *owned* pointer, or a new one.
221
+
222
+ Use this for constructor-style functions (e.g.
223
+ ``eis_seat_new_device``) whose docs say the caller already owns
224
+ the returned reference. Unlike :meth:`wrap`, this does not take an
225
+ extra ref -- doing so would leave the object's refcount one higher
226
+ than this wrapper's single finalizer unref ever brings it back
227
+ down to, leaking it for the life of the process.
228
+
229
+ Raises ``TypeError`` immediately, without touching the pointer, if
230
+ ``cls`` has ``_wrappable = False`` -- see :meth:`wrap`.
231
+ """
232
+ return cls._get_or_create(pointer, adopt=True)
233
+
234
+ def __eq__(self, other: object) -> bool:
235
+ if not isinstance(other, CObject):
236
+ return NotImplemented
237
+ if type(self) is not type(other):
238
+ return False
239
+ # A released wrapper no longer stands for a live C object, so it
240
+ # is only equal to itself. Comparing zeroed pointers instead would
241
+ # make every released wrapper of a given type compare equal to
242
+ # every other one, however unrelated the objects they once wrapped.
243
+ if self._pointer == 0 or other._pointer == 0:
244
+ return self is other
245
+ return self._pointer == other._pointer
246
+
247
+ def __hash__(self) -> int:
248
+ # Deliberately keyed on _hash_key, not _pointer: release() zeroes
249
+ # _pointer, and an object whose hash changes mid-life vanishes
250
+ # from any set or dict it was placed in. Two wrappers can share a
251
+ # hash without being equal -- __eq__ above settles that.
252
+ return hash((type(self), self._hash_key))