tinycwrap 0.1.1__tar.gz → 0.1.2__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.
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/PKG-INFO +16 -9
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/README.md +15 -8
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/pyproject.toml +1 -1
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tests/test_kernels.py +11 -4
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tinycwrap/__init__.py +1 -1
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tinycwrap/cmodule.py +97 -9
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tinycwrap/parsing.py +1 -0
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tinycwrap.egg-info/PKG-INFO +16 -9
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/setup.cfg +0 -0
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tests/test_geom.py +0 -0
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tests/test_path.py +0 -0
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tinycwrap.egg-info/SOURCES.txt +0 -0
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tinycwrap.egg-info/dependency_links.txt +0 -0
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tinycwrap.egg-info/requires.txt +0 -0
- {tinycwrap-0.1.1 → tinycwrap-0.1.2}/tinycwrap.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinycwrap
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Lightweight C-to-Python wrapper generator using CFFI and NumPy
|
|
5
5
|
Author: TinyCWrap Contributors
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -28,9 +28,7 @@ Write a small C file (only non-`static` functions are exported):
|
|
|
28
28
|
```c
|
|
29
29
|
/* kernels.c */
|
|
30
30
|
double dot(const double *x, const double *y, int len_x)
|
|
31
|
-
/* Return dot product between x and y
|
|
32
|
-
Contract: len_x=len(x);
|
|
33
|
-
*/
|
|
31
|
+
/* Return dot product between x and y */
|
|
34
32
|
{
|
|
35
33
|
double acc = 0.0;
|
|
36
34
|
for (int i = 0; i < len_x; ++i)
|
|
@@ -50,7 +48,7 @@ cm = CModule("kernels.c") # builds the shared library, creates wrappers
|
|
|
50
48
|
x = np.arange(5, dtype=np.float64)
|
|
51
49
|
y = np.ones_like(x)
|
|
52
50
|
|
|
53
|
-
cm.dot(x, y) # -> 10.0, len_x auto-filled by
|
|
51
|
+
cm.dot(x, y) # -> 10.0, len_x auto-filled by convention
|
|
54
52
|
print(cm.dot.__doc__) # docstring comes from the C comment
|
|
55
53
|
```
|
|
56
54
|
|
|
@@ -65,7 +63,7 @@ TinyCWrap infers how to build wrappers from simple C conventions:
|
|
|
65
63
|
- `T *arg` -> output/in-place array (prefer naming it `out_*` when possible).
|
|
66
64
|
- `T arg[N]` in the signature -> fixed-size array (input if `const`, otherwise output).
|
|
67
65
|
- Plain scalars (`double`, `int`, ...) -> Python scalars.
|
|
68
|
-
- **Length parameters**: integers
|
|
66
|
+
- **Length parameters**: integers named like `len_x`, `n_x`, or `size_x` are auto-filled from array `x` by convention. Otherwise pass them explicitly from Python or declare a contract (see below).
|
|
69
67
|
- **Docstrings**: the block comment immediately after the function header becomes the Python docstring.
|
|
70
68
|
- **Structs**: `typedef struct { ... } Name;` definitions in your headers/sources become Python classes with a `.dtype` and NumPy-backed storage.
|
|
71
69
|
- **Compilation**: extra sources can be passed (`CModule("main.c", "helper.c")`), and extra include dirs via `include_dirs=[...]`. Default compiler flags are `-O3 -shared -fPIC -march=native -mtune=native` plus the NumPy include path.
|
|
@@ -79,6 +77,15 @@ Contracts are declared inside the doc comment with `Contract:` (or `Contracts:`)
|
|
|
79
77
|
- `len(out)=len_x` — allocate a 1D output array.
|
|
80
78
|
- `postlen(out)=out_len` — slice the output after the call using an integer pointer result `out_len` (useful when the C code writes less than the allocated length).
|
|
81
79
|
- `own(return)` — the returned pointer is owned by the caller; the wrapper will `free` it after copying to NumPy (requires a `len(return)=...` contract).
|
|
80
|
+
- `expose(len_x)` — opt out of naming-convention inference for `len_x`, keeping it as a required Python argument.
|
|
81
|
+
|
|
82
|
+
TinyCWrap also infers low-risk contracts from names:
|
|
83
|
+
|
|
84
|
+
- `len_x`, `n_x`, or `size_x` are treated as `len(x)` when `x` is an array argument.
|
|
85
|
+
- `out_x` is allocated with `shape(x)` when input array `x` exists.
|
|
86
|
+
- `out_x` is allocated with `len_x`, `n_x`, or `size_x` when no input `x` exists but such a length argument does.
|
|
87
|
+
|
|
88
|
+
Explicit contracts override inferred contracts.
|
|
82
89
|
|
|
83
90
|
Examples pulled from the test suite:
|
|
84
91
|
|
|
@@ -109,9 +116,9 @@ Rules are parsed case-insensitively; whitespace does not matter.
|
|
|
109
116
|
|
|
110
117
|
## Python wrapper behavior
|
|
111
118
|
|
|
112
|
-
- **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
|
|
119
|
+
- **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, naming conventions, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
|
|
113
120
|
- **Struct pointer outputs**: non-const struct pointers are treated as in/out; when you pass an object/array explicitly, it is mutated in place and not returned. When you pass `None`, TinyCWrap allocates and returns the struct (or struct array).
|
|
114
|
-
- **Optional length arguments**: integer length parameters inferred from contracts default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the expression
|
|
121
|
+
- **Optional length arguments**: integer length parameters inferred from contracts or naming conventions default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the inferred expression is evaluated.
|
|
115
122
|
- **Post contracts**: when a contract uses `postlen(...)` or `post shape(...)`, the wrapper slices/reshapes outputs after the C call using the values written by the C function.
|
|
116
123
|
- **Scalar pointer outputs**: pointers to integer-like types (e.g., `int *out_len`) are returned as plain Python integers alongside other outputs.
|
|
117
124
|
- **Structs**: for `typedef struct` declarations TinyCWrap generates a Python class:
|
|
@@ -174,7 +181,7 @@ arr, n = cm.alloc_random_array() # returns NumPy array, frees the C buffer
|
|
|
174
181
|
## Tips
|
|
175
182
|
|
|
176
183
|
- Keep function declarations (or headers) visible at the top level; TinyCWrap scans the C files and headers you pass.
|
|
177
|
-
- If a length cannot be inferred from a contract, you must pass it explicitly from Python.
|
|
184
|
+
- If a length cannot be inferred from a naming convention or contract, you must pass it explicitly from Python.
|
|
178
185
|
- Use `cm.<func>.__source__` to inspect the wrapper code if something behaves unexpectedly.
|
|
179
186
|
- For debugging parsed signatures/contracts call `cm._debug_specs()`.
|
|
180
187
|
|
|
@@ -18,9 +18,7 @@ Write a small C file (only non-`static` functions are exported):
|
|
|
18
18
|
```c
|
|
19
19
|
/* kernels.c */
|
|
20
20
|
double dot(const double *x, const double *y, int len_x)
|
|
21
|
-
/* Return dot product between x and y
|
|
22
|
-
Contract: len_x=len(x);
|
|
23
|
-
*/
|
|
21
|
+
/* Return dot product between x and y */
|
|
24
22
|
{
|
|
25
23
|
double acc = 0.0;
|
|
26
24
|
for (int i = 0; i < len_x; ++i)
|
|
@@ -40,7 +38,7 @@ cm = CModule("kernels.c") # builds the shared library, creates wrappers
|
|
|
40
38
|
x = np.arange(5, dtype=np.float64)
|
|
41
39
|
y = np.ones_like(x)
|
|
42
40
|
|
|
43
|
-
cm.dot(x, y) # -> 10.0, len_x auto-filled by
|
|
41
|
+
cm.dot(x, y) # -> 10.0, len_x auto-filled by convention
|
|
44
42
|
print(cm.dot.__doc__) # docstring comes from the C comment
|
|
45
43
|
```
|
|
46
44
|
|
|
@@ -55,7 +53,7 @@ TinyCWrap infers how to build wrappers from simple C conventions:
|
|
|
55
53
|
- `T *arg` -> output/in-place array (prefer naming it `out_*` when possible).
|
|
56
54
|
- `T arg[N]` in the signature -> fixed-size array (input if `const`, otherwise output).
|
|
57
55
|
- Plain scalars (`double`, `int`, ...) -> Python scalars.
|
|
58
|
-
- **Length parameters**: integers
|
|
56
|
+
- **Length parameters**: integers named like `len_x`, `n_x`, or `size_x` are auto-filled from array `x` by convention. Otherwise pass them explicitly from Python or declare a contract (see below).
|
|
59
57
|
- **Docstrings**: the block comment immediately after the function header becomes the Python docstring.
|
|
60
58
|
- **Structs**: `typedef struct { ... } Name;` definitions in your headers/sources become Python classes with a `.dtype` and NumPy-backed storage.
|
|
61
59
|
- **Compilation**: extra sources can be passed (`CModule("main.c", "helper.c")`), and extra include dirs via `include_dirs=[...]`. Default compiler flags are `-O3 -shared -fPIC -march=native -mtune=native` plus the NumPy include path.
|
|
@@ -69,6 +67,15 @@ Contracts are declared inside the doc comment with `Contract:` (or `Contracts:`)
|
|
|
69
67
|
- `len(out)=len_x` — allocate a 1D output array.
|
|
70
68
|
- `postlen(out)=out_len` — slice the output after the call using an integer pointer result `out_len` (useful when the C code writes less than the allocated length).
|
|
71
69
|
- `own(return)` — the returned pointer is owned by the caller; the wrapper will `free` it after copying to NumPy (requires a `len(return)=...` contract).
|
|
70
|
+
- `expose(len_x)` — opt out of naming-convention inference for `len_x`, keeping it as a required Python argument.
|
|
71
|
+
|
|
72
|
+
TinyCWrap also infers low-risk contracts from names:
|
|
73
|
+
|
|
74
|
+
- `len_x`, `n_x`, or `size_x` are treated as `len(x)` when `x` is an array argument.
|
|
75
|
+
- `out_x` is allocated with `shape(x)` when input array `x` exists.
|
|
76
|
+
- `out_x` is allocated with `len_x`, `n_x`, or `size_x` when no input `x` exists but such a length argument does.
|
|
77
|
+
|
|
78
|
+
Explicit contracts override inferred contracts.
|
|
72
79
|
|
|
73
80
|
Examples pulled from the test suite:
|
|
74
81
|
|
|
@@ -99,9 +106,9 @@ Rules are parsed case-insensitively; whitespace does not matter.
|
|
|
99
106
|
|
|
100
107
|
## Python wrapper behavior
|
|
101
108
|
|
|
102
|
-
- **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
|
|
109
|
+
- **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, naming conventions, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
|
|
103
110
|
- **Struct pointer outputs**: non-const struct pointers are treated as in/out; when you pass an object/array explicitly, it is mutated in place and not returned. When you pass `None`, TinyCWrap allocates and returns the struct (or struct array).
|
|
104
|
-
- **Optional length arguments**: integer length parameters inferred from contracts default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the expression
|
|
111
|
+
- **Optional length arguments**: integer length parameters inferred from contracts or naming conventions default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the inferred expression is evaluated.
|
|
105
112
|
- **Post contracts**: when a contract uses `postlen(...)` or `post shape(...)`, the wrapper slices/reshapes outputs after the C call using the values written by the C function.
|
|
106
113
|
- **Scalar pointer outputs**: pointers to integer-like types (e.g., `int *out_len`) are returned as plain Python integers alongside other outputs.
|
|
107
114
|
- **Structs**: for `typedef struct` declarations TinyCWrap generates a Python class:
|
|
@@ -164,7 +171,7 @@ arr, n = cm.alloc_random_array() # returns NumPy array, frees the C buffer
|
|
|
164
171
|
## Tips
|
|
165
172
|
|
|
166
173
|
- Keep function declarations (or headers) visible at the top level; TinyCWrap scans the C files and headers you pass.
|
|
167
|
-
- If a length cannot be inferred from a contract, you must pass it explicitly from Python.
|
|
174
|
+
- If a length cannot be inferred from a naming convention or contract, you must pass it explicitly from Python.
|
|
168
175
|
- Use `cm.<func>.__source__` to inspect the wrapper code if something behaves unexpectedly.
|
|
169
176
|
- For debugging parsed signatures/contracts call `cm._debug_specs()`.
|
|
170
177
|
|
|
@@ -16,23 +16,30 @@ def cm():
|
|
|
16
16
|
def test_dot(cm):
|
|
17
17
|
x = np.array([1.0, 2.0, 3.0], dtype=np.float64)
|
|
18
18
|
y = np.array([4.0, 5.0, 6.0], dtype=np.float64)
|
|
19
|
-
assert cm.dot(x, y
|
|
19
|
+
assert cm.dot(x, y) == np.dot(x, y)
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def test_scale_auto_output(cm):
|
|
23
23
|
x = np.arange(10, dtype=np.float64)
|
|
24
|
-
scaled_auto = cm.scale(x, 1.1
|
|
24
|
+
scaled_auto = cm.scale(x, 1.1)
|
|
25
25
|
np.testing.assert_allclose(scaled_auto, x * 1.1)
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def test_scale_explicit_output(cm):
|
|
29
29
|
x = np.arange(10, dtype=np.float64)
|
|
30
30
|
out = np.empty_like(x)
|
|
31
|
-
scaled_explicit = cm.scale(x, 2.0,
|
|
31
|
+
scaled_explicit = cm.scale(x, 2.0, out_x=out)
|
|
32
32
|
np.testing.assert_allclose(out, x * 2.0)
|
|
33
33
|
np.testing.assert_allclose(scaled_explicit, x * 2.0)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def test_expose_keeps_inferred_length_visible(cm):
|
|
37
|
+
x = np.arange(5, dtype=np.float64)
|
|
38
|
+
assert cm.prefix_sum(x, len_x=3) == 3.0
|
|
39
|
+
with pytest.raises(TypeError):
|
|
40
|
+
cm.prefix_sum(x)
|
|
41
|
+
|
|
42
|
+
|
|
36
43
|
def test_mat_add_shape_contract(cm):
|
|
37
44
|
a = np.arange(6, dtype=np.float64).reshape(2, 3)
|
|
38
45
|
b = np.ones_like(a)
|
|
@@ -129,7 +136,7 @@ def test_struct_array_argument(cm):
|
|
|
129
136
|
particles["vel"][0] = [1.0, 0.0, 0.0]
|
|
130
137
|
particles["pos"][1] = [0.0, 0.0, 0.0]
|
|
131
138
|
particles["vel"][1] = [0.0, 2.0, 0.0]
|
|
132
|
-
ke = cm.kinetic_energy(particles
|
|
139
|
+
ke = cm.kinetic_energy(particles)
|
|
133
140
|
assert np.isclose(ke, 0.5 * (1.0 + 4.0))
|
|
134
141
|
|
|
135
142
|
|
|
@@ -169,6 +169,7 @@ class CModule:
|
|
|
169
169
|
self._func_specs.pop("free", None)
|
|
170
170
|
self._struct_specs = parse_structs_from_cdef(self._cdef)
|
|
171
171
|
self._attach_docs_from_source()
|
|
172
|
+
self._infer_contracts_from_names()
|
|
172
173
|
self._mark_length_params_from_contracts()
|
|
173
174
|
self._create_struct_classes()
|
|
174
175
|
self._create_wrappers()
|
|
@@ -448,6 +449,7 @@ class CModule:
|
|
|
448
449
|
fspec.doc = doc
|
|
449
450
|
contracts = []
|
|
450
451
|
owns: list[str] = []
|
|
452
|
+
exposes: set[str] = set()
|
|
451
453
|
|
|
452
454
|
def _normalize_shape_expr(expr: str) -> str:
|
|
453
455
|
expr = expr.strip()
|
|
@@ -483,6 +485,13 @@ class CModule:
|
|
|
483
485
|
if n:
|
|
484
486
|
owns.append(n)
|
|
485
487
|
continue
|
|
488
|
+
m_expose_call = re.match(r"expose\s*\(\s*([^)]+)\s*\)\s*$", entry, flags=re.IGNORECASE)
|
|
489
|
+
if m_expose_call:
|
|
490
|
+
for name in m_expose_call.group(1).split(","):
|
|
491
|
+
n = name.strip()
|
|
492
|
+
if n:
|
|
493
|
+
exposes.add(n)
|
|
494
|
+
continue
|
|
486
495
|
is_post = False
|
|
487
496
|
m_post = re.match(r"post\s*(.+)", entry, flags=re.IGNORECASE)
|
|
488
497
|
if m_post and (m_post.group(1).strip().lower().startswith("len(") or m_post.group(1).strip().lower().startswith("shape(")):
|
|
@@ -515,6 +524,84 @@ class CModule:
|
|
|
515
524
|
contracts.append((target, expr, is_post))
|
|
516
525
|
fspec.contracts = contracts or None
|
|
517
526
|
fspec.owns = owns or None
|
|
527
|
+
fspec.exposes = exposes or None
|
|
528
|
+
|
|
529
|
+
@staticmethod
|
|
530
|
+
def _length_arg_target(arg_name: str) -> str | None:
|
|
531
|
+
for prefix in ("len_", "n_", "size_"):
|
|
532
|
+
if arg_name.startswith(prefix) and len(arg_name) > len(prefix):
|
|
533
|
+
return arg_name[len(prefix) :]
|
|
534
|
+
return None
|
|
535
|
+
|
|
536
|
+
@staticmethod
|
|
537
|
+
def _is_integer_arg(arg) -> bool:
|
|
538
|
+
return arg.base_type in (
|
|
539
|
+
"int",
|
|
540
|
+
"unsigned int",
|
|
541
|
+
"unsigned",
|
|
542
|
+
"long",
|
|
543
|
+
"long int",
|
|
544
|
+
"long long",
|
|
545
|
+
"long long int",
|
|
546
|
+
"unsigned long",
|
|
547
|
+
"unsigned long long",
|
|
548
|
+
"size_t",
|
|
549
|
+
"ssize_t",
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def _infer_contracts_from_names(self):
|
|
553
|
+
"""Infer low-risk contracts from conventional argument names."""
|
|
554
|
+
for fspec in self._func_specs.values():
|
|
555
|
+
contracts = list(fspec.contracts or [])
|
|
556
|
+
exposes = fspec.exposes or set()
|
|
557
|
+
explicit_targets = {target for target, _expr, _is_post in contracts}
|
|
558
|
+
explicit_shapes = {
|
|
559
|
+
target[len("shape(") : -1]
|
|
560
|
+
for target, _expr, _is_post in contracts
|
|
561
|
+
if target.startswith("shape(") and target.endswith(")")
|
|
562
|
+
}
|
|
563
|
+
arg_by_name = {a.name: a for a in fspec.args}
|
|
564
|
+
array_names = {
|
|
565
|
+
a.name
|
|
566
|
+
for a in fspec.args
|
|
567
|
+
if a.is_array_in or a.is_array_out or a.is_pointer or a.array_len is not None
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
for arg in fspec.args:
|
|
571
|
+
if arg.name in exposes or not self._is_integer_arg(arg):
|
|
572
|
+
continue
|
|
573
|
+
target = self._length_arg_target(arg.name)
|
|
574
|
+
if target and target in array_names and arg.name not in explicit_targets:
|
|
575
|
+
contracts.append((arg.name, f"len({target})", False))
|
|
576
|
+
explicit_targets.add(arg.name)
|
|
577
|
+
|
|
578
|
+
length_names_by_target: dict[str, str] = {}
|
|
579
|
+
for arg in fspec.args:
|
|
580
|
+
target = self._length_arg_target(arg.name)
|
|
581
|
+
if target and self._is_integer_arg(arg):
|
|
582
|
+
length_names_by_target[target] = arg.name
|
|
583
|
+
|
|
584
|
+
for arg in fspec.args:
|
|
585
|
+
if not (arg.is_array_out and arg.name.lower().startswith("out_")):
|
|
586
|
+
continue
|
|
587
|
+
target = arg.name[4:]
|
|
588
|
+
if arg.name in explicit_targets or arg.name in explicit_shapes:
|
|
589
|
+
continue
|
|
590
|
+
if target in arg_by_name and (
|
|
591
|
+
arg_by_name[target].is_array_in
|
|
592
|
+
or arg_by_name[target].is_array_out
|
|
593
|
+
or arg_by_name[target].is_pointer
|
|
594
|
+
or arg_by_name[target].array_len is not None
|
|
595
|
+
):
|
|
596
|
+
contracts.append((f"shape({arg.name})", f"shape({target})", False))
|
|
597
|
+
explicit_shapes.add(arg.name)
|
|
598
|
+
continue
|
|
599
|
+
length_name = length_names_by_target.get(target)
|
|
600
|
+
if length_name:
|
|
601
|
+
contracts.append((arg.name, length_name, False))
|
|
602
|
+
explicit_targets.add(arg.name)
|
|
603
|
+
|
|
604
|
+
fspec.contracts = contracts or None
|
|
518
605
|
|
|
519
606
|
def _mark_length_params_from_contracts(self):
|
|
520
607
|
"""Mark length-like parameters based solely on explicit contracts."""
|
|
@@ -981,7 +1068,7 @@ class CModule:
|
|
|
981
1068
|
if a.is_array_out and a.name.lower().startswith("out"):
|
|
982
1069
|
extra.append("auto if None")
|
|
983
1070
|
if a.is_length_param:
|
|
984
|
-
extra.append("auto
|
|
1071
|
+
extra.append("auto")
|
|
985
1072
|
extra_txt = f" [{' '.join(extra)}]" if extra else ""
|
|
986
1073
|
arg_docs.append(f"{a.name} : {ctype} ({role}){extra_txt}")
|
|
987
1074
|
|
|
@@ -1194,22 +1281,23 @@ class CModule:
|
|
|
1194
1281
|
out_lines += [
|
|
1195
1282
|
f" arr_{a.name} = np.zeros(int({expr_py}), dtype=base_dtype)"
|
|
1196
1283
|
]
|
|
1197
|
-
elif a.array_len is None and len_expr is None and shape_expr is None:
|
|
1198
|
-
out_lines += [
|
|
1199
|
-
f" arr_{a.name} = np.zeros((), dtype=base_dtype)"
|
|
1200
|
-
]
|
|
1201
1284
|
elif ref_name:
|
|
1202
1285
|
out_lines += [
|
|
1203
1286
|
f" ref_arr = locals().get('arr_{ref_name}', None)",
|
|
1204
1287
|
" if ref_arr is not None:",
|
|
1205
1288
|
f" arr_{a.name} = np.zeros_like(ref_arr, dtype=base_dtype)",
|
|
1206
1289
|
" else:",
|
|
1207
|
-
f"
|
|
1290
|
+
f" arr_{a.name} = np.zeros((), dtype=base_dtype)",
|
|
1208
1291
|
]
|
|
1209
1292
|
else:
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1293
|
+
if a.array_len is None and len_expr is None and shape_expr is None:
|
|
1294
|
+
out_lines += [
|
|
1295
|
+
f" arr_{a.name} = np.zeros((), dtype=base_dtype)"
|
|
1296
|
+
]
|
|
1297
|
+
else:
|
|
1298
|
+
out_lines += [
|
|
1299
|
+
f" raise ValueError('{fspec.name}: provide {a.name} or a Contract for its length')",
|
|
1300
|
+
]
|
|
1213
1301
|
out_lines += [
|
|
1214
1302
|
" else:",
|
|
1215
1303
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinycwrap
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Lightweight C-to-Python wrapper generator using CFFI and NumPy
|
|
5
5
|
Author: TinyCWrap Contributors
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -28,9 +28,7 @@ Write a small C file (only non-`static` functions are exported):
|
|
|
28
28
|
```c
|
|
29
29
|
/* kernels.c */
|
|
30
30
|
double dot(const double *x, const double *y, int len_x)
|
|
31
|
-
/* Return dot product between x and y
|
|
32
|
-
Contract: len_x=len(x);
|
|
33
|
-
*/
|
|
31
|
+
/* Return dot product between x and y */
|
|
34
32
|
{
|
|
35
33
|
double acc = 0.0;
|
|
36
34
|
for (int i = 0; i < len_x; ++i)
|
|
@@ -50,7 +48,7 @@ cm = CModule("kernels.c") # builds the shared library, creates wrappers
|
|
|
50
48
|
x = np.arange(5, dtype=np.float64)
|
|
51
49
|
y = np.ones_like(x)
|
|
52
50
|
|
|
53
|
-
cm.dot(x, y) # -> 10.0, len_x auto-filled by
|
|
51
|
+
cm.dot(x, y) # -> 10.0, len_x auto-filled by convention
|
|
54
52
|
print(cm.dot.__doc__) # docstring comes from the C comment
|
|
55
53
|
```
|
|
56
54
|
|
|
@@ -65,7 +63,7 @@ TinyCWrap infers how to build wrappers from simple C conventions:
|
|
|
65
63
|
- `T *arg` -> output/in-place array (prefer naming it `out_*` when possible).
|
|
66
64
|
- `T arg[N]` in the signature -> fixed-size array (input if `const`, otherwise output).
|
|
67
65
|
- Plain scalars (`double`, `int`, ...) -> Python scalars.
|
|
68
|
-
- **Length parameters**: integers
|
|
66
|
+
- **Length parameters**: integers named like `len_x`, `n_x`, or `size_x` are auto-filled from array `x` by convention. Otherwise pass them explicitly from Python or declare a contract (see below).
|
|
69
67
|
- **Docstrings**: the block comment immediately after the function header becomes the Python docstring.
|
|
70
68
|
- **Structs**: `typedef struct { ... } Name;` definitions in your headers/sources become Python classes with a `.dtype` and NumPy-backed storage.
|
|
71
69
|
- **Compilation**: extra sources can be passed (`CModule("main.c", "helper.c")`), and extra include dirs via `include_dirs=[...]`. Default compiler flags are `-O3 -shared -fPIC -march=native -mtune=native` plus the NumPy include path.
|
|
@@ -79,6 +77,15 @@ Contracts are declared inside the doc comment with `Contract:` (or `Contracts:`)
|
|
|
79
77
|
- `len(out)=len_x` — allocate a 1D output array.
|
|
80
78
|
- `postlen(out)=out_len` — slice the output after the call using an integer pointer result `out_len` (useful when the C code writes less than the allocated length).
|
|
81
79
|
- `own(return)` — the returned pointer is owned by the caller; the wrapper will `free` it after copying to NumPy (requires a `len(return)=...` contract).
|
|
80
|
+
- `expose(len_x)` — opt out of naming-convention inference for `len_x`, keeping it as a required Python argument.
|
|
81
|
+
|
|
82
|
+
TinyCWrap also infers low-risk contracts from names:
|
|
83
|
+
|
|
84
|
+
- `len_x`, `n_x`, or `size_x` are treated as `len(x)` when `x` is an array argument.
|
|
85
|
+
- `out_x` is allocated with `shape(x)` when input array `x` exists.
|
|
86
|
+
- `out_x` is allocated with `len_x`, `n_x`, or `size_x` when no input `x` exists but such a length argument does.
|
|
87
|
+
|
|
88
|
+
Explicit contracts override inferred contracts.
|
|
82
89
|
|
|
83
90
|
Examples pulled from the test suite:
|
|
84
91
|
|
|
@@ -109,9 +116,9 @@ Rules are parsed case-insensitively; whitespace does not matter.
|
|
|
109
116
|
|
|
110
117
|
## Python wrapper behavior
|
|
111
118
|
|
|
112
|
-
- **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
|
|
119
|
+
- **Automatic allocation**: any `out_*` argument defaults to `None` in Python; TinyCWrap allocates it based on contracts, naming conventions, fixed array sizes, or by matching the shape of a related input (`out_x` matches `x` when no contract is present).
|
|
113
120
|
- **Struct pointer outputs**: non-const struct pointers are treated as in/out; when you pass an object/array explicitly, it is mutated in place and not returned. When you pass `None`, TinyCWrap allocates and returns the struct (or struct array).
|
|
114
|
-
- **Optional length arguments**: integer length parameters inferred from contracts default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the expression
|
|
121
|
+
- **Optional length arguments**: integer length parameters inferred from contracts or naming conventions default to `None` in the wrapper signature. If you pass them, they are cast to `int`; if not, the inferred expression is evaluated.
|
|
115
122
|
- **Post contracts**: when a contract uses `postlen(...)` or `post shape(...)`, the wrapper slices/reshapes outputs after the C call using the values written by the C function.
|
|
116
123
|
- **Scalar pointer outputs**: pointers to integer-like types (e.g., `int *out_len`) are returned as plain Python integers alongside other outputs.
|
|
117
124
|
- **Structs**: for `typedef struct` declarations TinyCWrap generates a Python class:
|
|
@@ -174,7 +181,7 @@ arr, n = cm.alloc_random_array() # returns NumPy array, frees the C buffer
|
|
|
174
181
|
## Tips
|
|
175
182
|
|
|
176
183
|
- Keep function declarations (or headers) visible at the top level; TinyCWrap scans the C files and headers you pass.
|
|
177
|
-
- If a length cannot be inferred from a contract, you must pass it explicitly from Python.
|
|
184
|
+
- If a length cannot be inferred from a naming convention or contract, you must pass it explicitly from Python.
|
|
178
185
|
- Use `cm.<func>.__source__` to inspect the wrapper code if something behaves unexpectedly.
|
|
179
186
|
- For debugging parsed signatures/contracts call `cm._debug_specs()`.
|
|
180
187
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|