tinycwrap 0.0.5__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.5
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.5"
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.5"
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."""
@@ -428,13 +494,14 @@ class CModule:
428
494
  "size_t",
429
495
  "ssize_t",
430
496
  )
431
- and (not arg.is_pointer)
432
497
  and arg.name in referenced
433
498
  ):
434
499
  arg.is_length_param = True
435
500
  for arg in fspec.args:
436
501
  if arg.is_length_param:
437
502
  arg.is_scalar = False
503
+ arg.is_array_in = False
504
+ arg.is_array_out = False
438
505
  elif (
439
506
  (not arg.is_pointer)
440
507
  and arg.array_len is None
@@ -639,6 +706,7 @@ class CModule:
639
706
  "_self": self,
640
707
  "np": np,
641
708
  "numpy_dtype_for_base_type": numpy_dtype_for_base_type,
709
+ "base_type_from_ctype": base_type_from_ctype,
642
710
  "_struct_classes": self._struct_classes,
643
711
  "_struct_dtypes": self._struct_dtypes,
644
712
  }
@@ -664,7 +732,6 @@ class CModule:
664
732
  Build the Python source string for a wrapper with an explicit signature.
665
733
  Keeping this separate allows inspection/debugging of the generated code.
666
734
  """
667
- src_parts: list[str] = []
668
735
  func_text = self._function_source(fspec.name)
669
736
  if func_text:
670
737
  c_source_text = func_text
@@ -753,6 +820,7 @@ class CModule:
753
820
  else:
754
821
  contract_map[target] = expr
755
822
 
823
+ length_pointer_names = {a.name for a in fspec.args if a.is_length_param and a.is_pointer}
756
824
  pointer_scalar_names = {
757
825
  a.name
758
826
  for a in fspec.args
@@ -773,7 +841,7 @@ class CModule:
773
841
  "ssize_t",
774
842
  )
775
843
  )
776
- }
844
+ } | length_pointer_names
777
845
 
778
846
  func_names = set(self._func_specs.keys())
779
847
 
@@ -782,6 +850,7 @@ class CModule:
782
850
  expr = re.sub(r"len\((\w+)\)", r"len(arr_\1)", expr)
783
851
  for name in pointer_scalar_names:
784
852
  expr = re.sub(rf"\b{name}\b", f"int(arr_{name}.ravel()[0])", expr)
853
+ expr = re.sub(rf"\b{name}\b", f"scalar_{name}", expr)
785
854
  if func_names:
786
855
  pattern = r"\b(" + "|".join(re.escape(n) for n in func_names) + r")\s*\(([^()]*)\)"
787
856
 
@@ -809,6 +878,17 @@ class CModule:
809
878
  scalar_lines: list[str] = []
810
879
 
811
880
  for a in fspec.args:
881
+ if a.is_length_param and a.is_pointer:
882
+ base_dtype = "np.dtype('int32')"
883
+ out_lines = [
884
+ f" arr_{a.name} = np.zeros((), dtype={base_dtype})",
885
+ f" ptr_{a.name} = _self._ffi.cast('{a.base_type} *', _self._ffi.from_buffer(arr_{a.name}))",
886
+ ]
887
+ call_args.append(f"ptr_{a.name}")
888
+ output_vars.append(f"arr_{a.name}")
889
+ output_names.append(a.name)
890
+ pointer_scalar_outputs.append((a.name, f"arr_{a.name}"))
891
+ continue
812
892
  if a.is_array_in:
813
893
  const_prefix = "const " if a.is_const else ""
814
894
  if a.base_type in struct_names:
@@ -840,21 +920,25 @@ class CModule:
840
920
  scalar_lines.append(f" {a.name} = {a.name}")
841
921
  call_args.append(a.name)
842
922
  elif a.is_length_param:
843
- if a.name in contract_map:
844
- expr_py = _expr_py(contract_map[a.name])
845
- length_lines += [
846
- f" if {a.name} is None:",
847
- f" {a.name} = int({expr_py})",
848
- f" else:",
849
- f" {a.name} = int({a.name})",
850
- ]
923
+ if a.is_pointer:
924
+ # handled in array/pointer branch
925
+ call_args.append(None)
851
926
  else:
