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/__init__.py +31 -0
- libei/_capi/__init__.py +6 -0
- libei/_capi/libei.py +247 -0
- libei/_capi/libeis.py +285 -0
- libei/_capi/liboeffis.py +29 -0
- libei/_capi/loader.py +112 -0
- libei/_cobject.py +252 -0
- libei/ei.py +1244 -0
- libei/eis.py +1234 -0
- libei/oeffis.py +238 -0
- libei/py.typed +0 -0
- python_libei-0.1.0.dist-info/METADATA +770 -0
- python_libei-0.1.0.dist-info/RECORD +16 -0
- python_libei-0.1.0.dist-info/WHEEL +5 -0
- python_libei-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_libei-0.1.0.dist-info/top_level.txt +1 -0
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))
|