tinycwrap 0.0.7__tar.gz → 0.1.1__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/PKG-INFO +181 -0
- tinycwrap-0.1.1/README.md +171 -0
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/pyproject.toml +1 -1
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/tests/test_kernels.py +70 -2
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/tinycwrap/__init__.py +1 -1
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/tinycwrap/cmodule.py +349 -91
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/tinycwrap/parsing.py +11 -1
- tinycwrap-0.1.1/tinycwrap.egg-info/PKG-INFO +181 -0
- tinycwrap-0.0.7/PKG-INFO +0 -13
- tinycwrap-0.0.7/README.md +0 -3
- tinycwrap-0.0.7/tinycwrap.egg-info/PKG-INFO +0 -13
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/setup.cfg +0 -0
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/tests/test_geom.py +0 -0
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/tests/test_path.py +0 -0
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/tinycwrap.egg-info/SOURCES.txt +0 -0
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/tinycwrap.egg-info/dependency_links.txt +0 -0
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/tinycwrap.egg-info/requires.txt +0 -0
- {tinycwrap-0.0.7 → tinycwrap-0.1.1}/tinycwrap.egg-info/top_level.txt +0 -0
tinycwrap-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinycwrap
|
|
3
|
+
Version: 0.1.1
|
|
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 is a lightweight helper around CFFI that:
|
|
14
|
+
|
|
15
|
+
- compiles one or more C sources into a shared library,
|
|
16
|
+
- auto-generates the `cdef` from your function/struct declarations,
|
|
17
|
+
- builds NumPy-friendly Python wrappers that take/return arrays and structs,
|
|
18
|
+
- optionally hot-reloads when the C file changes (inside IPython).
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install tinycwrap
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Write a small C file (only non-`static` functions are exported):
|
|
27
|
+
|
|
28
|
+
```c
|
|
29
|
+
/* kernels.c */
|
|
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
|
+
*/
|
|
34
|
+
{
|
|
35
|
+
double acc = 0.0;
|
|
36
|
+
for (int i = 0; i < len_x; ++i)
|
|
37
|
+
acc += x[i] * y[i];
|
|
38
|
+
return acc;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Wrap and call it from Python:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import numpy as np
|
|
46
|
+
from tinycwrap import CModule
|
|
47
|
+
|
|
48
|
+
cm = CModule("kernels.c") # builds the shared library, creates wrappers
|
|
49
|
+
|
|
50
|
+
x = np.arange(5, dtype=np.float64)
|
|
51
|
+
y = np.ones_like(x)
|
|
52
|
+
|
|
53
|
+
cm.dot(x, y) # -> 10.0, len_x auto-filled by the contract
|
|
54
|
+
print(cm.dot.__doc__) # docstring comes from the C comment
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See `examples/` and `tests/` for more involved cases.
|
|
58
|
+
|
|
59
|
+
## C coding conventions
|
|
60
|
+
|
|
61
|
+
TinyCWrap infers how to build wrappers from simple C conventions:
|
|
62
|
+
|
|
63
|
+
- **Inputs vs outputs**
|
|
64
|
+
- `const T *arg` -> input NumPy array.
|
|
65
|
+
- `T *arg` -> output/in-place array (prefer naming it `out_*` when possible).
|
|
66
|
+
- `T arg[N]` in the signature -> fixed-size array (input if `const`, otherwise output).
|
|
67
|
+
- Plain scalars (`double`, `int`, ...) -> Python scalars.
|
|
68
|
+
- **Length parameters**: integers such as `len_x`, `n`, `size_x` can be auto-filled if you declare a contract (see below). Otherwise pass them explicitly from Python.
|
|
69
|
+
- **Docstrings**: the block comment immediately after the function header becomes the Python docstring.
|
|
70
|
+
- **Structs**: `typedef struct { ... } Name;` definitions in your headers/sources become Python classes with a `.dtype` and NumPy-backed storage.
|
|
71
|
+
- **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.
|
|
72
|
+
|
|
73
|
+
## Contracts: tell the wrapper how to size things
|
|
74
|
+
|
|
75
|
+
Contracts are declared inside the doc comment with `Contract:` (or `Contracts:`). Separate multiple rules with semicolons. Supported forms:
|
|
76
|
+
|
|
77
|
+
- `len_x=len(x)` — mark `len_x` as the length of array `x`. If you omit `len_x` in Python, the wrapper fills it.
|
|
78
|
+
- `shape(out)=n,m` — allocate `out` with shape `(n, m)`. You can define `n,m=shape(a)` to capture the shape of an input first.
|
|
79
|
+
- `len(out)=len_x` — allocate a 1D output array.
|
|
80
|
+
- `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
|
+
- `own(return)` — the returned pointer is owned by the caller; the wrapper will `free` it after copying to NumPy (requires a `len(return)=...` contract).
|
|
82
|
+
|
|
83
|
+
Examples pulled from the test suite:
|
|
84
|
+
|
|
85
|
+
```c
|
|
86
|
+
void mat_add(const double *a, const double *b, int n, int m, double *out)
|
|
87
|
+
/* Elementwise addition
|
|
88
|
+
Contract: n,m=shape(a); shape(out)=n,m;
|
|
89
|
+
*/
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```c
|
|
93
|
+
void merge_sorted(const double *a, const double *b, int len_a, int len_b,
|
|
94
|
+
double *out, int *out_len)
|
|
95
|
+
/* Merge unique sorted values
|
|
96
|
+
Contract: len_a=len(a); len_b=len(b);
|
|
97
|
+
len(out)=len_a+len_b; postlen(out)=out_len;
|
|
98
|
+
*/
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```c
|
|
102
|
+
double *alloc_random_array(int *out_len)
|
|
103
|
+
/* Return a freshly malloc'ed array
|
|
104
|
+
Contract: len(return)=out_len; own(return);
|
|
105
|
+
*/
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Rules are parsed case-insensitively; whitespace does not matter.
|
|
109
|
+
|
|
110
|
+
## Python wrapper behavior
|
|
111
|
+
|
|
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).
|
|
113
|
+
- **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 from the contract is evaluated.
|
|
115
|
+
- **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
|
+
- **Scalar pointer outputs**: pointers to integer-like types (e.g., `int *out_len`) are returned as plain Python integers alongside other outputs.
|
|
117
|
+
- **Structs**: for `typedef struct` declarations TinyCWrap generates a Python class:
|
|
118
|
+
- fields are accessible as properties backed by a NumPy structured dtype (`Name.dtype`);
|
|
119
|
+
- scalar struct outputs return `Name` objects;
|
|
120
|
+
- struct-array outputs return compact `NameArray` wrappers, with field arrays as properties (`points.x`), scalar items as `Name` objects (`points[0]`), and Python construction as `NameArray(array_like)` or `NameArray(x=..., y=...)`;
|
|
121
|
+
- pointer fields paired with `len_<field>` automatically allocate NumPy arrays when you instantiate the struct with `len_field` or when you pass an array for that field;
|
|
122
|
+
- `_data` holds the underlying structured array, `Name.zeros(n)` returns a raw array of that dtype, and `NameArray.zeros(n)` returns the typed wrapper.
|
|
123
|
+
- **Reloading**: inside IPython, pass `reload=True` (default) to auto-recompile before each cell if the C sources changed.
|
|
124
|
+
|
|
125
|
+
## Worked examples
|
|
126
|
+
|
|
127
|
+
### Two outputs and automatic lengths
|
|
128
|
+
|
|
129
|
+
```c
|
|
130
|
+
void split_vectors(const double *inp, int len_inp,
|
|
131
|
+
double *out_even, double *out_odd)
|
|
132
|
+
/* Split even/odd elements
|
|
133
|
+
Contract: len_inp=len(inp); len(out_even)=len_inp/2; len(out_odd)=len_inp/2;
|
|
134
|
+
*/
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
data = np.array([0, 1, 2, 3, 4, 5], dtype=np.float64)
|
|
139
|
+
out_even, out_odd = cm.split_vectors(data) # lengths inferred, arrays allocated
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Structs and struct arrays
|
|
143
|
+
|
|
144
|
+
```c
|
|
145
|
+
typedef struct {
|
|
146
|
+
double real;
|
|
147
|
+
double imag;
|
|
148
|
+
} ComplexPair;
|
|
149
|
+
|
|
150
|
+
double complex_magnitude(const ComplexPair *z);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
cp = cm.ComplexPair(real=3.0, imag=4.0)
|
|
155
|
+
cm.complex_magnitude(cp) # -> 5.0
|
|
156
|
+
|
|
157
|
+
# struct arrays are NumPy dtypes; useful when C expects pointers to arrays of structs
|
|
158
|
+
pairs = cm.ComplexPair.zeros(2)
|
|
159
|
+
pairs["real"] = [1.0, 2.0]
|
|
160
|
+
pairs["imag"] = [0.0, -1.0]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Owned return value
|
|
164
|
+
|
|
165
|
+
```c
|
|
166
|
+
double *alloc_random_array(int *out_len)
|
|
167
|
+
/* Contract: len(return)=out_len; own(return); */
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
arr, n = cm.alloc_random_array() # returns NumPy array, frees the C buffer
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Tips
|
|
175
|
+
|
|
176
|
+
- 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.
|
|
178
|
+
- Use `cm.<func>.__source__` to inspect the wrapper code if something behaves unexpectedly.
|
|
179
|
+
- For debugging parsed signatures/contracts call `cm._debug_specs()`.
|
|
180
|
+
|
|
181
|
+
That is all you need to start turning small C helpers into ergonomic Python callables.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# tinycwrap
|
|
2
|
+
|
|
3
|
+
TinyCWrap is a lightweight helper around CFFI that:
|
|
4
|
+
|
|
5
|
+
- compiles one or more C sources into a shared library,
|
|
6
|
+
- auto-generates the `cdef` from your function/struct declarations,
|
|
7
|
+
- builds NumPy-friendly Python wrappers that take/return arrays and structs,
|
|
8
|
+
- optionally hot-reloads when the C file changes (inside IPython).
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install tinycwrap
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Write a small C file (only non-`static` functions are exported):
|
|
17
|
+
|
|
18
|
+
```c
|
|
19
|
+
/* kernels.c */
|
|
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
|
+
*/
|
|
24
|
+
{
|
|
25
|
+
double acc = 0.0;
|
|
26
|
+
for (int i = 0; i < len_x; ++i)
|
|
27
|
+
acc += x[i] * y[i];
|
|
28
|
+
return acc;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Wrap and call it from Python:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import numpy as np
|
|
36
|
+
from tinycwrap import CModule
|
|
37
|
+
|
|
38
|
+
cm = CModule("kernels.c") # builds the shared library, creates wrappers
|
|
39
|
+
|
|
40
|
+
x = np.arange(5, dtype=np.float64)
|
|
41
|
+
y = np.ones_like(x)
|
|
42
|
+
|
|
43
|
+
cm.dot(x, y) # -> 10.0, len_x auto-filled by the contract
|
|
44
|
+
print(cm.dot.__doc__) # docstring comes from the C comment
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
See `examples/` and `tests/` for more involved cases.
|
|
48
|
+
|
|
49
|
+
## C coding conventions
|
|
50
|
+
|
|
51
|
+
TinyCWrap infers how to build wrappers from simple C conventions:
|
|
52
|
+
|
|
53
|
+
- **Inputs vs outputs**
|
|
54
|
+
- `const T *arg` -> input NumPy array.
|
|
55
|
+
- `T *arg` -> output/in-place array (prefer naming it `out_*` when possible).
|
|
56
|
+
- `T arg[N]` in the signature -> fixed-size array (input if `const`, otherwise output).
|
|
57
|
+
- Plain scalars (`double`, `int`, ...) -> Python scalars.
|
|
58
|
+
- **Length parameters**: integers such as `len_x`, `n`, `size_x` can be auto-filled if you declare a contract (see below). Otherwise pass them explicitly from Python.
|
|
59
|
+
- **Docstrings**: the block comment immediately after the function header becomes the Python docstring.
|
|
60
|
+
- **Structs**: `typedef struct { ... } Name;` definitions in your headers/sources become Python classes with a `.dtype` and NumPy-backed storage.
|
|
61
|
+
- **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.
|
|
62
|
+
|
|
63
|
+
## Contracts: tell the wrapper how to size things
|
|
64
|
+
|
|
65
|
+
Contracts are declared inside the doc comment with `Contract:` (or `Contracts:`). Separate multiple rules with semicolons. Supported forms:
|
|
66
|
+
|
|
67
|
+
- `len_x=len(x)` — mark `len_x` as the length of array `x`. If you omit `len_x` in Python, the wrapper fills it.
|
|
68
|
+
- `shape(out)=n,m` — allocate `out` with shape `(n, m)`. You can define `n,m=shape(a)` to capture the shape of an input first.
|
|
69
|
+
- `len(out)=len_x` — allocate a 1D output array.
|
|
70
|
+
- `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
|
+
- `own(return)` — the returned pointer is owned by the caller; the wrapper will `free` it after copying to NumPy (requires a `len(return)=...` contract).
|
|
72
|
+
|
|
73
|
+
Examples pulled from the test suite:
|
|
74
|
+
|
|
75
|
+
```c
|
|
76
|
+
void mat_add(const double *a, const double *b, int n, int m, double *out)
|
|
77
|
+
/* Elementwise addition
|
|
78
|
+
Contract: n,m=shape(a); shape(out)=n,m;
|
|
79
|
+
*/
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```c
|
|
83
|
+
void merge_sorted(const double *a, const double *b, int len_a, int len_b,
|
|
84
|
+
double *out, int *out_len)
|
|
85
|
+
/* Merge unique sorted values
|
|
86
|
+
Contract: len_a=len(a); len_b=len(b);
|
|
87
|
+
len(out)=len_a+len_b; postlen(out)=out_len;
|
|
88
|
+
*/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```c
|
|
92
|
+
double *alloc_random_array(int *out_len)
|
|
93
|
+
/* Return a freshly malloc'ed array
|
|
94
|
+
Contract: len(return)=out_len; own(return);
|
|
95
|
+
*/
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Rules are parsed case-insensitively; whitespace does not matter.
|
|
99
|
+
|
|
100
|
+
## Python wrapper behavior
|
|
101
|
+
|
|
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).
|
|
103
|
+
- **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 from the contract is evaluated.
|
|
105
|
+
- **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
|
+
- **Scalar pointer outputs**: pointers to integer-like types (e.g., `int *out_len`) are returned as plain Python integers alongside other outputs.
|
|
107
|
+
- **Structs**: for `typedef struct` declarations TinyCWrap generates a Python class:
|
|
108
|
+
- fields are accessible as properties backed by a NumPy structured dtype (`Name.dtype`);
|
|
109
|
+
- scalar struct outputs return `Name` objects;
|
|
110
|
+
- struct-array outputs return compact `NameArray` wrappers, with field arrays as properties (`points.x`), scalar items as `Name` objects (`points[0]`), and Python construction as `NameArray(array_like)` or `NameArray(x=..., y=...)`;
|
|
111
|
+
- pointer fields paired with `len_<field>` automatically allocate NumPy arrays when you instantiate the struct with `len_field` or when you pass an array for that field;
|
|
112
|
+
- `_data` holds the underlying structured array, `Name.zeros(n)` returns a raw array of that dtype, and `NameArray.zeros(n)` returns the typed wrapper.
|
|
113
|
+
- **Reloading**: inside IPython, pass `reload=True` (default) to auto-recompile before each cell if the C sources changed.
|
|
114
|
+
|
|
115
|
+
## Worked examples
|
|
116
|
+
|
|
117
|
+
### Two outputs and automatic lengths
|
|
118
|
+
|
|
119
|
+
```c
|
|
120
|
+
void split_vectors(const double *inp, int len_inp,
|
|
121
|
+
double *out_even, double *out_odd)
|
|
122
|
+
/* Split even/odd elements
|
|
123
|
+
Contract: len_inp=len(inp); len(out_even)=len_inp/2; len(out_odd)=len_inp/2;
|
|
124
|
+
*/
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
data = np.array([0, 1, 2, 3, 4, 5], dtype=np.float64)
|
|
129
|
+
out_even, out_odd = cm.split_vectors(data) # lengths inferred, arrays allocated
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Structs and struct arrays
|
|
133
|
+
|
|
134
|
+
```c
|
|
135
|
+
typedef struct {
|
|
136
|
+
double real;
|
|
137
|
+
double imag;
|
|
138
|
+
} ComplexPair;
|
|
139
|
+
|
|
140
|
+
double complex_magnitude(const ComplexPair *z);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
cp = cm.ComplexPair(real=3.0, imag=4.0)
|
|
145
|
+
cm.complex_magnitude(cp) # -> 5.0
|
|
146
|
+
|
|
147
|
+
# struct arrays are NumPy dtypes; useful when C expects pointers to arrays of structs
|
|
148
|
+
pairs = cm.ComplexPair.zeros(2)
|
|
149
|
+
pairs["real"] = [1.0, 2.0]
|
|
150
|
+
pairs["imag"] = [0.0, -1.0]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Owned return value
|
|
154
|
+
|
|
155
|
+
```c
|
|
156
|
+
double *alloc_random_array(int *out_len)
|
|
157
|
+
/* Contract: len(return)=out_len; own(return); */
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
arr, n = cm.alloc_random_array() # returns NumPy array, frees the C buffer
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Tips
|
|
165
|
+
|
|
166
|
+
- 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.
|
|
168
|
+
- Use `cm.<func>.__source__` to inspect the wrapper code if something behaves unexpectedly.
|
|
169
|
+
- For debugging parsed signatures/contracts call `cm._debug_specs()`.
|
|
170
|
+
|
|
171
|
+
That is all you need to start turning small C helpers into ergonomic Python callables.
|
|
@@ -33,6 +33,20 @@ def test_scale_explicit_output(cm):
|
|
|
33
33
|
np.testing.assert_allclose(scaled_explicit, x * 2.0)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def test_mat_add_shape_contract(cm):
|
|
37
|
+
a = np.arange(6, dtype=np.float64).reshape(2, 3)
|
|
38
|
+
b = np.ones_like(a)
|
|
39
|
+
out = cm.mat_add(a, b)
|
|
40
|
+
assert out.shape == (2, 3)
|
|
41
|
+
np.testing.assert_allclose(out, a + b)
|
|
42
|
+
|
|
43
|
+
flat = np.zeros(a.size, dtype=np.float64)
|
|
44
|
+
reshaped = cm.mat_add(a, b, out=flat)
|
|
45
|
+
assert reshaped.shape == (2, 3)
|
|
46
|
+
np.testing.assert_allclose(reshaped, a + b)
|
|
47
|
+
np.testing.assert_allclose(flat.reshape(a.shape), a + b)
|
|
48
|
+
|
|
49
|
+
|
|
36
50
|
def test_struct_wrapper(cm):
|
|
37
51
|
cp = cm.ComplexPair(real=1.5, imag=-2.5)
|
|
38
52
|
assert repr(cp) == "ComplexPair(real=1.5, imag=-2.5)"
|
|
@@ -52,6 +66,53 @@ def test_struct_argument(cm):
|
|
|
52
66
|
assert np.isclose(mag_sq, 5.0)
|
|
53
67
|
|
|
54
68
|
|
|
69
|
+
def test_struct_pointer_inplace_no_return(cm):
|
|
70
|
+
cp = cm.ComplexPair(real=2.0, imag=-3.0)
|
|
71
|
+
res = cm.scale_complex(cp, 2.0)
|
|
72
|
+
assert res is None
|
|
73
|
+
assert cp.real == 4.0
|
|
74
|
+
assert cp.imag == -6.0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_struct_scalar_output_returns_struct_object(cm):
|
|
78
|
+
a = cm.ComplexPair(real=2.0, imag=4.0)
|
|
79
|
+
b = cm.ComplexPair(real=6.0, imag=10.0)
|
|
80
|
+
mid = cm.midpoint_complex(a, b)
|
|
81
|
+
assert isinstance(mid, cm.ComplexPair)
|
|
82
|
+
assert mid.real == 4.0
|
|
83
|
+
assert mid.imag == 7.0
|
|
84
|
+
|
|
85
|
+
out = cm.ComplexPair()
|
|
86
|
+
res = cm.midpoint_complex(a, b, out_z=out)
|
|
87
|
+
assert res is None
|
|
88
|
+
assert out.real == 4.0
|
|
89
|
+
assert out.imag == 7.0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_struct_array_output_returns_array_wrapper(cm):
|
|
93
|
+
pairs = cm.make_complex_pairs()
|
|
94
|
+
assert isinstance(pairs, cm.ComplexPairArray)
|
|
95
|
+
assert repr(pairs) == "<Array ComplexPair[3]>"
|
|
96
|
+
assert pairs.shape == (3,)
|
|
97
|
+
np.testing.assert_allclose(pairs.real, np.array([0.0, 1.0, 2.0]))
|
|
98
|
+
np.testing.assert_allclose(pairs.imag, np.array([0.0, -1.0, -2.0]))
|
|
99
|
+
np.testing.assert_allclose(pairs["real"], pairs.real)
|
|
100
|
+
assert isinstance(pairs[1], cm.ComplexPair)
|
|
101
|
+
assert pairs[1].real == 1.0
|
|
102
|
+
assert pairs[1].imag == -1.0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_struct_array_initialization(cm):
|
|
106
|
+
from_array_like = cm.ComplexPairArray([(1.0, 2.0), (3.0, 4.0)])
|
|
107
|
+
assert isinstance(from_array_like[0], cm.ComplexPair)
|
|
108
|
+
np.testing.assert_allclose(from_array_like.real, [1.0, 3.0])
|
|
109
|
+
np.testing.assert_allclose(from_array_like.imag, [2.0, 4.0])
|
|
110
|
+
|
|
111
|
+
from_fields = cm.ComplexPairArray(real=[5.0, 6.0], imag=-1.0)
|
|
112
|
+
np.testing.assert_allclose(from_fields.real, [5.0, 6.0])
|
|
113
|
+
np.testing.assert_allclose(from_fields.imag, [-1.0, -1.0])
|
|
114
|
+
|
|
115
|
+
|
|
55
116
|
def test_struct_array_member(cm):
|
|
56
117
|
p = cm.Particle()
|
|
57
118
|
p.pos = [1.0, 2.0, 3.0]
|
|
@@ -98,7 +159,14 @@ def test_owned_array(cm):
|
|
|
98
159
|
def test_struct_output_array(cm):
|
|
99
160
|
n = 4
|
|
100
161
|
particles = cm.Particle.zeros(n)
|
|
101
|
-
|
|
102
|
-
|
|
162
|
+
res = cm.make_particles(3.0, out_p=particles, len_p=n)
|
|
163
|
+
assert res is None
|
|
103
164
|
np.testing.assert_allclose(particles["pos"], np.ones((n, 3)))
|
|
104
165
|
np.testing.assert_allclose(particles["vel"], np.array([[3.0, 0.0, 0.0]] * n))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_struct_output_array_auto_return(cm):
|
|
169
|
+
n = 3
|
|
170
|
+
particles = cm.make_particles(2.5, len_p=n)
|
|
171
|
+
np.testing.assert_allclose(particles["pos"], np.ones((n, 3)))
|
|
172
|
+
np.testing.assert_allclose(particles["vel"], np.array([[2.5, 0.0, 0.0]] * n))
|
|
@@ -97,6 +97,7 @@ class CModule:
|
|
|
97
97
|
self._struct_specs: dict[str, StructSpec] = {}
|
|
98
98
|
self._struct_dtypes: dict[str, np.dtype] = {}
|
|
99
99
|
self._struct_classes: dict[str, type] = {}
|
|
100
|
+
self._struct_array_classes: dict[str, type] = {}
|
|
100
101
|
self._ipython_hook = None
|
|
101
102
|
self._cdef: str | None = None
|
|
102
103
|
|
|
@@ -187,10 +188,18 @@ class CModule:
|
|
|
187
188
|
delattr(self, sname)
|
|
188
189
|
except Exception:
|
|
189
190
|
pass
|
|
191
|
+
for sname in list(self._struct_array_classes.keys()):
|
|
192
|
+
aname = f"{sname}Array"
|
|
193
|
+
if hasattr(self, aname):
|
|
194
|
+
try:
|
|
195
|
+
delattr(self, aname)
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
190
198
|
self._func_specs.clear()
|
|
191
199
|
self._struct_specs.clear()
|
|
192
200
|
self._struct_dtypes.clear()
|
|
193
201
|
self._struct_classes.clear()
|
|
202
|
+
self._struct_array_classes.clear()
|
|
194
203
|
self._ffi = None
|
|
195
204
|
self._lib = None
|
|
196
205
|
self._so_path = None
|
|
@@ -439,29 +448,71 @@ class CModule:
|
|
|
439
448
|
fspec.doc = doc
|
|
440
449
|
contracts = []
|
|
441
450
|
owns: list[str] = []
|
|
451
|
+
|
|
452
|
+
def _normalize_shape_expr(expr: str) -> str:
|
|
453
|
+
expr = expr.strip()
|
|
454
|
+
if "," in expr and not expr.startswith("("):
|
|
455
|
+
expr = f"({expr})"
|
|
456
|
+
return expr
|
|
457
|
+
|
|
458
|
+
contract_items: list[str] = []
|
|
442
459
|
for line in doc.splitlines():
|
|
443
|
-
m_contract = re.search(r"
|
|
444
|
-
if
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
owns.append(n)
|
|
460
|
+
m_contract = re.search(r"contracts?\s*:\s*(.*)", line, flags=re.IGNORECASE)
|
|
461
|
+
if m_contract:
|
|
462
|
+
after = m_contract.group(1)
|
|
463
|
+
for part in after.split(";"):
|
|
464
|
+
part = part.strip()
|
|
465
|
+
if part:
|
|
466
|
+
contract_items.append(part)
|
|
451
467
|
continue
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
468
|
+
m_own = re.search(r"own:\s*(.*)", line, flags=re.IGNORECASE)
|
|
469
|
+
if m_own:
|
|
470
|
+
for name in m_own.group(1).split(","):
|
|
471
|
+
n = name.strip()
|
|
472
|
+
if n:
|
|
473
|
+
owns.append(n)
|
|
474
|
+
|
|
475
|
+
for item in contract_items:
|
|
476
|
+
entry = item.strip()
|
|
477
|
+
if not entry:
|
|
478
|
+
continue
|
|
479
|
+
m_own_call = re.match(r"own\s*\(\s*([^)]+)\s*\)\s*$", entry, flags=re.IGNORECASE)
|
|
480
|
+
if m_own_call:
|
|
481
|
+
for name in m_own_call.group(1).split(","):
|
|
482
|
+
n = name.strip()
|
|
483
|
+
if n:
|
|
484
|
+
owns.append(n)
|
|
485
|
+
continue
|
|
486
|
+
is_post = False
|
|
487
|
+
m_post = re.match(r"post\s*(.+)", entry, flags=re.IGNORECASE)
|
|
488
|
+
if m_post and (m_post.group(1).strip().lower().startswith("len(") or m_post.group(1).strip().lower().startswith("shape(")):
|
|
489
|
+
is_post = True
|
|
490
|
+
entry = m_post.group(1).strip()
|
|
491
|
+
mshape_out = re.match(r"shape\((\w+)\)\s*=\s*(.+)", entry, flags=re.IGNORECASE)
|
|
492
|
+
if mshape_out:
|
|
493
|
+
target = f"shape({mshape_out.group(1)})"
|
|
494
|
+
expr = _normalize_shape_expr(mshape_out.group(2))
|
|
495
|
+
contracts.append((target, expr, is_post))
|
|
496
|
+
continue
|
|
497
|
+
mshape_in = re.match(r"(.+?)\s*=\s*shape\((\w+)\)", entry, flags=re.IGNORECASE)
|
|
498
|
+
if mshape_in:
|
|
499
|
+
targets_raw = mshape_in.group(1)
|
|
500
|
+
arr_name = mshape_in.group(2)
|
|
501
|
+
targets = [t.strip() for t in targets_raw.split(",") if t.strip()]
|
|
502
|
+
for idx, tgt in enumerate(targets):
|
|
503
|
+
contracts.append((tgt, f"shape({arr_name})[{idx}]", is_post))
|
|
504
|
+
continue
|
|
505
|
+
mlen = re.match(r"len\((\w+)\)\s*=\s*(.+)", entry, flags=re.IGNORECASE)
|
|
506
|
+
if mlen:
|
|
507
|
+
target = mlen.group(1)
|
|
508
|
+
expr = mlen.group(2).strip()
|
|
509
|
+
contracts.append((target, expr, is_post))
|
|
510
|
+
continue
|
|
511
|
+
mgeneric = re.match(r"(\w+)\s*=\s*(.+)", entry)
|
|
512
|
+
if mgeneric:
|
|
513
|
+
target = mgeneric.group(1)
|
|
514
|
+
expr = mgeneric.group(2).strip()
|
|
515
|
+
contracts.append((target, expr, is_post))
|
|
465
516
|
fspec.contracts = contracts or None
|
|
466
517
|
fspec.owns = owns or None
|
|
467
518
|
|
|
@@ -633,10 +684,23 @@ class CModule:
|
|
|
633
684
|
parts = ", ".join(parts_list)
|
|
634
685
|
return f"{spec.name}({parts})"
|
|
635
686
|
|
|
687
|
+
@classmethod
|
|
688
|
+
def _from_data(cls, data, copy=False):
|
|
689
|
+
arr = np.asarray(data, dtype=dtype)
|
|
690
|
+
if arr.shape != ():
|
|
691
|
+
raise ValueError(f"{spec.name} expects scalar data, got shape {arr.shape}")
|
|
692
|
+
if copy:
|
|
693
|
+
arr = np.array(arr, dtype=dtype, copy=True)
|
|
694
|
+
obj = cls()
|
|
695
|
+
object.__setattr__(obj, "_data", arr)
|
|
696
|
+
object.__setattr__(obj, "_children", {})
|
|
697
|
+
return obj
|
|
698
|
+
|
|
636
699
|
namespace = {
|
|
637
700
|
"__slots__": slots,
|
|
638
701
|
"__init__": __init__,
|
|
639
702
|
"__repr__": __repr__,
|
|
703
|
+
"_from_data": _from_data,
|
|
640
704
|
"__doc__": f"Python wrapper for C struct {spec.name}. Fields: {', '.join(dtype.names)}.",
|
|
641
705
|
"dtype": dtype,
|
|
642
706
|
"zeros": staticmethod(
|
|
@@ -679,10 +743,147 @@ class CModule:
|
|
|
679
743
|
|
|
680
744
|
return type(spec.name, (), namespace)
|
|
681
745
|
|
|
746
|
+
def make_struct_array_class(spec, dtype, struct_cls):
|
|
747
|
+
slots = ("_data",)
|
|
748
|
+
class_name = f"{spec.name}Array"
|
|
749
|
+
|
|
750
|
+
def __init__(self, data=None, copy=False, **kwargs):
|
|
751
|
+
if data is not None and kwargs:
|
|
752
|
+
raise TypeError(f"{class_name} accepts either data or field values, not both")
|
|
753
|
+
if kwargs:
|
|
754
|
+
unknown = sorted(set(kwargs) - set(dtype.names))
|
|
755
|
+
if unknown:
|
|
756
|
+
names = ", ".join(unknown)
|
|
757
|
+
raise TypeError(f"{class_name} got unknown field(s): {names}")
|
|
758
|
+
field_values = {}
|
|
759
|
+
lengths = []
|
|
760
|
+
for fname, value in kwargs.items():
|
|
761
|
+
field_dtype = dtype.fields[fname][0]
|
|
762
|
+
if field_dtype.shape == ():
|
|
763
|
+
value_arr = np.asarray(value, dtype=field_dtype)
|
|
764
|
+
if value_arr.shape != ():
|
|
765
|
+
lengths.append(value_arr.shape[0])
|
|
766
|
+
else:
|
|
767
|
+
value_arr = np.asarray(value, dtype=field_dtype.base)
|
|
768
|
+
if value_arr.shape == field_dtype.shape:
|
|
769
|
+
pass
|
|
770
|
+
elif value_arr.shape[1:] == field_dtype.shape:
|
|
771
|
+
lengths.append(value_arr.shape[0])
|
|
772
|
+
else:
|
|
773
|
+
raise ValueError(
|
|
774
|
+
f"Field {fname} expects shape {field_dtype.shape} or "
|
|
775
|
+
f"(n, {', '.join(str(s) for s in field_dtype.shape)}), "
|
|
776
|
+
f"got {value_arr.shape}"
|
|
777
|
+
)
|
|
778
|
+
field_values[fname] = value_arr
|
|
779
|
+
if lengths and len(set(lengths)) != 1:
|
|
780
|
+
raise ValueError(f"{class_name} field values have inconsistent lengths: {lengths}")
|
|
781
|
+
n = lengths[0] if lengths else 1
|
|
782
|
+
arr = np.zeros(n, dtype=dtype)
|
|
783
|
+
for fname, value_arr in field_values.items():
|
|
784
|
+
arr[fname] = value_arr
|
|
785
|
+
elif data is None:
|
|
786
|
+
arr = np.zeros(0, dtype=dtype)
|
|
787
|
+
elif hasattr(data, "_data") and getattr(data, "_data", None) is not None:
|
|
788
|
+
arr = np.asarray(data._data, dtype=dtype)
|
|
789
|
+
elif isinstance(data, (list, tuple)) and data and hasattr(data[0], "_data"):
|
|
790
|
+
arr = np.asarray([item._data for item in data], dtype=dtype)
|
|
791
|
+
else:
|
|
792
|
+
arr = np.asarray(data, dtype=dtype)
|
|
793
|
+
if arr.shape == ():
|
|
794
|
+
arr = arr.reshape(1)
|
|
795
|
+
if copy:
|
|
796
|
+
arr = np.array(arr, dtype=dtype, copy=True)
|
|
797
|
+
object.__setattr__(self, "_data", arr)
|
|
798
|
+
|
|
799
|
+
@classmethod
|
|
800
|
+
def _from_data(cls, data, copy=False):
|
|
801
|
+
return cls(data, copy=copy)
|
|
802
|
+
|
|
803
|
+
@staticmethod
|
|
804
|
+
def zeros(n):
|
|
805
|
+
return array_cls(np.zeros(n, dtype=dtype))
|
|
806
|
+
|
|
807
|
+
def __len__(self):
|
|
808
|
+
return len(self._data)
|
|
809
|
+
|
|
810
|
+
def __iter__(self):
|
|
811
|
+
for idx in range(len(self)):
|
|
812
|
+
yield self[idx]
|
|
813
|
+
|
|
814
|
+
def __array__(self, dtype=None, copy=None):
|
|
815
|
+
arr = np.asarray(self._data, dtype=dtype)
|
|
816
|
+
if copy:
|
|
817
|
+
arr = np.array(arr, copy=True)
|
|
818
|
+
return arr
|
|
819
|
+
|
|
820
|
+
def __getitem__(self, key):
|
|
821
|
+
if isinstance(key, str):
|
|
822
|
+
return self._data[key]
|
|
823
|
+
if isinstance(key, (int, np.integer)):
|
|
824
|
+
idx = int(key)
|
|
825
|
+
if idx < 0:
|
|
826
|
+
idx += len(self._data)
|
|
827
|
+
return struct_cls._from_data(self._data[idx:idx + 1].reshape(()))
|
|
828
|
+
value = self._data[key]
|
|
829
|
+
if getattr(value, "dtype", None) == dtype:
|
|
830
|
+
if getattr(value, "shape", ()) == ():
|
|
831
|
+
return struct_cls._from_data(value)
|
|
832
|
+
return array_cls(value)
|
|
833
|
+
return value
|
|
834
|
+
|
|
835
|
+
def __setitem__(self, key, value):
|
|
836
|
+
if hasattr(value, "_data") and getattr(value, "_data", None) is not None:
|
|
837
|
+
self._data[key] = value._data
|
|
838
|
+
else:
|
|
839
|
+
self._data[key] = value
|
|
840
|
+
|
|
841
|
+
def __repr__(self):
|
|
842
|
+
return f"<Array {spec.name}[{len(self)}]>"
|
|
843
|
+
|
|
844
|
+
namespace = {
|
|
845
|
+
"__slots__": slots,
|
|
846
|
+
"__init__": __init__,
|
|
847
|
+
"__repr__": __repr__,
|
|
848
|
+
"__len__": __len__,
|
|
849
|
+
"__iter__": __iter__,
|
|
850
|
+
"__array__": __array__,
|
|
851
|
+
"__getitem__": __getitem__,
|
|
852
|
+
"__setitem__": __setitem__,
|
|
853
|
+
"_from_data": _from_data,
|
|
854
|
+
"zeros": zeros,
|
|
855
|
+
"dtype": dtype,
|
|
856
|
+
"__doc__": f"Python wrapper for arrays of C struct {spec.name}.",
|
|
857
|
+
"array": property(lambda self: self._data),
|
|
858
|
+
"shape": property(lambda self: self._data.shape),
|
|
859
|
+
"ndim": property(lambda self: self._data.ndim),
|
|
860
|
+
"size": property(lambda self: self._data.size),
|
|
861
|
+
"ctypes": property(lambda self: self._data.ctypes),
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
for fname in dtype.names:
|
|
865
|
+
namespace[fname] = property(lambda self, fname=fname: self._data[fname])
|
|
866
|
+
|
|
867
|
+
array_cls = type(class_name, (), namespace)
|
|
868
|
+
return array_cls
|
|
869
|
+
|
|
682
870
|
struct_cls = make_struct_class(sspec, dtype)
|
|
871
|
+
struct_array_cls = make_struct_array_class(sspec, dtype, struct_cls)
|
|
683
872
|
self._struct_dtypes[sname] = dtype
|
|
684
873
|
self._struct_classes[sname] = struct_cls
|
|
874
|
+
self._struct_array_classes[sname] = struct_array_cls
|
|
685
875
|
setattr(self, sname, struct_cls)
|
|
876
|
+
setattr(self, f"{sname}Array", struct_array_cls)
|
|
877
|
+
|
|
878
|
+
def _wrap_struct_output(self, value, struct_name: str):
|
|
879
|
+
struct_cls = self._struct_classes[struct_name]
|
|
880
|
+
struct_array_cls = self._struct_array_classes[struct_name]
|
|
881
|
+
if isinstance(value, (struct_cls, struct_array_cls)):
|
|
882
|
+
return value
|
|
883
|
+
arr = np.asarray(value, dtype=self._struct_dtypes[struct_name])
|
|
884
|
+
if arr.shape == ():
|
|
885
|
+
return struct_cls._from_data(arr)
|
|
886
|
+
return struct_array_cls._from_data(arr)
|
|
686
887
|
|
|
687
888
|
def _make_wrapper_from_spec(self, fspec: FuncSpec):
|
|
688
889
|
struct_names = set(self._struct_specs.keys())
|
|
@@ -709,6 +910,7 @@ class CModule:
|
|
|
709
910
|
"base_type_from_ctype": base_type_from_ctype,
|
|
710
911
|
"_struct_classes": self._struct_classes,
|
|
711
912
|
"_struct_dtypes": self._struct_dtypes,
|
|
913
|
+
"_struct_array_classes": self._struct_array_classes,
|
|
712
914
|
}
|
|
713
915
|
filename = f"<cmodule:{self._c_path.name}:{fspec.name}>"
|
|
714
916
|
linecache.cache[filename] = (
|
|
@@ -813,8 +1015,18 @@ class CModule:
|
|
|
813
1015
|
|
|
814
1016
|
contract_map: dict[str, str] = {}
|
|
815
1017
|
post_contract_map: dict[str, str] = {}
|
|
1018
|
+
shape_contract_map: dict[str, str] = {}
|
|
1019
|
+
post_shape_contract_map: dict[str, str] = {}
|
|
816
1020
|
if getattr(fspec, "contracts", None):
|
|
817
1021
|
for target, expr, is_post in fspec.contracts:
|
|
1022
|
+
is_shape = target.startswith("shape(") and target.endswith(")")
|
|
1023
|
+
if is_shape:
|
|
1024
|
+
arr_name = target[len("shape(") : -1]
|
|
1025
|
+
if is_post:
|
|
1026
|
+
post_shape_contract_map[arr_name] = expr
|
|
1027
|
+
else:
|
|
1028
|
+
shape_contract_map[arr_name] = expr
|
|
1029
|
+
continue
|
|
818
1030
|
if is_post:
|
|
819
1031
|
post_contract_map[target] = expr
|
|
820
1032
|
else:
|
|
@@ -848,6 +1060,7 @@ class CModule:
|
|
|
848
1060
|
def _expr_py(expr: str) -> str:
|
|
849
1061
|
expr = expr.strip()
|
|
850
1062
|
expr = re.sub(r"len\((\w+)\)", r"len(arr_\1)", expr)
|
|
1063
|
+
expr = re.sub(r"shape\((\w+)\)", r"np.shape(arr_\1)", expr)
|
|
851
1064
|
for name in pointer_scalar_names:
|
|
852
1065
|
expr = re.sub(rf"\b{name}\b", f"int(arr_{name}.ravel()[0])", expr)
|
|
853
1066
|
expr = re.sub(rf"\b{name}\b", f"scalar_{name}", expr)
|
|
@@ -870,6 +1083,8 @@ class CModule:
|
|
|
870
1083
|
call_args: list[str] = []
|
|
871
1084
|
output_vars: list[str] = []
|
|
872
1085
|
output_names: list[str] = []
|
|
1086
|
+
output_conditions: dict[str, str] = {}
|
|
1087
|
+
struct_output_types: dict[str, str] = {}
|
|
873
1088
|
pointer_scalar_outputs: list[tuple[str, str]] = []
|
|
874
1089
|
struct_scalar_outputs: list[tuple[str, str, str]] = []
|
|
875
1090
|
pre_lines: list[str] = []
|
|
@@ -952,20 +1167,34 @@ class CModule:
|
|
|
952
1167
|
else:
|
|
953
1168
|
dtype_expr = f"np.dtype('{np.dtype(numpy_dtype_for_base_type(a.base_type)).name}')"
|
|
954
1169
|
ref_name = a.name[4:] if a.name.lower().startswith("out_") else None
|
|
1170
|
+
shape_expr = shape_contract_map.get(a.name)
|
|
1171
|
+
len_expr = contract_map.get(a.name)
|
|
955
1172
|
out_lines += [
|
|
956
1173
|
f" base_dtype = {dtype_expr}",
|
|
1174
|
+
]
|
|
1175
|
+
if a.base_type in struct_names:
|
|
1176
|
+
out_lines += [
|
|
1177
|
+
f" _ret_{a.name} = {a.name} is None",
|
|
1178
|
+
]
|
|
1179
|
+
out_lines += [
|
|
957
1180
|
f" if {a.name} is None:",
|
|
958
1181
|
]
|
|
959
1182
|
if a.array_len is not None:
|
|
960
1183
|
out_lines += [
|
|
961
1184
|
f" arr_{a.name} = np.zeros({int(a.array_len)}, dtype=base_dtype)"
|
|
962
1185
|
]
|
|
963
|
-
elif
|
|
964
|
-
expr_py = _expr_py(
|
|
1186
|
+
elif shape_expr is not None:
|
|
1187
|
+
expr_py = _expr_py(shape_expr)
|
|
1188
|
+
out_lines += [
|
|
1189
|
+
f" _shape_{a.name} = tuple({expr_py})",
|
|
1190
|
+
f" arr_{a.name} = np.zeros(_shape_{a.name}, dtype=base_dtype)",
|
|
1191
|
+
]
|
|
1192
|
+
elif len_expr is not None:
|
|
1193
|
+
expr_py = _expr_py(len_expr)
|
|
965
1194
|
out_lines += [
|
|
966
1195
|
f" arr_{a.name} = np.zeros(int({expr_py}), dtype=base_dtype)"
|
|
967
1196
|
]
|
|
968
|
-
elif a.array_len is None and
|
|
1197
|
+
elif a.array_len is None and len_expr is None and shape_expr is None:
|
|
969
1198
|
out_lines += [
|
|
970
1199
|
f" arr_{a.name} = np.zeros((), dtype=base_dtype)"
|
|
971
1200
|
]
|
|
@@ -983,11 +1212,33 @@ class CModule:
|
|
|
983
1212
|
]
|
|
984
1213
|
out_lines += [
|
|
985
1214
|
" else:",
|
|
986
|
-
|
|
1215
|
+
]
|
|
1216
|
+
if a.base_type in struct_names:
|
|
1217
|
+
out_lines += [
|
|
1218
|
+
f" if hasattr({a.name}, '_data') and getattr({a.name}, '_data', None) is not None and getattr({a.name}, '_data').dtype == base_dtype:",
|
|
1219
|
+
f" arr_{a.name} = {a.name}._data",
|
|
1220
|
+
" else:",
|
|
1221
|
+
f" arr_{a.name} = np.ascontiguousarray({a.name}, dtype=base_dtype)",
|
|
1222
|
+
]
|
|
1223
|
+
else:
|
|
1224
|
+
out_lines += [
|
|
1225
|
+
f" arr_{a.name} = np.ascontiguousarray({a.name}, dtype=base_dtype)",
|
|
1226
|
+
]
|
|
1227
|
+
if shape_expr is not None:
|
|
1228
|
+
expr_py = _expr_py(shape_expr)
|
|
1229
|
+
out_lines += [
|
|
1230
|
+
f" _expected_shape_{a.name} = tuple({expr_py})",
|
|
1231
|
+
f" if arr_{a.name}.shape != _expected_shape_{a.name}:",
|
|
1232
|
+
f" arr_{a.name} = np.reshape(arr_{a.name}, _expected_shape_{a.name})",
|
|
1233
|
+
]
|
|
1234
|
+
out_lines += [
|
|
987
1235
|
f" ptr_{a.name} = _self._ffi.cast('{a.base_type} *', _self._ffi.from_buffer(arr_{a.name}))",
|
|
988
1236
|
]
|
|
989
1237
|
arg_call_args[idx] = f"ptr_{a.name}"
|
|
990
1238
|
output_names.append(a.name)
|
|
1239
|
+
if a.base_type in struct_names:
|
|
1240
|
+
output_conditions[a.name] = f"_ret_{a.name}"
|
|
1241
|
+
struct_output_types[a.name] = a.base_type
|
|
991
1242
|
if a.name in pointer_scalar_names:
|
|
992
1243
|
pointer_scalar_outputs.append((a.name, f"arr_{a.name}"))
|
|
993
1244
|
else:
|
|
@@ -1014,6 +1265,7 @@ class CModule:
|
|
|
1014
1265
|
]
|
|
1015
1266
|
else:
|
|
1016
1267
|
out_lines += [
|
|
1268
|
+
f" _ret_{a.name} = {a.name} is None",
|
|
1017
1269
|
f" if {a.name} is None:",
|
|
1018
1270
|
f" obj_{a.name} = _struct_classes['{a.base_type}']()",
|
|
1019
1271
|
f" arr_{a.name} = obj_{a.name}._data",
|
|
@@ -1026,6 +1278,7 @@ class CModule:
|
|
|
1026
1278
|
f" ptr_{a.name} = _self._ffi.cast('{a.base_type} *', _self._ffi.from_buffer(arr_{a.name}))",
|
|
1027
1279
|
]
|
|
1028
1280
|
output_names.append(a.name)
|
|
1281
|
+
output_conditions[a.name] = f"_ret_{a.name}"
|
|
1029
1282
|
output_vars.append(f"obj_{a.name} if obj_{a.name} is not None else arr_{a.name}")
|
|
1030
1283
|
arg_call_args[idx] = f"ptr_{a.name}"
|
|
1031
1284
|
elif a.is_scalar and not a.is_length_param and arg_call_args[idx] is None:
|
|
@@ -1061,6 +1314,7 @@ class CModule:
|
|
|
1061
1314
|
pairs_sorted = sorted(pairs, key=lambda p: 1 if p[1] in pointer_scalar_map else 0)
|
|
1062
1315
|
output_names_reordered: list[str] = []
|
|
1063
1316
|
output_vars_final: list[str] = []
|
|
1317
|
+
output_conditions_final: list[str | None] = []
|
|
1064
1318
|
for name, ov in pairs_sorted:
|
|
1065
1319
|
output_names_reordered.append(name)
|
|
1066
1320
|
if ov in struct_scalar_map:
|
|
@@ -1070,11 +1324,32 @@ class CModule:
|
|
|
1070
1324
|
output_vars_final.append(pointer_scalar_map[ov][1])
|
|
1071
1325
|
else:
|
|
1072
1326
|
output_vars_final.append(ov)
|
|
1327
|
+
output_conditions_final.append(output_conditions.get(name))
|
|
1073
1328
|
|
|
1074
1329
|
own_return_len_expr = None
|
|
1075
1330
|
if own_return and contract_map.get("return"):
|
|
1076
1331
|
own_return_len_expr = _expr_py(contract_map["return"])
|
|
1077
1332
|
|
|
1333
|
+
output_value_exprs = list(output_vars_final)
|
|
1334
|
+
if output_value_exprs:
|
|
1335
|
+
scalar_names = {n for n, _ in pointer_scalar_outputs}
|
|
1336
|
+
for idx, (out_name, out_var) in enumerate(
|
|
1337
|
+
zip(output_names_reordered, output_value_exprs)
|
|
1338
|
+
):
|
|
1339
|
+
if out_name in post_contract_map:
|
|
1340
|
+
expr_py = _expr_py(post_contract_map[out_name])
|
|
1341
|
+
if out_name in scalar_names:
|
|
1342
|
+
expr_py = expr_py.replace(out_name, f"scalar_{out_name}")
|
|
1343
|
+
output_value_exprs[idx] = f"({out_var})[:int({expr_py})]"
|
|
1344
|
+
elif out_name in post_shape_contract_map:
|
|
1345
|
+
expr_py = _expr_py(post_shape_contract_map[out_name])
|
|
1346
|
+
output_value_exprs[idx] = f"np.reshape({out_var}, tuple({expr_py}))"
|
|
1347
|
+
if out_name in struct_output_types:
|
|
1348
|
+
output_value_exprs[idx] = (
|
|
1349
|
+
f"_self._wrap_struct_output({output_value_exprs[idx]}, "
|
|
1350
|
+
f"'{struct_output_types[out_name]}')"
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1078
1353
|
if own_return:
|
|
1079
1354
|
base_ret = base_type_from_ctype(fspec.return_ctype)
|
|
1080
1355
|
ret_dtype_expr = f"np.dtype('{np.dtype(numpy_dtype_for_base_type(base_ret)).name}')"
|
|
@@ -1105,77 +1380,60 @@ class CModule:
|
|
|
1105
1380
|
lines.append(
|
|
1106
1381
|
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
1382
|
)
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1383
|
+
lines.append(" _ret_vals = []")
|
|
1384
|
+
lines.append(" _ret_names = set()")
|
|
1385
|
+
if output_value_exprs:
|
|
1386
|
+
for out_name, ov, cond in zip(
|
|
1387
|
+
output_names_reordered, output_value_exprs, output_conditions_final
|
|
1388
|
+
):
|
|
1389
|
+
if cond:
|
|
1390
|
+
lines.append(f" if {cond}:")
|
|
1391
|
+
lines.append(f" _ret_vals.append({ov})")
|
|
1392
|
+
lines.append(f" _ret_names.add('{out_name}')")
|
|
1393
|
+
else:
|
|
1394
|
+
lines.append(f" _ret_vals.append({ov})")
|
|
1395
|
+
lines.append(f" _ret_names.add('{out_name}')")
|
|
1396
|
+
lines.append(" ret_parts = [arr_ret]")
|
|
1397
|
+
lines.append(" if _ret_vals:")
|
|
1398
|
+
lines.append(" ret_parts.extend(_ret_vals)")
|
|
1114
1399
|
for name, _arr in pointer_scalar_outputs:
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
if len(ret_parts) == 1:
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
ret_expr = "(" + ", ".join(ret_parts) + ")"
|
|
1122
|
-
lines.append(f" return {ret_expr}")
|
|
1400
|
+
lines.append(f" if '{name}' not in _ret_names:")
|
|
1401
|
+
lines.append(f" ret_parts.append(scalar_{name})")
|
|
1402
|
+
lines.append(f" _ret_names.add('{name}')")
|
|
1403
|
+
lines.append(" if len(ret_parts) == 1:")
|
|
1404
|
+
lines.append(" return ret_parts[0]")
|
|
1405
|
+
lines.append(" return tuple(ret_parts)")
|
|
1123
1406
|
return "\n".join(lines)
|
|
1124
1407
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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})]")
|
|
1144
|
-
if 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
|
-
]
|
|
1150
|
-
if isinstance(ret_expr, str) and ret_expr.startswith("("):
|
|
1151
|
-
ret_expr = (
|
|
1152
|
-
ret_expr[:-1]
|
|
1153
|
-
+ (", " if len(scalars) else "")
|
|
1154
|
-
+ ", ".join(scalars)
|
|
1155
|
-
+ ")"
|
|
1156
|
-
)
|
|
1408
|
+
lines.append(" _ret_vals = []")
|
|
1409
|
+
lines.append(" _ret_names = set()")
|
|
1410
|
+
if output_value_exprs:
|
|
1411
|
+
for out_name, ov, cond in zip(
|
|
1412
|
+
output_names_reordered, output_value_exprs, output_conditions_final
|
|
1413
|
+
):
|
|
1414
|
+
if cond:
|
|
1415
|
+
lines.append(f" if {cond}:")
|
|
1416
|
+
lines.append(f" _ret_vals.append({ov})")
|
|
1417
|
+
lines.append(f" _ret_names.add('{out_name}')")
|
|
1157
1418
|
else:
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
lines.append(
|
|
1419
|
+
lines.append(f" _ret_vals.append({ov})")
|
|
1420
|
+
lines.append(f" _ret_names.add('{out_name}')")
|
|
1421
|
+
for name, _arr in pointer_scalar_outputs:
|
|
1422
|
+
lines.append(f" if '{name}' not in _ret_names:")
|
|
1423
|
+
lines.append(f" _ret_vals.append(scalar_{name})")
|
|
1424
|
+
lines.append(f" _ret_names.add('{name}')")
|
|
1425
|
+
if ret_type == "void":
|
|
1426
|
+
lines.append(" if _ret_vals:")
|
|
1427
|
+
lines.append(" if len(_ret_vals) == 1:")
|
|
1428
|
+
lines.append(" return _ret_vals[0]")
|
|
1429
|
+
lines.append(" return tuple(_ret_vals)")
|
|
1430
|
+
lines.append(" return None")
|
|
1167
1431
|
else:
|
|
1168
|
-
if
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
scalars = [f"scalar_{n}" for n, _ in pointer_scalar_outputs]
|
|
1174
|
-
if len(scalars) == 1:
|
|
1175
|
-
ret_expr = f"({ret_expr}, {scalars[0]})"
|
|
1176
|
-
else:
|
|
1177
|
-
ret_expr = f"({ret_expr}, " + ", ".join(scalars) + ")"
|
|
1178
|
-
lines.append(f" return {ret_expr}")
|
|
1432
|
+
lines.append(" if _ret_vals:")
|
|
1433
|
+
lines.append(" if len(_ret_vals) == 1:")
|
|
1434
|
+
lines.append(" return _ret_vals[0], res")
|
|
1435
|
+
lines.append(" return tuple(_ret_vals), res")
|
|
1436
|
+
lines.append(" return res")
|
|
1179
1437
|
|
|
1180
1438
|
return "\n".join(lines)
|
|
1181
1439
|
|
|
@@ -240,7 +240,8 @@ def _parse_functions_regex(cdef: str) -> dict[str, FuncSpec]:
|
|
|
240
240
|
decls.append(decl)
|
|
241
241
|
buff = []
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
# allow pointer-return functions where the * may be glued to the name
|
|
244
|
+
func_re = re.compile(r"(.+?)\s+(\*?\w+)\s*\((.*?)\)\s*;")
|
|
244
245
|
|
|
245
246
|
funcs: dict[str, FuncSpec] = {}
|
|
246
247
|
for decl in decls:
|
|
@@ -249,6 +250,12 @@ def _parse_functions_regex(cdef: str) -> dict[str, FuncSpec]:
|
|
|
249
250
|
continue
|
|
250
251
|
ret_ctype, fname, arglist = m.groups()
|
|
251
252
|
ret_ctype = ret_ctype.strip()
|
|
253
|
+
fname = fname.strip()
|
|
254
|
+
# handle prototypes like `double *fn(...)` where `*` attaches to the name
|
|
255
|
+
if fname.startswith("*"):
|
|
256
|
+
star_count = len(fname) - len(fname.lstrip("*"))
|
|
257
|
+
fname = fname.lstrip("*")
|
|
258
|
+
ret_ctype = f"{ret_ctype} {'*' * star_count}".strip()
|
|
252
259
|
arglist = arglist.strip()
|
|
253
260
|
|
|
254
261
|
argspecs: list[ArgSpec] = []
|
|
@@ -305,6 +312,9 @@ def _parse_structs_regex(cdef: str) -> dict[str, StructSpec]:
|
|
|
305
312
|
name = m.group("name")
|
|
306
313
|
fields: list[StructField] = []
|
|
307
314
|
body_clean = re.sub(r"\bstruct\b", "", body)
|
|
315
|
+
# Strip C++-style comments before splitting fields so inline comments
|
|
316
|
+
# don't swallow subsequent declarations (e.g., "double x; // comment\n double y;").
|
|
317
|
+
body_clean = re.sub(r"//.*?$", "", body_clean, flags=re.MULTILINE)
|
|
308
318
|
for line in body_clean.split(";"):
|
|
309
319
|
line = line.strip()
|
|
310
320
|
if not line:
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tinycwrap
|
|
3
|
+
Version: 0.1.1
|
|
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 is a lightweight helper around CFFI that:
|
|
14
|
+
|
|
15
|
+
- compiles one or more C sources into a shared library,
|
|
16
|
+
- auto-generates the `cdef` from your function/struct declarations,
|
|
17
|
+
- builds NumPy-friendly Python wrappers that take/return arrays and structs,
|
|
18
|
+
- optionally hot-reloads when the C file changes (inside IPython).
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install tinycwrap
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Write a small C file (only non-`static` functions are exported):
|
|
27
|
+
|
|
28
|
+
```c
|
|
29
|
+
/* kernels.c */
|
|
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
|
+
*/
|
|
34
|
+
{
|
|
35
|
+
double acc = 0.0;
|
|
36
|
+
for (int i = 0; i < len_x; ++i)
|
|
37
|
+
acc += x[i] * y[i];
|
|
38
|
+
return acc;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Wrap and call it from Python:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import numpy as np
|
|
46
|
+
from tinycwrap import CModule
|
|
47
|
+
|
|
48
|
+
cm = CModule("kernels.c") # builds the shared library, creates wrappers
|
|
49
|
+
|
|
50
|
+
x = np.arange(5, dtype=np.float64)
|
|
51
|
+
y = np.ones_like(x)
|
|
52
|
+
|
|
53
|
+
cm.dot(x, y) # -> 10.0, len_x auto-filled by the contract
|
|
54
|
+
print(cm.dot.__doc__) # docstring comes from the C comment
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See `examples/` and `tests/` for more involved cases.
|
|
58
|
+
|
|
59
|
+
## C coding conventions
|
|
60
|
+
|
|
61
|
+
TinyCWrap infers how to build wrappers from simple C conventions:
|
|
62
|
+
|
|
63
|
+
- **Inputs vs outputs**
|
|
64
|
+
- `const T *arg` -> input NumPy array.
|
|
65
|
+
- `T *arg` -> output/in-place array (prefer naming it `out_*` when possible).
|
|
66
|
+
- `T arg[N]` in the signature -> fixed-size array (input if `const`, otherwise output).
|
|
67
|
+
- Plain scalars (`double`, `int`, ...) -> Python scalars.
|
|
68
|
+
- **Length parameters**: integers such as `len_x`, `n`, `size_x` can be auto-filled if you declare a contract (see below). Otherwise pass them explicitly from Python.
|
|
69
|
+
- **Docstrings**: the block comment immediately after the function header becomes the Python docstring.
|
|
70
|
+
- **Structs**: `typedef struct { ... } Name;` definitions in your headers/sources become Python classes with a `.dtype` and NumPy-backed storage.
|
|
71
|
+
- **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.
|
|
72
|
+
|
|
73
|
+
## Contracts: tell the wrapper how to size things
|
|
74
|
+
|
|
75
|
+
Contracts are declared inside the doc comment with `Contract:` (or `Contracts:`). Separate multiple rules with semicolons. Supported forms:
|
|
76
|
+
|
|
77
|
+
- `len_x=len(x)` — mark `len_x` as the length of array `x`. If you omit `len_x` in Python, the wrapper fills it.
|
|
78
|
+
- `shape(out)=n,m` — allocate `out` with shape `(n, m)`. You can define `n,m=shape(a)` to capture the shape of an input first.
|
|
79
|
+
- `len(out)=len_x` — allocate a 1D output array.
|
|
80
|
+
- `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
|
+
- `own(return)` — the returned pointer is owned by the caller; the wrapper will `free` it after copying to NumPy (requires a `len(return)=...` contract).
|
|
82
|
+
|
|
83
|
+
Examples pulled from the test suite:
|
|
84
|
+
|
|
85
|
+
```c
|
|
86
|
+
void mat_add(const double *a, const double *b, int n, int m, double *out)
|
|
87
|
+
/* Elementwise addition
|
|
88
|
+
Contract: n,m=shape(a); shape(out)=n,m;
|
|
89
|
+
*/
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```c
|
|
93
|
+
void merge_sorted(const double *a, const double *b, int len_a, int len_b,
|
|
94
|
+
double *out, int *out_len)
|
|
95
|
+
/* Merge unique sorted values
|
|
96
|
+
Contract: len_a=len(a); len_b=len(b);
|
|
97
|
+
len(out)=len_a+len_b; postlen(out)=out_len;
|
|
98
|
+
*/
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```c
|
|
102
|
+
double *alloc_random_array(int *out_len)
|
|
103
|
+
/* Return a freshly malloc'ed array
|
|
104
|
+
Contract: len(return)=out_len; own(return);
|
|
105
|
+
*/
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Rules are parsed case-insensitively; whitespace does not matter.
|
|
109
|
+
|
|
110
|
+
## Python wrapper behavior
|
|
111
|
+
|
|
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).
|
|
113
|
+
- **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 from the contract is evaluated.
|
|
115
|
+
- **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
|
+
- **Scalar pointer outputs**: pointers to integer-like types (e.g., `int *out_len`) are returned as plain Python integers alongside other outputs.
|
|
117
|
+
- **Structs**: for `typedef struct` declarations TinyCWrap generates a Python class:
|
|
118
|
+
- fields are accessible as properties backed by a NumPy structured dtype (`Name.dtype`);
|
|
119
|
+
- scalar struct outputs return `Name` objects;
|
|
120
|
+
- struct-array outputs return compact `NameArray` wrappers, with field arrays as properties (`points.x`), scalar items as `Name` objects (`points[0]`), and Python construction as `NameArray(array_like)` or `NameArray(x=..., y=...)`;
|
|
121
|
+
- pointer fields paired with `len_<field>` automatically allocate NumPy arrays when you instantiate the struct with `len_field` or when you pass an array for that field;
|
|
122
|
+
- `_data` holds the underlying structured array, `Name.zeros(n)` returns a raw array of that dtype, and `NameArray.zeros(n)` returns the typed wrapper.
|
|
123
|
+
- **Reloading**: inside IPython, pass `reload=True` (default) to auto-recompile before each cell if the C sources changed.
|
|
124
|
+
|
|
125
|
+
## Worked examples
|
|
126
|
+
|
|
127
|
+
### Two outputs and automatic lengths
|
|
128
|
+
|
|
129
|
+
```c
|
|
130
|
+
void split_vectors(const double *inp, int len_inp,
|
|
131
|
+
double *out_even, double *out_odd)
|
|
132
|
+
/* Split even/odd elements
|
|
133
|
+
Contract: len_inp=len(inp); len(out_even)=len_inp/2; len(out_odd)=len_inp/2;
|
|
134
|
+
*/
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
data = np.array([0, 1, 2, 3, 4, 5], dtype=np.float64)
|
|
139
|
+
out_even, out_odd = cm.split_vectors(data) # lengths inferred, arrays allocated
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Structs and struct arrays
|
|
143
|
+
|
|
144
|
+
```c
|
|
145
|
+
typedef struct {
|
|
146
|
+
double real;
|
|
147
|
+
double imag;
|
|
148
|
+
} ComplexPair;
|
|
149
|
+
|
|
150
|
+
double complex_magnitude(const ComplexPair *z);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
cp = cm.ComplexPair(real=3.0, imag=4.0)
|
|
155
|
+
cm.complex_magnitude(cp) # -> 5.0
|
|
156
|
+
|
|
157
|
+
# struct arrays are NumPy dtypes; useful when C expects pointers to arrays of structs
|
|
158
|
+
pairs = cm.ComplexPair.zeros(2)
|
|
159
|
+
pairs["real"] = [1.0, 2.0]
|
|
160
|
+
pairs["imag"] = [0.0, -1.0]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Owned return value
|
|
164
|
+
|
|
165
|
+
```c
|
|
166
|
+
double *alloc_random_array(int *out_len)
|
|
167
|
+
/* Contract: len(return)=out_len; own(return); */
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
arr, n = cm.alloc_random_array() # returns NumPy array, frees the C buffer
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Tips
|
|
175
|
+
|
|
176
|
+
- 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.
|
|
178
|
+
- Use `cm.<func>.__source__` to inspect the wrapper code if something behaves unexpectedly.
|
|
179
|
+
- For debugging parsed signatures/contracts call `cm._debug_specs()`.
|
|
180
|
+
|
|
181
|
+
That is all you need to start turning small C helpers into ergonomic Python callables.
|
tinycwrap-0.0.7/PKG-INFO
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: tinycwrap
|
|
3
|
-
Version: 0.0.7
|
|
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.
|
tinycwrap-0.0.7/README.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: tinycwrap
|
|
3
|
-
Version: 0.0.7
|
|
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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|