Generate zero-copy C99 Python extensions from declarative YAML interface files. Use when wrapping C functions for Python, creating high-performance numerical extensions, binding SIMD-optimized C code, or when the user mentions c2py23, C extensions, or buffer-protocol wrapping. Supports Python 2.7-3.15, GIL release, free-threading, and automatic buffer shape/type dispatch.
Resources
11Install
npx skillscat add jonwright/c2py23 Install via the SkillsCat registry.
c2py23 Skill
What c2py23 Does
c2py23 transpiles declarative .c2py interface files into CPython C extensions. It is designed for high-performance numerical code where Python owns all memory and C functions operate on buffers passed in by the caller. The wrapper is zero-copy and allocation-free.
Key capabilities:
- Wraps C functions with zero copy (buffer protocol, no intermediate allocations)
- Automatic dispatch based on buffer properties (element type, dimensionality, contiguity)
- GIL release for pure-C sections (
gil_release: true) - Free-threading support for Python 3.14t+ (
free_threading: true) - Per-function performance timing instrumentation (
timing: true) - Template expansion for generating families of functions (
expand:) - Works on Python 2.7 through 3.15 from a single
.sofile
When to Use
- Wrapping C numerical kernels (FFT, linear algebra, signal processing)
- Performance-critical loops that benefit from GIL release
- Code that must work on Python 2.7 AND modern Python (HPC, legacy systems)
- SIMD dispatch from Python (select fastest code path at runtime)
- Hard real-time or low-latency use cases (zero-copy, no allocation in hot path)
When NOT to Use
- Wrapping object-oriented C++ libraries (use pybind11 or nanobind)
- Wrapping code that allocates memory and returns it to Python (use ctypes or cffi)
- Simple scalar-only functions (use ctypes)
- Code that requires Python keyword arguments (c2py23 is positional-only)
- Async/await patterns (not supported)
Quick Start
1. Write a .c2py interface file
module: mymod
source: [kernel.c]
functions:
- py_sig: "add_arrays(a: buffer, b: buffer, out: buffer)"
checks:
- "a.format == 'd'"
- "b.format == 'd'"
- "out.format == 'd'"
- "a.n == b.n"
- "out.n >= a.n"
c_overloads:
- sig: "void add_double(int n, const double *a, const double *b, double *out)"
map:
a: "a.ptr"
b: "b.ptr"
out: "out.ptr"
n: "min(a.n, b.n)"2. Build the module
c2py23 build mymod.c2pyThis produces mymod.so (or .pyd on Windows).
3. Use from Python
import ctypes
import mymod
a = (ctypes.c_double * 1000)(*range(1000))
b = (ctypes.c_double * 1000)(*[1.0] * 1000)
out = (ctypes.c_double * 1000)()
mymod.add_arrays(a, b, out)Critical Rules
1. Always validate buffer dimensions in checks:
Without size checks, a caller can pass a too-small output buffer, causing a segfault. This is the most important rule:
checks:
- "out.format == 'd'" # element type
- "out.n >= a.n" # output large enough (prevents segfaults!)2. Memory is owned by Python
The C function receives raw pointers with no bounds information. The ONLY defense against buffer overruns is the checks: block. There is no runtime bounds checking in the C code.
3. Format char portability
Never use 'l' or 'L' for fixed-width dispatch. They are platform-sized (sizeof(long) differs between Linux and Windows). Use:
| Format | C type | Size |
|---|---|---|
'i' / 'I' |
int32_t / uint32_t |
4 bytes |
'q' / 'Q' |
int64_t / uint64_t |
8 bytes |
'd' |
double |
8 bytes |
'f' |
float |
4 bytes |
4. Python dict format preferred over YAML
The Python dict format requires no PyYAML dependency and auto-detects:
{
"module": "mymod",
"source": ["kernel.c"],
"functions": [
{
"py_sig": "add_arrays(a: bytes, b: bytes, out: bytes)",
"checks": ["a.format == 'd'", "out.n >= a.n"],
"c_overloads": [
{
"sig": "void add_double(int n, const double *a, const double *b, double *out)",
"map": {"a": "a.ptr", "b": "b.ptr", "out": "out.ptr", "n": "min(a.n, b.n)"},
},
],
},
],
}Dispatch System
c2py23 dispatches to the correct C function based on buffer properties at call time:
sig:declares the C function signature and required buffer propertieswhen:is a C expression evaluated per-overload (e.g.,"out.format == 'd'")map:binds Python parameter attributes to C function arguments- Overloads are tried in order; first match wins
- Use
default_raise:for a clear error when no overload matches
Buffer attributes available in map: and when: expressions:
| Attribute | Type | Meaning |
|---|---|---|
buf.ptr |
void* |
Pointer to buffer data |
buf.n |
Py_ssize_t |
Number of elements |
buf.itemsize |
Py_ssize_t |
Bytes per element |
buf.format |
char |
PEP 3118 format character |
buf.ndim |
int |
Number of dimensions |
buf.shape[i] |
Py_ssize_t |
Size of dimension i |
buf.strides[i] |
Py_ssize_t |
Stride of dimension i |
Common Pitfalls
- Missing size checks -- the #1 cause of segfaults. Always check
out.n >= in.nor similar. - Wrong format character --
'd'for double,'f'for float. Mis-match causes silent wrong results. - Using ctypes wrong -- ctypes arrays implement the buffer protocol on Python 2.7+, use them for test data.
- Map expressions that allocate --
map:is evaluated once per call, but generated code is allocation-free. Keep map expressions simple. - Forgetting to
sys.path.insertwhen running tests from example directories that import built.sofiles.
Building and Testing
pip install -e . # install c2py23 in dev mode
c2py23 build mymod.c2py # build a module
c2py23 build --asan mymod.c2py # build with Address Sanitizer
python -m pytest tests/ # run all tests
bash tests/run_tests.sh python3.12 # build + test for one Python version
python3 tests/test_all.py # cross-version container validationSpec and Examples
- Full specification: https://jonwright.github.io/c2py23/specification/
- Examples:
examples/directory in the repoexamples/threading_bench/-- GIL release + OpenMP benchmarkexamples/kissfft_wrap/-- FFT wrappingexamples/lz4_wrap/-- Compression wrappingexamples/simd_dispatch/-- Runtime SIMD dispatchexamples/wheel_demo/-- Packaging as wheels