Skip to main content

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 cdef from 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 it out_* when possible).
    • T arg[N] in the signature -> fixed-size array (input if const, otherwise output).
    • Plain scalars (double, int, ...) -> Python scalars.
  • 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).
  • 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 .dtype and NumPy-backed storage.
  • 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.

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) — mark len_x as the length of array x. If you omit len_x in Python, the wrapper fills it.
  • 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.
  • len(out)=len_x — allocate a 1D output array.
  • 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).
  • own(return) — the returned pointer is owned by the caller; the wrapper will free it after copying to NumPy (requires a len(return)=... contract).
  • expose(len_x) — opt out of naming-convention inference for len_x, keeping it as a required Python argument.

TinyCWrap also infers low-risk contracts from names:

  • len_x, n_x, or size_x are treated as len(x) when x is an array argument.
  • out_x is allocated with shape(x) when input array x exists.
  • out_x is allocated with len_x, n_x, or size_x when no input x exists 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 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).
  • 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 None in the wrapper signature. If you pass them, they are cast to int; if not, the inferred expression is evaluated.
  • 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.
  • 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 struct declarations TinyCWrap generates a Python class:
    • fields are accessible as properties backed by a NumPy structured dtype (Name.dtype);
    • scalar struct outputs return Name objects;
    • 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=...);
    • 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;
    • _data holds the underlying structured array, Name.zeros(n) returns a raw array of that dtype, and NameArray.zeros(n) returns the typed wrapper.
  • 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tinycwrap-0.1.2.tar.gz (26.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tinycwrap-0.1.2-py3-none-any.whl (21.2 kB view details)

Uploaded Python 3

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

Hashes for tinycwrap-0.1.2.tar.gz
Algorithm Hash digest
SHA256 89764c5da4f7349237a6fb8e2d1acaa2bfb96b3462e42db9454945e491683969
MD5 6310fea5ffb5177b950e161779e2678d
BLAKE2b-256 eeaed9847b4b73fe0504ecb4cff56fc318b017ed17ae2e35f3ab1701f737256a

See more details on using hashes here.

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

Hashes for tinycwrap-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 d4b56155de539356e5108e60bc24ef9f64fb08af1c308d2c7af0d5b09aaec7ad
MD5 c2fc7d36ed10d7e26385c279642bd10a
BLAKE2b-256 fe11bcfd4faf1d7bddc650af22af321b12a8c4733a9a72433a0223bb9a3ac1a3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page