852
- length_lines += [
853
- f" if {a.name} is None:",
854
- f" raise ValueError('{fspec.name}: length parameter {a.name} requires an explicit Contract')",
855
- f" {a.name} = int({a.name})",
856
- ]
857
- call_args.append(a.name)
927
+ if a.name in contract_map:
928
+ expr_py = _expr_py(contract_map[a.name])
929
+ length_lines += [
930
+ f" if {a.name} is None:",
931
+ f" {a.name} = int({expr_py})",
932
+ f" else:",
933
+ f" {a.name} = int({a.name})",
934
+ ]
935
+ else:
936
+ length_lines += [
937
+ f" if {a.name} is None:",
938
+ f" raise ValueError('{fspec.name}: length parameter {a.name} requires an explicit Contract')",
939
+ f" {a.name} = int({a.name})",
940
+ ]
941
+ call_args.append(a.name)
858
942
  else:
859
943
  # defer outputs / pointer handling
860
944
  call_args.append(None) # placeholder
@@ -955,15 +1039,14 @@ class CModule:
955
1039
 
956
1040
  ret_type = fspec.return_ctype.strip()
957
1041
  call_expr = f"cfun({', '.join(arg_call_args)})"
958
- if ret_type == "void":
959
- 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}")
960
1045
  else:
961
- lines.append(f" res = {call_expr}")
962
-
963
- for out_name, out_var in zip(output_names, output_vars):
964
- if out_name in post_contract_map:
965
- expr_py = _expr_py(post_contract_map[out_name])
966
- lines.append(f" {out_var} = {out_var}[:int({expr_py})]")
1046
+ if ret_type == "void":
1047
+ lines.append(f" {call_expr}")
1048
+ else:
1049
+ lines.append(f" res = {call_expr}")
967
1050
 
968
1051
  for name, arr_var in pointer_scalar_outputs:
969
1052
  lines.append(f" scalar_{name} = int(np.asarray({arr_var}).ravel()[0])")
@@ -972,14 +1055,73 @@ class CModule:
972
1055
  for name, arr_var, cls_expr in struct_scalar_outputs:
973
1056
  lines.append(f" obj_{name} = {cls_expr}()")
974
1057
  lines.append(f" object.__setattr__(obj_{name}, '_data', np.array({arr_var}, copy=True))")
1058
+ pointer_scalar_map = {arr: (name, f"scalar_{name}") for name, arr in pointer_scalar_outputs}
1059
+ pairs = list(zip(output_names, output_vars))
1060
+ # place array outputs before scalar pointer lengths
1061
+ pairs_sorted = sorted(pairs, key=lambda p: 1 if p[1] in pointer_scalar_map else 0)
1062
+ output_names_reordered: list[str] = []
975
1063
  output_vars_final: list[str] = []
976
- for ov in output_vars:
1064
+ for name, ov in pairs_sorted:
1065
+ output_names_reordered.append(name)
977
1066
  if ov in struct_scalar_map:
978
1067
  n, _ = struct_scalar_map[ov]
979
1068
  output_vars_final.append(f"obj_{n}")
1069
+ elif ov in pointer_scalar_map:
1070
+ output_vars_final.append(pointer_scalar_map[ov][1])
980
1071
  else:
981
1072
  output_vars_final.append(ov)
982
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
+
983
1125
  if output_vars_final:
984
1126
  if ret_type == "void":
985
1127
  if len(output_vars_final) == 1:
@@ -991,8 +1133,20 @@ class CModule:
991
1133
  ret_expr = f"{output_vars_final[0]}, res"
992
1134
  else:
993
1135
  ret_expr = "(" + ", ".join(output_vars_final) + "), res"
1136
+ # apply post-contract slicing using scalar lengths if available
1137
+ for out_name, out_var in zip(output_names_reordered, output_vars_final):
1138
+ if out_name in post_contract_map:
1139
+ expr_py = _expr_py(post_contract_map[out_name])
1140
+ scalar_names = {n for n, _ in pointer_scalar_outputs}
1141
+ if out_name in scalar_names:
1142
+ expr_py = expr_py.replace(out_name, f"scalar_{out_name}")
1143
+ ret_expr = ret_expr.replace(out_var, f"({out_var})[:int({expr_py})]")
994
1144
  if pointer_scalar_outputs:
995
- scalars = [f"scalar_{n}" for n, _ in pointer_scalar_outputs]
1145
+ scalars = [
1146
+ f"scalar_{n}"
1147
+ for n, _ in pointer_scalar_outputs
1148
+ if f"scalar_{n}" not in output_vars_final
1149
+ ]
996
1150
  if isinstance(ret_expr, str) and ret_expr.startswith("("):
997
1151
  ret_expr = (
998
1152
  ret_expr[:-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.5
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