tinycwrap 0.0.6__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.
@@ -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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinycwrap"
7
- version = "0.0.6"
7
+ version = "0.1.1"
8
8
  description = "Lightweight C-to-Python wrapper generator using CFFI and NumPy"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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]
@@ -88,10 +149,24 @@ def test_merge_sorted(cm):
88
149
  assert out_len_val == 4
89
150
 
90
151
 
152
+ def test_owned_array(cm):
153
+ arr, n = cm.alloc_random_array()
154
+ assert arr.shape[0] == n
155
+ assert n >= 3
156
+ assert np.all(arr[:n] >= 1.0)
157
+
158
+
91
159
  def test_struct_output_array(cm):
92
160
  n = 4
93
161
  particles = cm.Particle.zeros(n)
94
- out_particles = cm.make_particles(3.0, out_p=particles, len_p=n)
95
- np.testing.assert_array_equal(out_particles, particles)
162
+ res = cm.make_particles(3.0, out_p=particles, len_p=n)
163
+ assert res is None
96
164
  np.testing.assert_allclose(particles["pos"], np.ones((n, 3)))
97
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))
@@ -6,6 +6,6 @@ from .cmodule import CModule
6
6
  try:
7
7
  __version__ = version("tinycwrap")
8
8
  except PackageNotFoundError:
9
- __version__ = "0.0.6"
9
+ __version__ = "0.1.1"
10
10
 
11
11
  __all__ = ["CModule", "__version__"]