Lightweight C-to-Python wrapper generator using CFFI and NumPy
Project description
tinycwrap
TinyCWrap is a lightweight helper around CFFI that:
- compiles one or more C sources into a shared library,
- auto-generates the
cdeffrom your function/struct declarations, - builds NumPy-friendly Python wrappers that take/return arrays and structs,
- optionally hot-reloads when the C file changes (inside IPython).
Quick start
pip install tinycwrap
Write a small C file (only non-static functions are exported):
/* kernels.c */
double dot(const double *x, const double *y, int len_x)
/* Return dot product between x and y */
{
double acc = 0.0;
for (int i = 0; i < len_x; ++i)
acc += x[i] * y[i];
return acc;
}
Wrap and call it from Python:
import numpy as np
from tinycwrap import CModule
cm = CModule("kernels.c") # builds the shared library, creates wrappers
x = np.arange(5, dtype=np.float64)
y = np.ones_like(x)
cm.dot(x, y) # -> 10.0, len_x auto-filled by convention
print(cm.dot.__doc__) # docstring comes from the C comment
See examples/ and tests/ for more involved cases.
C coding conventions
TinyCWrap infers how to build wrappers from simple C conventions:
- Inputs vs outputs
const T *arg-> input NumPy array.T *arg-> output/in-place array (prefer naming itout_*when possible).T arg[N]in the signature -> fixed-size array (input ifconst, otherwise output).- Plain scalars (
double,int, ...) -> Python scalars.
- Length parameters: integers named like
len_x,n_x, orsize_xare auto-filled from arrayxby convention. Otherwise pass them explicitly from Python or declare a contract (see below). - Docstrings: the block comment immediately after the function header becomes the Python docstring.
- Structs:
typedef struct { ... } Name;definitions in your headers/sources become Python classes with a.dtypeand NumPy-backed storage. - Compilation: extra sources can be passed (
CModule("main.c", "helper.c")), and extra include dirs viainclude_dirs=[...]. Default compiler flags are-O3 -shared -fPIC -march=native -mtune=nativeplus the NumPy include path.
Contracts: tell the wrapper how to size things
Contracts are declared inside the doc comment with Contract: (or Contracts:). Separate multiple rules with semicolons. Supported forms:
len_x=len(x)— marklen_xas the length of arrayx. If you omitlen_xin Python, the wrapper fills it.shape(out)=n,m— allocateoutwith shape(n, m). You can definen,m=shape(a)to capture the shape of an input first.len(out)=len_x— allocate a 1D output array.postlen(out)=out_len— slice the output after the call using an integer pointer resultout_len(useful when the C code writes less than the allocated length).own(return)— the returned pointer is owned by the caller; the wrapper willfreeit after copying to NumPy (requires alen(return)=...contract).expose(len_x)— opt out of naming-convention inference forlen_x, keeping it as a required Python argument.
TinyCWrap also infers low-risk contracts from names:
len_x,n_x, orsize_xare treated aslen(x)whenxis an array argument.out_xis allocated withshape(x)when input arrayxexists.out_xis allocated withlen_x,n_x, orsize_xwhen no inputxexists but such a length argument does.
Explicit contracts override inferred contracts.
Examples pulled from the test suite:
void mat_add(const double *a, const double *b, int n, int m, double *out)
/* Elementwise addition
Contract: n,m=shape(a); shape(out)=n,m;
*/
void merge_sorted(const double *a, const double *b, int len_a, int len_b,
double *out, int *out_len)
/* Merge unique sorted values
Contract: len_a=len(a); len_b=len(b);
len(out)=len_a+len_b; postlen(out)=out_len;
*/
double *alloc_random_array(int *out_len)
/* Return a freshly malloc'ed array
Contract: len(return)=out_len; own(return);
*/
Rules are parsed case-insensitively; whitespace does not matter.
Python wrapper behavior
- Automatic allocation: any
out_*argument defaults toNonein Python; TinyCWrap allocates it based on contracts, naming conventions, fixed array sizes, or by matching the shape of a related input (out_xmatchesxwhen no contract is present). - 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). - Optional length arguments: integer length parameters inferred from contracts or naming conventions default to
Nonein the wrapper signature. If you pass them, they are cast toint; if not, the inferred expression is evaluated. - Post contracts: when a contract uses
postlen(...)orpost shape(...), the wrapper slices/reshapes outputs after the C call using the values written by the C function. - Scalar pointer outputs: pointers to integer-like types (e.g.,
int *out_len) are returned as plain Python integers alongside other outputs. - Structs: for
typedef structdeclarations TinyCWrap generates a Python class:- fields are accessible as properties backed by a NumPy structured dtype (
Name.dtype); - scalar struct outputs return
Nameobjects; - struct-array outputs return compact
NameArraywrappers, with field arrays as properties (points.x), scalar items asNameobjects (points[0]), and Python construction asNameArray(array_like)orNameArray(x=..., y=...); - pointer fields paired with
len_<field>automatically allocate NumPy arrays when you instantiate the struct withlen_fieldor when you pass an array for that field; _dataholds the underlying structured array,Name.zeros(n)returns a raw array of that dtype, andNameArray.zeros(n)returns the typed wrapper.
- fields are accessible as properties backed by a NumPy structured dtype (
- Reloading: inside IPython, pass
reload=True(default) to auto-recompile before each cell if the C sources changed.
Worked examples
Two outputs and automatic lengths
void split_vectors(const double *inp, int len_inp,
double *out_even, double *out_odd)
/* Split even/odd elements
Contract: len_inp=len(inp); len(out_even)=len_inp/2; len(out_odd)=len_inp/2;
*/
data = np.array([0, 1, 2, 3, 4, 5], dtype=np.float64)
out_even, out_odd = cm.split_vectors(data) # lengths inferred, arrays allocated
Structs and struct arrays
typedef struct {
double real;
double imag;
} ComplexPair;
double complex_magnitude(const ComplexPair *z);
cp = cm.ComplexPair(real=3.0, imag=4.0)
cm.complex_magnitude(cp) # -> 5.0
# struct arrays are NumPy dtypes; useful when C expects pointers to arrays of structs
pairs = cm.ComplexPair.zeros(2)
pairs["real"] = [1.0, 2.0]
pairs["imag"] = [0.0, -1.0]
Owned return value
double *alloc_random_array(int *out_len)
/* Contract: len(return)=out_len; own(return); */
arr, n = cm.alloc_random_array() # returns NumPy array, frees the C buffer
Tips
- Keep function declarations (or headers) visible at the top level; TinyCWrap scans the C files and headers you pass.
- If a length cannot be inferred from a naming convention or contract, you must pass it explicitly from Python.
- Use
cm.<func>.__source__to inspect the wrapper code if something behaves unexpectedly. - For debugging parsed signatures/contracts call
cm._debug_specs().
That is all you need to start turning small C helpers into ergonomic Python callables.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tinycwrap-0.1.2.tar.gz.
File metadata
- Download URL: tinycwrap-0.1.2.tar.gz
- Upload date:
- Size: 26.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
89764c5da4f7349237a6fb8e2d1acaa2bfb96b3462e42db9454945e491683969
|
|
| MD5 |
6310fea5ffb5177b950e161779e2678d
|
|
| BLAKE2b-256 |
eeaed9847b4b73fe0504ecb4cff56fc318b017ed17ae2e35f3ab1701f737256a
|
File details
Details for the file tinycwrap-0.1.2-py3-none-any.whl.
File metadata
- Download URL: tinycwrap-0.1.2-py3-none-any.whl
- Upload date:
- Size: 21.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d4b56155de539356e5108e60bc24ef9f64fb08af1c308d2c7af0d5b09aaec7ad
|
|
| MD5 |
c2fc7d36ed10d7e26385c279642bd10a
|
|
| BLAKE2b-256 |
fe11bcfd4faf1d7bddc650af22af321b12a8c4733a9a72433a0223bb9a3ac1a3
|