tinycwrap 0.0.6__tar.gz → 0.0.7__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinycwrap
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Lightweight C-to-Python wrapper generator using CFFI and NumPy
5
5
  Author: TinyCWrap Contributors
6
6
  Requires-Python: >=3.9
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinycwrap"
7
- version = "0.0.6"
7
+ version = "0.0.7"
8
8
  description = "Lightweight C-to-Python wrapper generator using CFFI and NumPy"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -88,6 +88,13 @@ def test_merge_sorted(cm):
88
88
  assert out_len_val == 4
89
89
 
90
90
 
91
+ def test_owned_array(cm):
92
+ arr, n = cm.alloc_random_array()
93
+ assert arr.shape[0] == n
94
+ assert n >= 3
95
+ assert np.all(arr[:n] >= 1.0)
96
+
97
+
91
98
  def test_struct_output_array(cm):
92
99
  n = 4
93
100
  particles = cm.Particle.zeros(n)
@@ -6,6 +6,6 @@ from .cmodule import CModule
6
6
  try:
7
7
  __version__ = version("tinycwrap")
8
8
  except PackageNotFoundError:
9
- __version__ = "0.0.6"
9
+ __version__ = "0.0.7"
10
10
 
11
11
  __all__ = ["CModule", "__version__"]
@@ -11,6 +11,7 @@ from cffi import FFI
11
11
  from .parsing import (
12
12
  FuncSpec,
13
13
  StructSpec,
14
+ base_type_from_ctype,
14
15
  numpy_dtype_for_base_type,
15
16
  parse_functions_from_cdef,
16
17
  parse_structs_from_cdef,
@@ -163,6 +164,8 @@ class CModule:
163
164
 
164
165
  # Re-parse cdef and attach docs from C source, then create wrappers
165
166
  self._func_specs = parse_functions_from_cdef(self._cdef)
167
+ # remove helper prototypes we add for ownership
168
+ self._func_specs.pop("free", None)
166
169
  self._struct_specs = parse_structs_from_cdef(self._cdef)
167
170
  self._attach_docs_from_source()
168
171
  self._mark_length_params_from_contracts()
@@ -287,6 +290,21 @@ class CModule:
287
290
  src_wo_pp = re.sub(r"__declspec\s*\([^)]+\)", "", src_wo_pp)
288
291
  src_wo_pp = re.sub(r"__asm__\s*\([^)]*\)", "", src_wo_pp)
289
292
 
293
+ # Keep a version with only top-level text (brace depth 0) to avoid
294
+ # picking up statements inside function bodies.
295
+ top_level_chars = []
296
+ depth = 0
297
+ for ch in src_wo_pp:
298
+ if ch == "{":
299
+ depth += 1
300
+ if depth == 0:
301
+ top_level_chars.append(ch)
302
+ if ch == "}" and depth > 0:
303
+ depth -= 1
304
+ if depth == 0:
305
+ top_level_chars.append("\n")
306
+ top_level_src = "".join(top_level_chars)
307
+
290
308
  # regex for function definitions
291
309
  func_def_re = re.compile(
292
310
  r"""
@@ -311,11 +329,15 @@ class CModule:
311
329
  r"typedef\s+struct\s+(?:\w+\s*)?{(?P<body>[^}]*)}\s*(?P<name>\w+)\s*;",
312
330
  re.DOTALL,
313
331
  )
332
+ seen_structs = set()
314
333
  for m in struct_re.finditer(src_wo_pp):
315
- struct_text = m.group(0)
316
- struct_defs.append(struct_text.strip())
334
+ struct_text = m.group(0).strip()
335
+ if struct_text not in seen_structs:
336
+ struct_defs.append(struct_text)
337
+ seen_structs.add(struct_text)
317
338
 
318
339
  proto_set = set()
340
+ proto_name_set = set()
319
341
  for m in func_def_re.finditer(src_wo_pp):
320
342
  if m.group("prefix") and "static" in m.group("prefix"):
321
343
  # ignore static functions, not exported
@@ -339,12 +361,46 @@ class CModule:
339
361
  continue
340
362
  args = " ".join(strip_restrict_keywords(m.group("args")).split())
341
363
  proto = f"{ret} {name}({args});"
342
- if proto not in proto_set:
364
+ if name not in proto_name_set:
365
+ prototypes.append(proto)
366
+ proto_set.add(proto)
367
+ proto_name_set.add(name)
368
+
369
+ # also pick up function declarations (without body), e.g., from headers
370
+ decl_re = re.compile(r"([A-Za-z_][A-Za-z0-9_\s\*]*?)\s+([A-Za-z_]\w*)\s*\(([^)]*)\)\s*;")
371
+ for m in decl_re.finditer(top_level_src):
372
+ if "return" in m.group(0) or "=" in m.group(0):
373
+ continue
374
+ ret = " ".join(strip_restrict_keywords(m.group(1)).split())
375
+ if "static" in ret.split():
376
+ continue
377
+ name = m.group(2)
378
+ args = " ".join(strip_restrict_keywords(m.group(3)).split())
379
+ proto = f"{ret} {name}({args});"
380
+ if name not in proto_name_set:
343
381
  prototypes.append(proto)
344
382
  proto_set.add(proto)
383
+ proto_name_set.add(name)
345
384
 
346
385
  if not prototypes and not struct_defs:
347
386
  raise RuntimeError(f"No functions found in {self._c_path} to generate cdef")
387
+ # fallback: if we missed pointer-returning prototypes present as lines
388
+ type_prefix_re = re.compile(r"^(void|int|float|double|long|unsigned|signed|struct|char)\b")
389
+ for line in top_level_src.splitlines():
390
+ line_clean = line.strip()
391
+ if not line_clean or line_clean.startswith("typedef") or ":" in line_clean:
392
+ continue
393
+ if not type_prefix_re.match(line_clean):
394
+ continue
395
+ if "=" in line_clean:
396
+ continue
397
+ if "(" in line_clean and ")" in line_clean and line_clean.endswith(";"):
398
+ name_match = re.match(r"[A-Za-z_][A-Za-z0-9_\s\*]*?\s+([A-Za-z_]\w*)\s*\(", line_clean)
399
+ fname = name_match.group(1) if name_match else line_clean
400
+ if fname not in proto_name_set:
401
+ prototypes.append(line_clean)
402
+ proto_set.add(line_clean)
403
+ proto_name_set.add(fname)
348
404
 
349
405
  cdef_parts = []
350
406
  if struct_defs:
@@ -357,6 +413,8 @@ class CModule:
357
413
  continue
358
414
  cdef_lines.append(line)
359
415
  cdef = "\n".join(cdef_lines)
416
+ if "free(" not in cdef:
417
+ cdef += "\nvoid free(void *);"
360
418
  show = self._verbose if verbose is None else verbose
361
419
  if show:
362
420
  print("[CModule] Auto-generated cdef:\n" + cdef)
@@ -380,9 +438,16 @@ class CModule:
380
438
  doc = re.sub(r"\n\s+", "\n", doc)
381
439
  fspec.doc = doc
382
440
  contracts = []
441
+ owns: list[str] = []
383
442
  for line in doc.splitlines():
384
443
  m_contract = re.search(r"(post-)?contract:\s*(.*)", line, flags=re.IGNORECASE)
385
444
  if not m_contract:
445
+ m_own = re.search(r"own:\s*(.*)", line, flags=re.IGNORECASE)
446
+ if m_own:
447
+ for name in m_own.group(1).split(","):
448
+ n = name.strip()
449
+ if n:
450
+ owns.append(n)
386
451
  continue
387
452
  is_post = m_contract.group(1) is not None
388
453
  after = m_contract.group(2)
@@ -398,6 +463,7 @@ class CModule:
398
463
  expr = mlen.group(2).strip()
399
464
  contracts.append((target, expr, is_post))
400
465
  fspec.contracts = contracts or None
466
+ fspec.owns = owns or None
401
467
 
402
468
  def _mark_length_params_from_contracts(self):
403
469
  """Mark length-like parameters based solely on explicit contracts."""
@@ -640,6 +706,7 @@ class CModule:
640
706
  "_self": self,
641
707
  "np": np,
642
708
  "numpy_dtype_for_base_type": numpy_dtype_for_base_type,
709
+ "base_type_from_ctype": base_type_from_ctype,
643
710
  "_struct_classes": self._struct_classes,
644
711
  "_struct_dtypes": self._struct_dtypes,
645
712
  }
@@ -665,7 +732,6 @@ class CModule:
665
732
  Build the Python source string for a wrapper with an explicit signature.
666
733
  Keeping this separate allows inspection/debugging of the generated code.
667
734
  """
668
- src_parts: list[str] = []
669
735
  func_text = self._function_source(fspec.name)
670
736
  if func_text:
671
737
  c_source_text = func_text
@@ -973,10 +1039,14 @@ class CModule:
973
1039
 
974
1040
  ret_type = fspec.return_ctype.strip()
975
1041
  call_expr = f"cfun({', '.join(arg_call_args)})"
976
- if ret_type == "void":
977
- lines.append(f" {call_expr}")
1042
+ own_return = fspec.owns and "return" in fspec.owns and fspec.return_ctype.strip().endswith("*")
1043
+ if own_return:
1044
+ lines.append(f" res_ptr = {call_expr}")
978
1045
  else:
979
- lines.append(f" res = {call_expr}")
1046
+ if ret_type == "void":
1047
+ lines.append(f" {call_expr}")
1048
+ else:
1049
+ lines.append(f" res = {call_expr}")
980
1050
 
981
1051
  for name, arr_var in pointer_scalar_outputs:
982
1052
  lines.append(f" scalar_{name} = int(np.asarray({arr_var}).ravel()[0])")
@@ -1001,6 +1071,57 @@ class CModule:
1001
1071
  else:
1002
1072
  output_vars_final.append(ov)
1003
1073
 
1074
+ own_return_len_expr = None
1075
+ if own_return and contract_map.get("return"):
1076
+ own_return_len_expr = _expr_py(contract_map["return"])
1077
+
1078
+ if own_return:
1079
+ base_ret = base_type_from_ctype(fspec.return_ctype)
1080
+ ret_dtype_expr = f"np.dtype('{np.dtype(numpy_dtype_for_base_type(base_ret)).name}')"
1081
+ lines.append(" lib_free = None")
1082
+ lines.append(" try:")
1083
+ lines.append(" lib_free = _self._lib.free")
1084
+ lines.append(" except AttributeError:")
1085
+ lines.append(" pass")
1086
+ lines.append(" libc_free = None")
1087
+ lines.append(" for _cand in (None, 'libc.so.6', 'libc.so', 'libc.dylib'):")
1088
+ lines.append(" if libc_free is not None:")
1089
+ lines.append(" break")
1090
+ lines.append(" try:")
1091
+ lines.append(" _libc = _self._ffi.dlopen(_cand)")
1092
+ lines.append(" libc_free = getattr(_libc, 'free', None)")
1093
+ lines.append(" except OSError:")
1094
+ lines.append(" continue")
1095
+ lines.append(" free_fn = lib_free or libc_free")
1096
+ lines.append(f" if free_fn is None: raise RuntimeError('{fspec.name}: cannot locate free() for owned return')")
1097
+ if own_return_len_expr is None:
1098
+ lines.append(f" raise ValueError('{fspec.name}: Own return requires len(return)=... Contract')")
1099
+ else:
1100
+ for name, arr_var in pointer_scalar_outputs:
1101
+ if f"scalar_{name}" not in [v for v in output_vars_final]:
1102
+ lines.append(f" scalar_{name} = int(np.asarray({arr_var}).ravel()[0])")
1103
+ # allow len(return)=out_len style expressions that use pointer-scalar values
1104
+ lines.append(f" res_buf = _self._ffi.gc(res_ptr, free_fn)")
1105
+ lines.append(
1106
+ f" arr_ret = np.frombuffer(_self._ffi.buffer(res_buf, int({own_return_len_expr}) * np.dtype({ret_dtype_expr}).itemsize), dtype={ret_dtype_expr})"
1107
+ )
1108
+ ret_parts: list[str] = ["arr_ret"]
1109
+ if output_vars_final:
1110
+ for v in output_vars_final:
1111
+ if v not in ret_parts:
1112
+ ret_parts.append(v)
1113
+ # append any scalar lengths not already in ret_parts
1114
+ for name, _arr in pointer_scalar_outputs:
1115
+ sval = f"scalar_{name}"
1116
+ if sval not in ret_parts:
1117
+ ret_parts.append(sval)
1118
+ if len(ret_parts) == 1:
1119
+ ret_expr = ret_parts[0]
1120
+ else:
1121
+ ret_expr = "(" + ", ".join(ret_parts) + ")"
1122
+ lines.append(f" return {ret_expr}")
1123
+ return "\n".join(lines)
1124
+
1004
1125
  if output_vars_final:
1005
1126
  if ret_type == "void":
1006
1127
  if len(output_vars_final) == 1:
@@ -72,6 +72,7 @@ class FuncSpec:
72
72
  args: list # list[ArgSpec]
73
73
  doc: str | None = None
74
74
  contracts: list[tuple[str, str, bool]] | None = None
75
+ owns: list[str] | None = None
75
76
 
76
77
 
77
78
  @dataclass
@@ -151,7 +152,9 @@ def _parse_functions_with_pycparser(cdef: str) -> dict[str, FuncSpec]:
151
152
  for ext in ast.ext:
152
153
  if isinstance(ext, c_ast.Decl) and isinstance(ext.type, c_ast.FuncDecl):
153
154
  fname = ext.name
154
- ret_ctype, _, _, _ = _ctype_from_decl(ext.type.type)
155
+ ret_ctype, ret_is_ptr, _, _ = _ctype_from_decl(ext.type.type)
156
+ if ret_is_ptr:
157
+ ret_ctype = ret_ctype + " *"
155
158
  argspecs: list[ArgSpec] = []
156
159
  args = ext.type.args
157
160
  if args and args.params:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinycwrap
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: Lightweight C-to-Python wrapper generator using CFFI and NumPy
5
5
  Author: TinyCWrap Contributors
6
6
  Requires-Python: >=3.9
File without changes
File without changes
File without changes
File without changes