ctyped 0.8.1__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.
ctyped/__init__.py ADDED
@@ -0,0 +1 @@
1
+ VERSION = '0.8.1'
ctyped/exceptions.py ADDED
@@ -0,0 +1,20 @@
1
+
2
+
3
+ class CtypedException(Exception):
4
+ """"""
5
+
6
+
7
+ class FunctionRedeclared(CtypedException):
8
+ """"""
9
+
10
+
11
+ class UnsupportedTypeError(CtypedException):
12
+ """"""
13
+
14
+
15
+ class TypehintError(CtypedException):
16
+ """"""
17
+
18
+
19
+ class SniffingError(CtypedException):
20
+ """"""
ctyped/library.py ADDED
@@ -0,0 +1,472 @@
1
+ import ctypes
2
+ import inspect
3
+ import logging
4
+ from contextlib import contextmanager
5
+ from ctypes.util import find_library
6
+ from functools import partial, partialmethod, reduce
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Union, List, Dict, Type, ContextManager
9
+
10
+ from .exceptions import UnsupportedTypeError, TypehintError, CtypedException
11
+ from .sniffer import NmSymbolSniffer, SniffResult
12
+ from .types import CChars, CastedTypeBase, CStruct
13
+ from .utils import cast_type, extract_func_info, FuncInfo
14
+
15
+ LOGGER = logging.getLogger(__name__)
16
+
17
+
18
+ class Scopes:
19
+
20
+ def __init__(self, params: dict):
21
+ self._scopes: list[dict] = []
22
+ self._keys = ['prefix', 'str_type', 'int_bits', 'int_sign']
23
+ self.push(params)
24
+
25
+ def __call__(
26
+ this,
27
+ prefix: str | None = None,
28
+ str_type: type[CastedTypeBase] = CChars,
29
+ int_bits: int | None = None,
30
+ int_sign: bool | None = None,
31
+ **kwargs) -> ContextManager['Scopes']:
32
+ """
33
+
34
+ :param prefix: Function name prefix to apply to functions under the manager.
35
+
36
+ :param str_type: Type to represent strings.
37
+
38
+ * ``CChars`` - strings as chars (ANSI) **default**
39
+ * ``CCharsW`` - strings as wide chars (UTF)
40
+
41
+ :param int_bits: int length to be used in function.
42
+
43
+ Possible values: 8, 16, 32, 64
44
+
45
+ :param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints.
46
+
47
+ :param kwargs:
48
+
49
+ """
50
+ return this.context(locals())
51
+
52
+ def push(self, params: dict):
53
+
54
+ scope = {key: params.get(key) for key in self._keys}
55
+ self._scopes.append(scope)
56
+
57
+ def pop(self):
58
+ self._scopes.pop()
59
+
60
+ def flatten(self):
61
+
62
+ scopes = self._scopes
63
+ keys_bool = {'int_sign'}
64
+ keys_concat = {'prefix'}
65
+ result = {}
66
+
67
+ def choose(current, prev):
68
+ return current or prev
69
+
70
+ def pick_bool(current, prev):
71
+ return current if current is not None else prev
72
+
73
+ def concat(current, prev):
74
+ return (prev or '') + (current or '')
75
+
76
+ for key in self._keys:
77
+
78
+ if key in keys_concat:
79
+ reducer = concat
80
+
81
+ elif key in keys_bool:
82
+ reducer = pick_bool
83
+
84
+ else:
85
+ reducer = choose
86
+
87
+ result[key] = reduce(reducer, (scope[key] for scope in scopes[::-1]))
88
+
89
+ return result
90
+
91
+ @contextmanager
92
+ def context(self, params: dict):
93
+ self.push(params)
94
+ yield self
95
+ self.pop()
96
+
97
+
98
+ class Library:
99
+ """Main entry point to describe C library interface.
100
+
101
+ Basic usage:
102
+
103
+ .. code-block:: python
104
+
105
+ lib = Library('mylib')
106
+
107
+ with lib.scope(prefix='mylib_'):
108
+
109
+ @lib.function()
110
+ def my_func():
111
+ ...
112
+
113
+ lib.bind_types()
114
+
115
+ """
116
+
117
+ def __init__(
118
+ self, name: Union[str, Path], *, autoload: bool = True,
119
+ prefix: str | None = None,
120
+ str_type: type[CastedTypeBase] = CChars,
121
+ int_bits: int | None = None,
122
+ int_sign: bool | None = None
123
+ ):
124
+ """
125
+
126
+ :param name: Shared library name or filepath.
127
+
128
+ :param autoload: Load library just on Library object initialization.
129
+
130
+ :param prefix: Function name prefix to apply to functions in the library.
131
+
132
+ Useful when C functions have common prefixes.
133
+
134
+ :param str_type: Type to represent strings.
135
+
136
+ * ``CChars`` - strings as chars (ANSI) **default**
137
+ * ``CCharsW`` - strings as wide chars (UTF)
138
+
139
+ .. note:: This setting is global to library. Can be changed on function definition level.
140
+
141
+ :param int_bits: int length to use by default.
142
+
143
+ Possible values: 8, 16, 32, 64
144
+
145
+ .. note:: This setting is global to library. Can be changed on function definition level.
146
+
147
+ :param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints.
148
+
149
+ .. note:: This setting is global to library. Can be changed on function definition level.
150
+
151
+ """
152
+ self.scope = Scopes(locals())
153
+ self.s = self.scope
154
+
155
+ self.name = str(name)
156
+ self.lib = None
157
+ self.funcs: dict[str, Union[Callable, partialmethod[Any]]] = {}
158
+
159
+ autoload and self.load()
160
+
161
+ def load(self):
162
+ """Loads shared library."""
163
+ name = self.name
164
+
165
+ if not Path(name).exists():
166
+ name = name.replace('lib', '', 1)
167
+ name = find_library(name)
168
+
169
+ lib = ctypes.CDLL(name, use_errno=True)
170
+
171
+ if lib._name is None:
172
+ lib = None
173
+
174
+ if lib is None:
175
+ raise CtypedException(f'Unable to find library: {name or self.name}')
176
+
177
+ self.lib = lib
178
+
179
+ def structure(
180
+ self, *,
181
+ pack: int | None = None,
182
+ str_type: CastedTypeBase | None = None,
183
+ int_bits: int | None = None,
184
+ int_sign: bool | None = None,
185
+ ):
186
+ """Class decorator for C structures definition.
187
+
188
+ .. code-block:: python
189
+
190
+ @lib.structure
191
+ class MyStruct:
192
+
193
+ first: int
194
+ second: str
195
+ third: 'MyStruct'
196
+
197
+ :param pack: Allows custom maximum alignment for the fields (as #pragma pack(n)).
198
+
199
+ :param str_type: Type to represent strings.
200
+
201
+ :param int_bits: int length to be used in function.
202
+
203
+ :param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints.
204
+
205
+ """
206
+ params = locals()
207
+
208
+ def wrapper(cls_):
209
+
210
+ annotations = {
211
+ attrname: attrhint for attrname, attrhint in cls_.__annotations__.items()
212
+ if not attrname.startswith('_')}
213
+
214
+ with self.scope(**params):
215
+
216
+ cls_name = cls_.__name__
217
+
218
+ info = FuncInfo(
219
+ name_py=cls_name, name_c=None,
220
+ annotations=annotations, options=self.scope.flatten())
221
+
222
+ # todo maybe support big/little byte order
223
+ struct = type(cls_name, (CStruct, cls_), {})
224
+
225
+ ct_fields = {}
226
+ fields = []
227
+
228
+ for attrname, attrhint in annotations.items():
229
+
230
+ if attrhint == cls_name:
231
+ casted = ctypes.POINTER(struct)
232
+ ct_fields[attrname] = struct
233
+
234
+ else:
235
+ casted = cast_type(info, attrname, attrhint)
236
+
237
+ if issubclass(casted, CastedTypeBase):
238
+ ct_fields[attrname] = casted
239
+
240
+ fields.append((attrname, casted))
241
+
242
+ LOGGER.debug('Structure %s fields: %s', cls_name, fields)
243
+
244
+ if pack:
245
+ struct._pack_ = pack
246
+
247
+ struct._ct_fields = ct_fields
248
+ struct._fields_ = fields
249
+
250
+ return struct
251
+
252
+ return wrapper
253
+
254
+ def cls(
255
+ self, *,
256
+ prefix: str | None = None,
257
+ str_type: CastedTypeBase | None = None,
258
+ int_bits: int | None = None,
259
+ int_sign: bool | None = None,
260
+ ):
261
+ """Class decorator. Allows common parameters application for class methods.
262
+
263
+ .. code-block:: python
264
+
265
+ @lib.cls(prefix='common_', str_type=CCharsW)
266
+ class Wide:
267
+
268
+ @staticmethod
269
+ @lib.function()
270
+ def get_utf(some: str) -> str:
271
+ ...
272
+
273
+ :param prefix: Function name prefix to apply to functions under the manager.
274
+
275
+ :param str_type: Type to represent strings.
276
+
277
+ :param int_bits: int length to be used in function.
278
+
279
+ :param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints.
280
+
281
+ """
282
+ self.scope.push(locals())
283
+
284
+ def wrapper(cls_):
285
+ # Class body construction is done, unwind scope.
286
+ self.scope.pop()
287
+ return cls_
288
+
289
+ return wrapper
290
+
291
+ def function(
292
+ self, name_c: Union[str, Callable] | None = None, *, wrap: bool = False,
293
+ str_type: CastedTypeBase | None = None,
294
+ int_bits: int | None = None,
295
+ int_sign: bool | None = None,
296
+
297
+ ) -> Callable:
298
+ """Decorator to mark functions which exported from the library.
299
+
300
+ :param name_c: C function name with or without prefix (see ``.scope(prefix=)``).
301
+ If not set, Python function name is used.
302
+
303
+ :param wrap: Do not replace decorated function with ctypes function,
304
+ but with wrapper, allowing pre- or post-process ctypes function call.
305
+
306
+ Useful to organize functions to classes (to automatically pass ``self``)
307
+ to ctypes function to C function.
308
+
309
+ .. code-block:: python
310
+
311
+ class Thing(CObject):
312
+
313
+ @lib.function(wrap=True)
314
+ def one(self, some: int) -> int:
315
+ # Implicitly pass Thing instance alongside
316
+ # with explicitly passed `some` arg.
317
+ ...
318
+
319
+ @lib.function(wrap=True)
320
+ def two(self, some:int, cfunc: Callable) -> int:
321
+ # `cfunc` is a wrapper, calling an actual ctypes function.
322
+ # If no arguments provided the wrapper will try detect them
323
+ # automatically.
324
+ result = cfunc()
325
+ return result + 1
326
+
327
+ :param str_type: Type to represent strings.
328
+
329
+ .. note:: Overrides the same named param from library level (see ``__init__`` description).
330
+
331
+ :param int_bits: int length to be used in function.
332
+
333
+ .. note:: Overrides the same named param from library level (see ``__init__`` description).
334
+
335
+ :param int_sign: Flag. Whether to use signed (True) or unsigned (False) ints.
336
+
337
+ .. note:: Overrides the same named param from library level (see ``__init__`` description).
338
+
339
+ """
340
+ def cfunc_wrapped(*args, f: Callable, **kwargs):
341
+
342
+ if not args:
343
+ argvals = inspect.getargvalues(inspect.currentframe().f_back)
344
+ loc = argvals.locals
345
+ args = tuple(loc[argname] for argname in argvals.args if argname != 'cfunc')
346
+
347
+ return f(*args)
348
+
349
+ def function_(func_py: Callable, *, name_c: str | None, scope: dict):
350
+
351
+ info = extract_func_info(func_py, name_c=name_c, scope=scope, registry=self.funcs)
352
+ name = info.name_c
353
+
354
+ func_c = getattr(self.lib, name)
355
+
356
+ # Prepare for late binding in .bind_types().
357
+ func_c.ctyped = info
358
+
359
+ if wrap:
360
+ func_args = inspect.getfullargspec(func_py).args
361
+
362
+ if 'cfunc' in func_args:
363
+ # Use existing function, pass `cfunc`.
364
+
365
+ LOGGER.debug('Func [ %s -> %s ] uses wrapped manual call.', name, info.name_py)
366
+
367
+ func_swapped = partialmethod(func_py, cfunc=partial(cfunc_wrapped, f=func_c))
368
+
369
+ else:
370
+ # Automatically bind first param (self, cls)
371
+
372
+ LOGGER.debug('Func [ %s -> %s ] uses wrapped auto call.', name, info.name_py)
373
+
374
+ func_swapped = partialmethod(func_c)
375
+
376
+ func_swapped.cfunc = func_c
377
+ func_out = func_swapped
378
+
379
+ else:
380
+
381
+ LOGGER.debug('Func [ %s -> %s ] uses direct call.', name, info.name_py)
382
+
383
+ func_out = func_c
384
+
385
+ self.funcs[name] = func_out
386
+
387
+ return func_out
388
+
389
+ if callable(name_c):
390
+ # Decorator without params.
391
+ scope = self.scope.flatten()
392
+ py_func, name_c = name_c, None
393
+ return function_(py_func, name_c=name_c, scope=scope)
394
+
395
+ # Decorator with parameters.
396
+ with self.scope(**locals()) as scope:
397
+ scope = scope.flatten()
398
+
399
+ return partial(function_, name_c=name_c, scope=scope)
400
+
401
+ def method(self, name_c: str | None = None, **kwargs):
402
+ """Decorator. The same as ``.function()`` with ``wrap=True``."""
403
+ return self.function(name_c=name_c, wrap=True, **kwargs)
404
+
405
+ def sniff(self) -> SniffResult:
406
+ """Sniffs the library for symbols.
407
+
408
+ Sniffing result can be used as 'ctyped' code generator.
409
+
410
+ """
411
+ sniffer = NmSymbolSniffer(self.lib._name)
412
+ result = sniffer.sniff()
413
+ return result
414
+
415
+ def bind_types(self):
416
+ """Deduces ctypes argument and result types from Python type hints,
417
+ binding those types to ctypes functions.
418
+
419
+ """
420
+ LOGGER.debug('Binding signature types to ctypes functions ...')
421
+
422
+ for name_c, func_out in self.funcs.items():
423
+
424
+ func_c = getattr(func_out, 'cfunc', func_out)
425
+ func_info: FuncInfo = func_c.ctyped
426
+
427
+ name_py = func_info.name_py
428
+ annotations = func_info.annotations
429
+ errcheck = None
430
+
431
+ try:
432
+ return_is_annotated = 'return' in annotations
433
+ restype = cast_type(func_info, 'return', annotations.pop('return', None))
434
+
435
+ if restype and issubclass(restype, CastedTypeBase):
436
+ errcheck = restype._ct_res
437
+
438
+ argtypes = [cast_type(func_info, argname, argtype) for argname, argtype in annotations.items()]
439
+
440
+ except TypehintError:
441
+ # Reset annotations to allow subsequent .bind_types() calls w/o exceptions.
442
+ func_info.annotations.clear()
443
+ raise
444
+
445
+ try:
446
+ if argtypes:
447
+ func_c.argtypes = argtypes
448
+
449
+ if restype or return_is_annotated:
450
+ func_c.restype = restype
451
+
452
+ if errcheck:
453
+ func_c.errcheck = errcheck
454
+
455
+ except TypeError as e:
456
+
457
+ raise UnsupportedTypeError(
458
+ f'Unsupported types declared for {name_py} ({name_c}). '
459
+ f'Args: {argtypes}. Result: {restype}. Errcheck: {errcheck}.'
460
+ ) from e
461
+
462
+ #####################################################################################
463
+ # Shortcuts
464
+
465
+ f = function
466
+ """Shortcut for ``.function()``."""
467
+
468
+ m = method
469
+ """Shortcut for ``.method()``."""
470
+
471
+ s = None # type: ignore
472
+ """Shortcut for ``.scope()``."""
ctyped/sniffer.py ADDED
@@ -0,0 +1,128 @@
1
+ import subprocess
2
+ from typing import NamedTuple
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from textwrap import dedent
6
+ from typing import List, Union
7
+
8
+ from .exceptions import SniffingError
9
+
10
+
11
+ class SniffedSymbol(NamedTuple):
12
+ name: str
13
+ address: str
14
+ line: str
15
+ """Represents a symbol sniffed from a library."""
16
+
17
+
18
+ class SniffResult:
19
+ """Represents a library sniffing results."""
20
+
21
+ def __init__(self, *, libpath: str):
22
+ self.symbols: list[SniffedSymbol] = []
23
+ self.libpath = libpath
24
+
25
+ def add_symbol(self, symbol: SniffedSymbol):
26
+ """Added a symbol to the result."""
27
+ self.symbols.append(symbol)
28
+
29
+ def to_ctyped(self):
30
+ """Generates ctyped code from sniff result."""
31
+
32
+ dumped = [
33
+ '###',
34
+ f'# Code below was automatically generated {datetime.utcnow()} UTC',
35
+ f'# Total functions: {len(self.symbols)}',
36
+ '###',
37
+ f"lib = Library('{self.libpath}')",
38
+ ''
39
+ ]
40
+
41
+ dumped.extend(dedent(
42
+ f'''
43
+ @lib.f
44
+ def {symbol.name}():
45
+ """{symbol.line}"""
46
+ '''
47
+ ) for symbol in self.symbols)
48
+
49
+ dumped.append('\nlib.bind_types()')
50
+
51
+ return '\n'.join(dumped)
52
+
53
+
54
+ class NmSymbolSniffer:
55
+ """Uses 'nm' command from 'binutils' package to sniff a library for exported symbols."""
56
+
57
+ def __init__(self, libpath: Union[str, Path]):
58
+ """
59
+
60
+ :param libpath: Library path to sniff for symbols.
61
+
62
+ """
63
+ self.libpath = str(libpath)
64
+
65
+ def _run(self) -> list[str]:
66
+
67
+ try:
68
+ result = subprocess.run(
69
+ ['nm', '-DCl', self.libpath],
70
+ capture_output=True,
71
+ )
72
+
73
+ except FileNotFoundError: # pragma: nocover
74
+
75
+ raise SniffingError(
76
+ "Command 'nm' execution failed. "
77
+ "Make sure 'nm' command from 'binutils' package is available.") from None
78
+
79
+ if result.returncode: # pragma: nocover
80
+ raise SniffingError(f"Command 'nm' execution failed: {result.stderr.decode()}")
81
+
82
+ return result.stdout.decode().splitlines()
83
+
84
+ def _get_symbols(self, lines: list[str]) -> list[SniffedSymbol]:
85
+
86
+ symbols = []
87
+
88
+ for line in lines:
89
+
90
+ if line.startswith(' '):
91
+ continue
92
+
93
+ chunks = line.split(' ')
94
+ chunks_len = len(chunks)
95
+
96
+ if chunks_len < 2 or chunks[1] != 'T':
97
+ continue
98
+
99
+ if len(chunks) != 3: # pragma: nocover
100
+ raise SniffingError(
101
+ f"Command 'nm' execution failed: 3 chunks line expected, but given {chunks}")
102
+
103
+ address, symtype, name = chunks
104
+
105
+ if symtype != 'T' or name.startswith('_'):
106
+ continue
107
+
108
+ name, _, srcline = name.partition('\t')
109
+
110
+ symbols.append(
111
+ SniffedSymbol(
112
+ name=name,
113
+ address=address,
114
+ line=srcline,
115
+ )
116
+ )
117
+
118
+ return symbols
119
+
120
+ def sniff(self) -> SniffResult:
121
+ """Runs symbols sniffing for library."""
122
+
123
+ result = SniffResult(libpath=self.libpath)
124
+
125
+ for symbol in self._get_symbols(self._run()):
126
+ result.add_symbol(symbol)
127
+
128
+ return result
ctyped/toolbox.py ADDED
@@ -0,0 +1,3 @@
1
+ from .library import Library
2
+ from .types import CObject, CRef
3
+ from .utils import get_last_error, c_callback
ctyped/types.py ADDED
@@ -0,0 +1,200 @@
1
+ import ctypes
2
+ from typing import Any, Union
3
+
4
+
5
+ class CastedTypeBase:
6
+
7
+ @classmethod
8
+ def _ct_prep(cls, val: Any) -> Union[bytes, int]:
9
+ # Prepare value. Used for structure fields.
10
+ return val
11
+
12
+ @classmethod
13
+ def _ct_res(cls, cobj: Any, *args, **kwargs) -> Any: # pragma: nocover
14
+ # Function result caster.
15
+ raise NotImplementedError(f'{cls.__name__}._ct_res() is not implemented')
16
+
17
+ @classmethod
18
+ def from_param(cls, val: Any) -> Any:
19
+ # Function parameter caster.
20
+ return val
21
+
22
+
23
+ # getattr to cheat type hints
24
+ CShort: int = ctypes.c_short
25
+ CShortU: int = ctypes.c_ushort
26
+ CLong: int = ctypes.c_long
27
+ CLongU: int = ctypes.c_ulong
28
+ CLongLong: int = ctypes.c_longlong
29
+ CLongLongU: int = ctypes.c_ulonglong
30
+
31
+ CInt: int = ctypes.c_int
32
+ CIntU: int = ctypes.c_uint
33
+ CInt8: int = ctypes.c_int8
34
+ CInt8U: int = ctypes.c_uint8
35
+ CInt16: int = ctypes.c_int16
36
+ CInt16U: int = ctypes.c_uint16
37
+ CInt32: int = ctypes.c_int32
38
+ CInt32U: int = ctypes.c_uint32
39
+ CInt64: int = ctypes.c_int64
40
+ CInt64U: int = ctypes.c_uint64
41
+
42
+ CPointer: Any = ctypes.c_void_p
43
+ CObject = CPointer # Mere alias for those who prefer ``class My(CObject): ...`` better.
44
+
45
+
46
+ class CStruct(CastedTypeBase, ctypes.Structure):
47
+ """Helper to represent a structure using native byte order."""
48
+
49
+ @classmethod
50
+ def _ct_prep(cls, val):
51
+ return ctypes.pointer(val)
52
+
53
+ @classmethod
54
+ def _ct_res(cls, cobj: Any, *args, **kwargs) -> Any:
55
+
56
+ if isinstance(cobj, cls):
57
+ # Structure as a function result.
58
+ return cobj
59
+
60
+ # Substructure handling.
61
+ return cobj.contents
62
+
63
+ def __setattr__(self, key, value):
64
+ # Allows structure field value casting on assignments.
65
+
66
+ casted = self._ct_fields.get(key)
67
+
68
+ if casted:
69
+ value = casted._ct_prep(value)
70
+
71
+ super().__setattr__(key, value)
72
+
73
+ def __getattribute__(self, key):
74
+ # Allows customizes structure members types handling.
75
+
76
+ get = super().__getattribute__
77
+
78
+ value = get(key)
79
+
80
+ casted = get('_ct_fields').get(key)
81
+
82
+ if casted:
83
+ return casted._ct_res(value)
84
+
85
+ return value
86
+
87
+
88
+ class CRef(CastedTypeBase):
89
+ """Reference helper."""
90
+
91
+ @classmethod
92
+ def carray(cls, typecls: Any, *, size: int = 1) -> 'CRef':
93
+ """Alternative constructor. Creates a reference to array."""
94
+
95
+ typecls = {
96
+
97
+ int: ctypes.c_int,
98
+ str: ctypes.c_char,
99
+ bool: ctypes.c_bool,
100
+ float: ctypes.c_float,
101
+
102
+ }.get(typecls, typecls)
103
+
104
+ val = (typecls * (size or 1))()
105
+
106
+ return cls(val)
107
+
108
+ @classmethod
109
+ def cbool(cls, value: bool = False) -> 'CRef':
110
+ """Alternative constructor. Creates a reference to boolean."""
111
+ return cls(ctypes.c_bool(value))
112
+
113
+ @classmethod
114
+ def cint(cls, value: int = 0) -> 'CRef':
115
+ """Alternative constructor. Creates a reference to integer."""
116
+ return cls(ctypes.c_int(value))
117
+
118
+ @classmethod
119
+ def cfloat(cls, value: float = 0.0) -> 'CRef':
120
+ """Alternative constructor. Creates a reference to float."""
121
+ return cls(ctypes.c_float(value))
122
+
123
+ @classmethod
124
+ def from_param(cls, obj: 'CRef'):
125
+ return ctypes.byref(obj._ct_val)
126
+
127
+ def __init__(self, cval: Any):
128
+ self._ct_val = cval
129
+
130
+ def __iter__(self):
131
+ # Allows iteration for arrays.
132
+ return iter(self._ct_val)
133
+
134
+ def __str__(self):
135
+ val = self._ct_val.value
136
+
137
+ if isinstance(val, bytes):
138
+ val = val.decode('utf-8')
139
+
140
+ return str(val)
141
+
142
+ def __int__(self):
143
+ return int(self._ct_val.value)
144
+
145
+ def __float__(self):
146
+ return float(self._ct_val.value)
147
+
148
+ def __bool__(self):
149
+ return bool(self._ct_val.value)
150
+
151
+ def __eq__(self, other):
152
+ return self._ct_val.value == other
153
+
154
+ def __ne__(self, other):
155
+ return self._ct_val.value != other
156
+
157
+ def __lt__(self, other):
158
+ return self._ct_val.value < other
159
+
160
+ def __gt__(self, other):
161
+ return self._ct_val.value > other
162
+
163
+ def __le__(self, other):
164
+ return self._ct_val.value <= other
165
+
166
+ def __ge__(self, other):
167
+ return self._ct_val.value >= other
168
+
169
+
170
+ class CChars(CastedTypeBase, ctypes.c_char_p):
171
+ """Represents a Python string as a C chars pointer."""
172
+
173
+ @classmethod
174
+ def _ct_prep(cls, val):
175
+ return val.encode('utf-8')
176
+
177
+ @classmethod
178
+ def _ct_res(cls, cobj: 'CChars', *args, **kwargs) -> str | None:
179
+ value = cobj.value
180
+
181
+ if not value:
182
+ return ''
183
+
184
+ return value.decode('utf-8')
185
+
186
+ @classmethod
187
+ def from_param(cls, val: str):
188
+ return ctypes.c_char_p(val.encode('utf-8'))
189
+
190
+
191
+ class CCharsW(CastedTypeBase, ctypes.c_wchar_p):
192
+ """Represents a Python string as a C wide chars pointer."""
193
+
194
+ @classmethod
195
+ def _ct_res(cls, cobj: 'CCharsW', *args, **kwargs) -> str | None:
196
+ return cobj.value or ''
197
+
198
+ @classmethod
199
+ def from_param(cls, val: str):
200
+ return ctypes.c_wchar_p(val)
ctyped/utils.py ADDED
@@ -0,0 +1,172 @@
1
+ import inspect
2
+ from typing import NamedTuple
3
+ from ctypes import get_errno, CFUNCTYPE
4
+ from errno import errorcode
5
+ from os import strerror
6
+ from typing import Callable, Optional
7
+
8
+ from .exceptions import TypehintError, FunctionRedeclared
9
+ from .types import *
10
+
11
+ class FuncInfo(NamedTuple):
12
+ name_py: str
13
+ name_c: str
14
+ annotations: dict
15
+ options: dict
16
+
17
+
18
+ class ErrorInfo(NamedTuple):
19
+ num: int
20
+ code: int
21
+ msg: str
22
+
23
+
24
+ class _Missing(NamedTuple):
25
+ pass
26
+
27
+
28
+ _MISSING = _Missing()
29
+
30
+
31
+ def extract_func_info(func: Callable, *, name_c: str | None, scope: dict, registry: dict) -> FuncInfo:
32
+
33
+ name_py = func.__name__
34
+ name = scope.get('prefix', '') + (name_c or name_py)
35
+
36
+ if name in registry:
37
+ raise FunctionRedeclared(f'Unable to redeclare: {name} ({name_py})')
38
+
39
+ annotated_args = {}
40
+ annotations = func.__annotations__
41
+
42
+ # Gather all args and annotations for them.
43
+ for argname in inspect.getfullargspec(func).args:
44
+
45
+ if argname == 'cfunc':
46
+ continue
47
+
48
+ annotation = annotations.get(argname, _MISSING)
49
+
50
+ if argname == 'self' and annotation is _MISSING:
51
+ # Pick class name from qualname.
52
+ annotation = func.__qualname__.split('.')[-2]
53
+
54
+ annotated_args[argname] = annotation
55
+
56
+ annotated_args['return'] = annotations.get('return')
57
+
58
+ return FuncInfo(name_py=name_py, name_c=name, annotations=annotated_args, options=scope)
59
+
60
+
61
+ def thint_str_to_obj(thint: str):
62
+
63
+ fback = inspect.currentframe().f_back
64
+
65
+ while fback:
66
+ target = fback.f_globals.get(thint)
67
+
68
+ if target:
69
+ return target
70
+
71
+ fback = fback.f_back
72
+
73
+
74
+ def cast_type(func_info, argname: str, thint: Any):
75
+
76
+ if thint is None:
77
+ return None
78
+
79
+ if isinstance(thint, str):
80
+ thint_orig = thint
81
+ thint = thint_str_to_obj(thint)
82
+
83
+ if thint is None:
84
+ raise TypehintError(
85
+ f'Unable to resolve type hint. '
86
+ f'Function: {func_info.name_py}. Arg: {argname}. Type: {thint_orig}.')
87
+
88
+ if thint is bool:
89
+ thint = ctypes.c_bool
90
+
91
+ elif thint is float:
92
+ thint = ctypes.c_float
93
+
94
+ elif thint is str:
95
+ thint = func_info.options.get('str_type', CChars)
96
+
97
+ elif thint is int:
98
+ int_bits = func_info.options.get('int_bits')
99
+ int_sign = func_info.options.get('int_sign', False)
100
+
101
+ thint_map = {
102
+ 8: (CInt8, CInt8U),
103
+ 16: (CInt16, CInt16U),
104
+ 32: (CInt32, CInt32U),
105
+ 64: (CInt64, CInt64U),
106
+
107
+ }
108
+
109
+ if int_bits:
110
+ assert int_bits in thint_map.keys(), 'Wrong value passed for int_bits.'
111
+
112
+ else:
113
+ int_bits = 64 # todo maybe try to guess
114
+
115
+ type_idx = 1 if int_sign is False else 0
116
+
117
+ thint = thint_map[int_bits][type_idx] or thint
118
+
119
+ return thint
120
+
121
+
122
+ def get_last_error() -> ErrorInfo:
123
+ """Returns last error (``errno``) information named tuple:
124
+
125
+ .. code-block:: python
126
+
127
+ (err_no, err_code, err_message)
128
+
129
+ """
130
+ num = get_errno()
131
+ code = errorcode[num]
132
+ msg = strerror(num)
133
+
134
+ return ErrorInfo(num=num, code=code, msg=msg)
135
+
136
+
137
+ def c_callback(use_errno: bool = False) -> Callable:
138
+ """Decorator to turn a Python function into a C callback function.
139
+
140
+ .. code-block:: python
141
+
142
+ @lib.f
143
+ def c_func_using_callback(hook: CPointer) -> int:
144
+ ...
145
+
146
+ @c_callback
147
+ def hook(num: int) -> int:
148
+ return num + 10
149
+
150
+ c_func_using_callback(hook)
151
+
152
+ :param use_errno:
153
+
154
+ """
155
+ def cfunction_(func: Callable) -> Callable:
156
+
157
+ func_info = extract_func_info(func, name_c=None, scope={}, registry={})
158
+ annotations = func_info.annotations
159
+
160
+ restype = cast_type(func_info, 'return', annotations.pop('return', None))
161
+ argtypes = [cast_type(func_info, argname, argtype) for argname, argtype in annotations.items()]
162
+
163
+ functype = CFUNCTYPE(restype, *argtypes, use_errno=use_errno)
164
+ cfunc = functype(func)
165
+
166
+ return cfunc
167
+
168
+ if callable(use_errno):
169
+ # Decorator without params.
170
+ return cfunction_(use_errno)
171
+
172
+ return cfunction_
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.5
2
+ Name: ctyped
3
+ Version: 0.8.1
4
+ Summary: Build ctypes interfaces for shared libraries with type hinting
5
+ Project-URL: Homepage, https://github.com/idlesign/ctyped
6
+ Author-email: Igor Starikov <idlesign@yandex.ru>
7
+ License-Expression: BSD-3-Clause
8
+ License-File: LICENSE
9
+ Keywords: cffi,ctypes,shared library
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+
24
+ # ctyped
25
+
26
+ <https://github.com/idlesign/ctyped>
27
+
28
+ [![release](https://img.shields.io/pypi/v/ctyped.svg)](https://pypi.python.org/pypi/ctyped)
29
+ [![lic](https://img.shields.io/pypi/l/ctyped.svg)](https://pypi.python.org/pypi/ctyped)
30
+ [![coverage](https://img.shields.io/coveralls/idlesign/ctyped/master.svg)](https://coveralls.io/r/idlesign/ctyped)
31
+
32
+ ## Description
33
+
34
+ *Build ctypes interfaces for shared libraries with type hinting*
35
+
36
+ **Requires Python 3.10+**
37
+
38
+ * Less boilerplate;
39
+ * Logical structuring;
40
+ * Basic code generator (.so function -> ctyped function);
41
+ * Useful helpers.
42
+
43
+ ```python
44
+ from ctyped.toolbox import Library
45
+
46
+ # Define library.
47
+ lib = Library('mylib.so')
48
+
49
+ @lib.structure
50
+ class Box:
51
+
52
+ one: int
53
+ two: str
54
+
55
+ # Type less with function names prefixes.
56
+ with lib.scope(prefix='mylib_'):
57
+
58
+ # Describe function available in the library.
59
+ @lib.function
60
+ def some_func(title: str, year: int, box: Box) -> str:
61
+ ...
62
+ ```
63
+
64
+ ## Documentation
65
+
66
+ <https://ctyped.readthedocs.io/>
@@ -0,0 +1,11 @@
1
+ ctyped/__init__.py,sha256=WH2l3vFfy8w0Du6NRYJJK1jNK0WjKXJ202C4JjWcUWo,18
2
+ ctyped/exceptions.py,sha256=_GJHxHRPevkL7DldeUvBvOTXXNVhSpMuIfp7z-UYvco,263
3
+ ctyped/library.py,sha256=ptDGhOS7Pg6CKMEJn1Co43EWgAW6gwvvp2-FHHEM8kg,14418
4
+ ctyped/sniffer.py,sha256=pteQKKsEX4BL7S4tdPqTwZ2cQHztLEkjN4pQtfKfMcg,3361
5
+ ctyped/toolbox.py,sha256=6Yg1tM3HTudlP9ziH-Aogdj29AJdiHZ2O1iKgAjuTlY,108
6
+ ctyped/types.py,sha256=NKJ5l_53k-EWuejOGMp6xiEKzPV3LB7_xoFUJaK3m8Q,5011
7
+ ctyped/utils.py,sha256=mXAm7YW4axakHxsaMMejRM6Qvn3zfm3nPhefAzlMnjE,4135
8
+ ctyped-0.8.1.dist-info/METADATA,sha256=4vUo0QRi8pz1iBh576G0ygKcNoJXpJFog6TXUNA7ixY,1919
9
+ ctyped-0.8.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ ctyped-0.8.1.dist-info/licenses/LICENSE,sha256=vfvP40vyus-Y2rmKhWDSIGSBpLgTBuLj8YN-SU7Boyw,1499
11
+ ctyped-0.8.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,28 @@
1
+ Copyright (c) 2019-2026, Igor `idle sign` Starikov
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of the ctyped nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+