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
   Contract: len_x=len(x);
*/
{
    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 the contract
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 such as len_x, n, size_x can be auto-filled if you declare a contract (see below). Otherwise pass them explicitly from Python.
  • 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).

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, 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 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.
  • 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 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.1.tar.gz (25.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.1-py3-none-any.whl (20.4 kB view details)

Uploaded Python 3

File details

Details for the file tinycwrap-0.1.1.tar.gz.

File metadata

  • Download URL: tinycwrap-0.1.1.tar.gz
  • Upload date:
  • Size: 25.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.1.tar.gz
Algorithm Hash digest
SHA256 f4494592f8e7a2a489d45fe9e1ebf4586c25f65381bd8cc82ad266708696f785
MD5 780484e313b393648a3ea14da2b45a81
BLAKE2b-256 8e1a155772349e5a8a60bd1f01763725c75bb20345aa25ff9a53cc256bf7df77

See more details on using hashes here.

File details

Details for the file tinycwrap-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: tinycwrap-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 20.4 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b890d5416f0424b9a706509e4e31b9d7744b94516a40c062ecf6f3084959fd13
MD5 ebd03358a77b87e055d1a4258f0fac50
BLAKE2b-256 87c592a03121ea7142bec266ffe2124ceb20f4090e30f6a888db499cd8b05504

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