tinycwrap 0.0.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.
- tinycwrap/__init__.py +11 -0
- tinycwrap/cmodule.py +507 -0
- tinycwrap-0.0.0.dist-info/METADATA +13 -0
- tinycwrap-0.0.0.dist-info/RECORD +6 -0
- tinycwrap-0.0.0.dist-info/WHEEL +5 -0
- tinycwrap-0.0.0.dist-info/top_level.txt +1 -0
tinycwrap/__init__.py
ADDED
tinycwrap/cmodule.py
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import hashlib
|
|
3
|
+
import tempfile
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from cffi import FFI
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------- helpers --------------------------------------------------
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _strip_restrict_keywords(text: str) -> str:
|
|
16
|
+
"""Remove C restrict qualifiers (including compiler-specific variants)."""
|
|
17
|
+
return re.sub(r"\b(__restrict__|__restrict|restrict)\b", "", text)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _base_type_from_ctype(ctype: str) -> str:
|
|
21
|
+
"""Normalize base C type (strip const, *, etc.)."""
|
|
22
|
+
ctype = _strip_restrict_keywords(ctype)
|
|
23
|
+
ctype = ctype.replace("const", "").replace("volatile", "")
|
|
24
|
+
ctype = ctype.replace("*", "").strip()
|
|
25
|
+
return " ".join(ctype.split())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _numpy_dtype_for_base_type(base: str):
|
|
29
|
+
"""Map base C type -> numpy dtype."""
|
|
30
|
+
if base == "double":
|
|
31
|
+
return np.float64
|
|
32
|
+
if base == "float":
|
|
33
|
+
return np.float32
|
|
34
|
+
if base in ("int", "signed int"):
|
|
35
|
+
return np.int32
|
|
36
|
+
if base in ("long long", "long long int", "signed long long"):
|
|
37
|
+
return np.int64
|
|
38
|
+
if base in ("unsigned int", "unsigned"):
|
|
39
|
+
return np.uint32
|
|
40
|
+
# Extend here if you need more types
|
|
41
|
+
raise TypeError(f"Unsupported C base type for NumPy mapping: {base!r}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_length_name(name: str) -> bool:
|
|
45
|
+
name = name.lower()
|
|
46
|
+
if name in ("n", "len", "length", "size"):
|
|
47
|
+
return True
|
|
48
|
+
return name.startswith("len_") or name.startswith("n_") or name.startswith("size_")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ArgSpec:
|
|
53
|
+
name: str
|
|
54
|
+
raw_ctype: str
|
|
55
|
+
base_type: str
|
|
56
|
+
is_pointer: bool
|
|
57
|
+
is_const: bool
|
|
58
|
+
is_length_param: bool = False
|
|
59
|
+
is_array_in: bool = False
|
|
60
|
+
is_array_out: bool = False
|
|
61
|
+
is_scalar: bool = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class FuncSpec:
|
|
66
|
+
name: str
|
|
67
|
+
return_ctype: str
|
|
68
|
+
args: list # list[ArgSpec]
|
|
69
|
+
doc: str | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------- main class ----------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CModule:
|
|
76
|
+
"""
|
|
77
|
+
Compile & hot-reload a C file into a shared library and:
|
|
78
|
+
|
|
79
|
+
* auto-generate CFFI cdef from function definitions
|
|
80
|
+
* auto-generate NumPy-friendly Python wrappers
|
|
81
|
+
|
|
82
|
+
Conventions:
|
|
83
|
+
|
|
84
|
+
* We export all **non-static** functions found in the C file.
|
|
85
|
+
* Arguments:
|
|
86
|
+
- `const double *x` -> input NumPy array
|
|
87
|
+
- `double *x` -> in-place / output NumPy array
|
|
88
|
+
- `int len_x`, `int n`, `int size_x` -> length parameter (hidden)
|
|
89
|
+
- other scalars (int/double/float/long long) -> Python scalars
|
|
90
|
+
* A block comment immediately after the function header is used
|
|
91
|
+
as the Python docstring, e.g.:
|
|
92
|
+
|
|
93
|
+
double dot(const double *x, const double *y, int len_x)
|
|
94
|
+
/* Return dot product between x and y */
|
|
95
|
+
{
|
|
96
|
+
...
|
|
97
|
+
}
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
c_path,
|
|
103
|
+
cdef: str | None = None,
|
|
104
|
+
extra_sources=None,
|
|
105
|
+
include_dirs=None,
|
|
106
|
+
compiler="gcc",
|
|
107
|
+
compile_args=None,
|
|
108
|
+
auto_wrap=True,
|
|
109
|
+
auto_cdef=True,
|
|
110
|
+
):
|
|
111
|
+
self.c_path = Path(c_path)
|
|
112
|
+
self.extra_sources = [Path(p) for p in (extra_sources or [])]
|
|
113
|
+
self.include_dirs = list(include_dirs or [])
|
|
114
|
+
self.compiler = compiler
|
|
115
|
+
self.compile_args = compile_args or ["-O3", "-shared", "-fPIC"]
|
|
116
|
+
self.auto_wrap = auto_wrap
|
|
117
|
+
self.auto_cdef = auto_cdef
|
|
118
|
+
|
|
119
|
+
self._ffi = None
|
|
120
|
+
self._lib = None
|
|
121
|
+
self._sig = None
|
|
122
|
+
self._so_path = None
|
|
123
|
+
|
|
124
|
+
# will be filled after parsing
|
|
125
|
+
self._func_specs: dict[str, FuncSpec] = {}
|
|
126
|
+
|
|
127
|
+
# if cdef not provided, generate it from C file
|
|
128
|
+
if cdef is None and auto_cdef:
|
|
129
|
+
cdef = self._generate_cdef_from_source()
|
|
130
|
+
self.cdef = cdef
|
|
131
|
+
|
|
132
|
+
self.ensure_compiled()
|
|
133
|
+
|
|
134
|
+
# ---------- build & reload ---------------------------------------------
|
|
135
|
+
|
|
136
|
+
def _compute_sig(self):
|
|
137
|
+
h = hashlib.sha1()
|
|
138
|
+
all_paths = [self.c_path] + self.extra_sources
|
|
139
|
+
for p in all_paths:
|
|
140
|
+
st = p.stat()
|
|
141
|
+
h.update(str(p.resolve()).encode("utf-8"))
|
|
142
|
+
h.update(str(st.st_mtime_ns).encode("utf-8"))
|
|
143
|
+
return h.hexdigest()[:16]
|
|
144
|
+
|
|
145
|
+
def _needs_recompile(self):
|
|
146
|
+
if self._sig is None:
|
|
147
|
+
return True
|
|
148
|
+
return self._compute_sig() != self._sig
|
|
149
|
+
|
|
150
|
+
def _compile_and_load(self):
|
|
151
|
+
sig = self._compute_sig()
|
|
152
|
+
build_dir = Path(tempfile.gettempdir()) / "cmodule_build"
|
|
153
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
|
|
155
|
+
so_name = f"cmodule_{self.c_path.stem}_{sig}.so"
|
|
156
|
+
so_path = build_dir / so_name
|
|
157
|
+
|
|
158
|
+
cmd = [self.compiler, *self.compile_args]
|
|
159
|
+
|
|
160
|
+
include_dirs = self.include_dirs + [np.get_include()]
|
|
161
|
+
for inc in include_dirs:
|
|
162
|
+
cmd.extend(["-I", str(inc)])
|
|
163
|
+
|
|
164
|
+
sources = [str(self.c_path), *(str(p) for p in self.extra_sources)]
|
|
165
|
+
cmd.extend(["-o", str(so_path), *sources])
|
|
166
|
+
|
|
167
|
+
print(f"[CModule] Compiling: {' '.join(cmd)}")
|
|
168
|
+
subprocess.run(cmd, check=True)
|
|
169
|
+
|
|
170
|
+
ffi = FFI()
|
|
171
|
+
ffi.cdef(self.cdef)
|
|
172
|
+
lib = ffi.dlopen(str(so_path))
|
|
173
|
+
|
|
174
|
+
self._ffi = ffi
|
|
175
|
+
self._lib = lib
|
|
176
|
+
self._sig = sig
|
|
177
|
+
self._so_path = so_path
|
|
178
|
+
|
|
179
|
+
print(f"[CModule] Loaded {so_path}")
|
|
180
|
+
|
|
181
|
+
# Re-parse cdef and attach docs from C source, then create wrappers
|
|
182
|
+
if self.auto_wrap:
|
|
183
|
+
self._func_specs = self._parse_cdef(self.cdef)
|
|
184
|
+
self._attach_docs_from_source()
|
|
185
|
+
self._create_wrappers()
|
|
186
|
+
|
|
187
|
+
def ensure_compiled(self):
|
|
188
|
+
if self._needs_recompile():
|
|
189
|
+
self._compile_and_load()
|
|
190
|
+
|
|
191
|
+
# ---------- properties ---------------------------------------------------
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def ffi(self):
|
|
195
|
+
self.ensure_compiled()
|
|
196
|
+
return self._ffi
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def lib(self):
|
|
200
|
+
self.ensure_compiled()
|
|
201
|
+
return self._lib
|
|
202
|
+
|
|
203
|
+
# ---------- 1) auto-generate cdef from C source -------------------------
|
|
204
|
+
|
|
205
|
+
def _generate_cdef_from_source(self) -> str:
|
|
206
|
+
"""
|
|
207
|
+
Parse the C file and auto-generate a minimal cdef string.
|
|
208
|
+
|
|
209
|
+
We look for *definitions* of non-static functions of the form:
|
|
210
|
+
|
|
211
|
+
[static] <ret> name(<args>)
|
|
212
|
+
/* optional doc */
|
|
213
|
+
{
|
|
214
|
+
|
|
215
|
+
and turn them into:
|
|
216
|
+
<ret> name(<args>);
|
|
217
|
+
"""
|
|
218
|
+
src = self.c_path.read_text(encoding="utf8")
|
|
219
|
+
|
|
220
|
+
# Remove preprocessor lines to simplify
|
|
221
|
+
src_wo_pp = re.sub(r"^\s*#.*$", "", src, flags=re.MULTILINE)
|
|
222
|
+
|
|
223
|
+
# regex for function definitions
|
|
224
|
+
func_def_re = re.compile(
|
|
225
|
+
r"""
|
|
226
|
+
(?P<prefix>static\s+)? # optional 'static'
|
|
227
|
+
(?P<ret>[^{}();]+?) # return type
|
|
228
|
+
\s+
|
|
229
|
+
(?P<name>\w+)\s* # function name
|
|
230
|
+
\(
|
|
231
|
+
(?P<args>[^)]*) # arguments (no nested parentheses)
|
|
232
|
+
\)
|
|
233
|
+
\s*
|
|
234
|
+
(?:/\*.*?\*/\s*)? # optional trailing comment
|
|
235
|
+
\{ # function body begins
|
|
236
|
+
""",
|
|
237
|
+
re.VERBOSE | re.DOTALL,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
prototypes = []
|
|
241
|
+
|
|
242
|
+
for m in func_def_re.finditer(src_wo_pp):
|
|
243
|
+
if m.group("prefix") and "static" in m.group("prefix"):
|
|
244
|
+
# ignore static functions, not exported
|
|
245
|
+
continue
|
|
246
|
+
ret = " ".join(_strip_restrict_keywords(m.group("ret")).split())
|
|
247
|
+
name = m.group("name")
|
|
248
|
+
args = " ".join(_strip_restrict_keywords(m.group("args")).split())
|
|
249
|
+
proto = f"{ret} {name}({args});"
|
|
250
|
+
prototypes.append(proto)
|
|
251
|
+
|
|
252
|
+
if not prototypes:
|
|
253
|
+
raise RuntimeError(f"No functions found in {self.c_path} to generate cdef")
|
|
254
|
+
|
|
255
|
+
cdef = "\n".join(prototypes)
|
|
256
|
+
print("[CModule] Auto-generated cdef:\n" + cdef)
|
|
257
|
+
return cdef
|
|
258
|
+
|
|
259
|
+
# ---------- 2) parse cdef -> FuncSpec/ArgSpec ---------------------------
|
|
260
|
+
|
|
261
|
+
def _parse_cdef(self, cdef: str) -> dict[str, FuncSpec]:
|
|
262
|
+
text = cdef
|
|
263
|
+
text = re.sub(r"/\*.*?\*/", "", text, flags=re.DOTALL)
|
|
264
|
+
text = re.sub(r"//.*?$", "", text, flags=re.MULTILINE)
|
|
265
|
+
text = _strip_restrict_keywords(text)
|
|
266
|
+
|
|
267
|
+
decls = []
|
|
268
|
+
buff = []
|
|
269
|
+
for line in text.splitlines():
|
|
270
|
+
line = line.strip()
|
|
271
|
+
if not line:
|
|
272
|
+
continue
|
|
273
|
+
buff.append(line)
|
|
274
|
+
if ";" in line:
|
|
275
|
+
decl = " ".join(buff)
|
|
276
|
+
decls.append(decl)
|
|
277
|
+
buff = []
|
|
278
|
+
|
|
279
|
+
func_re = re.compile(r"(.+?)\s+(\w+)\s*\((.*?)\)\s*;")
|
|
280
|
+
|
|
281
|
+
funcs: dict[str, FuncSpec] = {}
|
|
282
|
+
|
|
283
|
+
for decl in decls:
|
|
284
|
+
m = func_re.match(decl)
|
|
285
|
+
if not m:
|
|
286
|
+
continue
|
|
287
|
+
ret_ctype, fname, arglist = m.groups()
|
|
288
|
+
ret_ctype = ret_ctype.strip()
|
|
289
|
+
arglist = arglist.strip()
|
|
290
|
+
|
|
291
|
+
if arglist == "void" or arglist == "":
|
|
292
|
+
argspecs = []
|
|
293
|
+
else:
|
|
294
|
+
argspecs = []
|
|
295
|
+
|
|
296
|
+
for raw_arg in re.split(r"\s*,\s*", arglist):
|
|
297
|
+
raw_arg = raw_arg.strip()
|
|
298
|
+
if not raw_arg:
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
# Match: [type stuff possibly with *] [name]
|
|
302
|
+
# Handles: "const double *x", "double* x", "double * x", etc.
|
|
303
|
+
m_arg = re.match(r"(.+?)\s*([A-Za-z_]\w*)$", raw_arg)
|
|
304
|
+
if not m_arg:
|
|
305
|
+
# couldn't parse this arg, skip or raise
|
|
306
|
+
# raise ValueError(f"Cannot parse argument: {raw_arg!r}")
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
ctype = m_arg.group(1).strip()
|
|
310
|
+
name = m_arg.group(2)
|
|
311
|
+
|
|
312
|
+
is_pointer = "*" in ctype
|
|
313
|
+
is_const = "const" in ctype
|
|
314
|
+
base = _base_type_from_ctype(ctype)
|
|
315
|
+
|
|
316
|
+
aspec = ArgSpec(
|
|
317
|
+
name=name,
|
|
318
|
+
raw_ctype=ctype,
|
|
319
|
+
base_type=base,
|
|
320
|
+
is_pointer=is_pointer,
|
|
321
|
+
is_const=is_const,
|
|
322
|
+
)
|
|
323
|
+
argspecs.append(aspec)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# classify
|
|
327
|
+
for a in argspecs:
|
|
328
|
+
if (not a.is_pointer) and _is_length_name(a.name) and a.base_type in (
|
|
329
|
+
"int",
|
|
330
|
+
"unsigned int",
|
|
331
|
+
"unsigned",
|
|
332
|
+
):
|
|
333
|
+
a.is_length_param = True
|
|
334
|
+
|
|
335
|
+
for a in argspecs:
|
|
336
|
+
if a.is_pointer:
|
|
337
|
+
if a.is_const:
|
|
338
|
+
a.is_array_in = True
|
|
339
|
+
else:
|
|
340
|
+
a.is_array_out = True
|
|
341
|
+
else:
|
|
342
|
+
if not a.is_length_param:
|
|
343
|
+
a.is_scalar = True
|
|
344
|
+
|
|
345
|
+
funcs[fname] = FuncSpec(name=fname, return_ctype=ret_ctype, args=argspecs)
|
|
346
|
+
|
|
347
|
+
return funcs
|
|
348
|
+
|
|
349
|
+
# ---------- 3) attach docstrings from C comments ------------------------
|
|
350
|
+
|
|
351
|
+
def _attach_docs_from_source(self):
|
|
352
|
+
try:
|
|
353
|
+
src = self.c_path.read_text(encoding="utf8")
|
|
354
|
+
except OSError:
|
|
355
|
+
return
|
|
356
|
+
|
|
357
|
+
for fname, fspec in self._func_specs.items():
|
|
358
|
+
# we look for: name ( ... ) /* ... */
|
|
359
|
+
pattern = rf"{re.escape(fname)}\s*\([^{{;]*\)\s*/\*(.*?)\*/"
|
|
360
|
+
m = re.search(pattern, src, flags=re.DOTALL)
|
|
361
|
+
if not m:
|
|
362
|
+
continue
|
|
363
|
+
doc = m.group(1).strip()
|
|
364
|
+
doc = re.sub(r"\s+\n", "\n", doc)
|
|
365
|
+
doc = re.sub(r"\n\s+", "\n", doc)
|
|
366
|
+
fspec.doc = doc
|
|
367
|
+
|
|
368
|
+
# ---------- 4) create NumPy wrappers ------------------------------------
|
|
369
|
+
|
|
370
|
+
def _create_wrappers(self):
|
|
371
|
+
for fname, fspec in self._func_specs.items():
|
|
372
|
+
if hasattr(self, fname):
|
|
373
|
+
continue
|
|
374
|
+
try:
|
|
375
|
+
wrapper = self._make_wrapper_from_spec(fspec)
|
|
376
|
+
except NotImplementedError:
|
|
377
|
+
# too complex for our heuristics, skip
|
|
378
|
+
continue
|
|
379
|
+
setattr(self, fname, wrapper)
|
|
380
|
+
|
|
381
|
+
def _make_wrapper_from_spec(self, fspec: FuncSpec):
|
|
382
|
+
array_args = [a for a in fspec.args if a.is_array_in or a.is_array_out]
|
|
383
|
+
scalar_args = [a for a in fspec.args if a.is_scalar]
|
|
384
|
+
length_args = [a for a in fspec.args if a.is_length_param]
|
|
385
|
+
|
|
386
|
+
if len(length_args) > 1:
|
|
387
|
+
raise NotImplementedError(
|
|
388
|
+
f"{fspec.name}: multiple length params not supported yet"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# validate array types
|
|
392
|
+
for a in array_args:
|
|
393
|
+
_numpy_dtype_for_base_type(a.base_type)
|
|
394
|
+
|
|
395
|
+
def wrapper(*args):
|
|
396
|
+
self.ensure_compiled()
|
|
397
|
+
lib = self.lib
|
|
398
|
+
ffi = self.ffi
|
|
399
|
+
cfun = getattr(lib, fspec.name)
|
|
400
|
+
|
|
401
|
+
out_array_specs = [
|
|
402
|
+
a for a in array_args if a.is_array_out and a.name.lower().startswith("out")
|
|
403
|
+
]
|
|
404
|
+
in_array_specs = [a for a in array_args if a not in out_array_specs]
|
|
405
|
+
|
|
406
|
+
if len(args) != len(in_array_specs) + len(scalar_args):
|
|
407
|
+
raise TypeError(
|
|
408
|
+
f"{fspec.name} expects {len(in_array_specs)} array args and "
|
|
409
|
+
f"{len(scalar_args)} scalar args, got {len(args)}"
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
py_array_vals = args[: len(in_array_specs)]
|
|
413
|
+
py_scalar_vals = args[len(in_array_specs) :]
|
|
414
|
+
|
|
415
|
+
# prepare arrays
|
|
416
|
+
c_array_ptrs = []
|
|
417
|
+
lengths = []
|
|
418
|
+
output_arrays: list[np.ndarray] = []
|
|
419
|
+
|
|
420
|
+
# user-provided (input) arrays
|
|
421
|
+
for a, val in zip(in_array_specs, py_array_vals):
|
|
422
|
+
base_dtype = _numpy_dtype_for_base_type(a.base_type)
|
|
423
|
+
arr = np.ascontiguousarray(val, dtype=base_dtype)
|
|
424
|
+
lengths.append(arr.size)
|
|
425
|
+
ctype = f"{'const ' if a.is_const else ''}{a.base_type} *"
|
|
426
|
+
ptr = ffi.cast(ctype, ffi.from_buffer(arr))
|
|
427
|
+
c_array_ptrs.append((a, arr, ptr))
|
|
428
|
+
|
|
429
|
+
# auto-created output arrays
|
|
430
|
+
for a in out_array_specs:
|
|
431
|
+
base_dtype = _numpy_dtype_for_base_type(a.base_type)
|
|
432
|
+
ref_arr = None
|
|
433
|
+
if a.name.lower().startswith("out_"):
|
|
434
|
+
ref_name = a.name[4:]
|
|
435
|
+
for (aa, arr, _ptr) in c_array_ptrs:
|
|
436
|
+
if aa.name == ref_name:
|
|
437
|
+
ref_arr = arr
|
|
438
|
+
break
|
|
439
|
+
if ref_arr is not None:
|
|
440
|
+
arr = np.empty_like(ref_arr, dtype=base_dtype)
|
|
441
|
+
else:
|
|
442
|
+
target_len = lengths[0] if lengths else None
|
|
443
|
+
if target_len is None:
|
|
444
|
+
raise ValueError(
|
|
445
|
+
f"{fspec.name}: cannot determine length for output array {a.name}"
|
|
446
|
+
)
|
|
447
|
+
arr = np.empty(target_len, dtype=base_dtype)
|
|
448
|
+
lengths.append(arr.size)
|
|
449
|
+
ctype = f"{a.base_type} *"
|
|
450
|
+
ptr = ffi.cast(ctype, ffi.from_buffer(arr))
|
|
451
|
+
c_array_ptrs.append((a, arr, ptr))
|
|
452
|
+
output_arrays.append(arr)
|
|
453
|
+
|
|
454
|
+
if lengths:
|
|
455
|
+
n0 = lengths[0]
|
|
456
|
+
for ln in lengths[1:]:
|
|
457
|
+
if ln != n0:
|
|
458
|
+
raise ValueError("Array arguments must have same length")
|
|
459
|
+
inferred_len = n0
|
|
460
|
+
else:
|
|
461
|
+
inferred_len = None
|
|
462
|
+
|
|
463
|
+
scalar_vals_iter = iter(py_scalar_vals)
|
|
464
|
+
c_args = []
|
|
465
|
+
|
|
466
|
+
for a in fspec.args:
|
|
467
|
+
if a.is_array_in or a.is_array_out:
|
|
468
|
+
for (aa, arr, ptr) in c_array_ptrs:
|
|
469
|
+
if aa is a:
|
|
470
|
+
c_args.append(ptr)
|
|
471
|
+
break
|
|
472
|
+
else:
|
|
473
|
+
raise RuntimeError("Internal error mapping array arg")
|
|
474
|
+
elif a.is_length_param:
|
|
475
|
+
if inferred_len is None:
|
|
476
|
+
raise ValueError(
|
|
477
|
+
f"{fspec.name}: cannot infer length for {a.name}"
|
|
478
|
+
)
|
|
479
|
+
c_args.append(inferred_len)
|
|
480
|
+
elif a.is_scalar:
|
|
481
|
+
try:
|
|
482
|
+
sv = next(scalar_vals_iter)
|
|
483
|
+
except StopIteration:
|
|
484
|
+
raise TypeError("Not enough scalar arguments")
|
|
485
|
+
c_args.append(sv)
|
|
486
|
+
else:
|
|
487
|
+
raise RuntimeError("Arg classification inconsistent")
|
|
488
|
+
|
|
489
|
+
res = cfun(*c_args)
|
|
490
|
+
|
|
491
|
+
if output_arrays:
|
|
492
|
+
outputs = output_arrays[0] if len(output_arrays) == 1 else tuple(output_arrays)
|
|
493
|
+
if fspec.return_ctype.strip() == "void":
|
|
494
|
+
return outputs
|
|
495
|
+
return outputs, res
|
|
496
|
+
|
|
497
|
+
if fspec.return_ctype.strip() == "void":
|
|
498
|
+
return None
|
|
499
|
+
else:
|
|
500
|
+
return res
|
|
501
|
+
|
|
502
|
+
wrapper.__name__ = fspec.name
|
|
503
|
+
doc_lines = [f"Auto-wrapped C function `{fspec.name}`."]
|
|
504
|
+
if fspec.doc:
|
|
505
|
+
doc_lines.append(fspec.doc)
|
|
506
|
+
wrapper.__doc__ = "\n".join(doc_lines)
|
|
507
|
+
return wrapper
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinycwrap
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Lightweight C-to-Python wrapper generator using CFFI and NumPy
|
|
5
|
+
Author: TinyCWrap Contributors
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy>=1.24
|
|
9
|
+
Requires-Dist: cffi>=1.15
|
|
10
|
+
|
|
11
|
+
# tinycwrap
|
|
12
|
+
|
|
13
|
+
TinyCWrap provides a small helper (`CModule`) to compile C sources with CFFI, auto-generate `cdef`s, and expose NumPy-friendly Python wrappers.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
tinycwrap/__init__.py,sha256=1mwJcIAulfsBoVSUQXRz_zz4b3Dh2yhKGmvchJBUhjA,230
|
|
2
|
+
tinycwrap/cmodule.py,sha256=4_PAFuxbJbdbFpETjGeRHLqWAyGuCS9yRsMjcWg9LyI,16972
|
|
3
|
+
tinycwrap-0.0.0.dist-info/METADATA,sha256=lAf9f80uJKGtgxVlYPVpONlae8ul0Oj_Y-TqANPfcsk,430
|
|
4
|
+
tinycwrap-0.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
+
tinycwrap-0.0.0.dist-info/top_level.txt,sha256=p2bqckqsfD7QKVnH2eC3rt73_QvPwLmtqigrH5XSvbA,10
|
|
6
|
+
tinycwrap-0.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tinycwrap
|