Design Decisions
This page documents what c2py23 does and does not do, with references to the GitHub issues where each decision was made.
What c2py23 Does
- Zero-copy buffer access -- acquires raw C pointers from Python buffer objects via PEP 3118, with Python 2.7 old-buffer fallback
- One
.soeverywhere -- usesdlopen(NULL)+dlsym()(the nimpy trick) rather than linking-lpython. No#include <Python.h>anywhere. For runtimes where this fails (GraalPy) or for debugging, use--pythonhinstead (docs). - Format dispatch -- route to different C overloads based on PEP 3118 format
characters:
'd'(float64),'f'(float32),'i'(int32), etc. - CPU feature dispatch -- compile the same C source with different
-mflags and select at runtime viawhen: c2py_amd64_avx2 - Auto-derived array checks --
const double gv[][3]in a C sig generates ndim, shape, and contiguity checks automatically - Auto-generated docstrings -- Python
help()shows sigs, params, checks, and overloads - GIL release -- opt-in per-function
gil_release: true - Free-threading -- module-level
free_threading: truefor Python 3.14t
What c2py23 Does NOT Do
No keyword arguments (#44)
C99 has no keyword arguments -- function parameters are positional, matched by order at the call site. c2py23 maps directly to the C calling model, so all wrapped functions are positional-only.
C99 does have <stdarg.h> for variadic functions, which is the C equivalent
of optional arguments. c2py23 supports this pattern through default values
on positional parameters (py_sig: "fn(a: buffer, n: int = 1) -> int"),
but callers must pass arguments in order: fn(data, 5) works, fn(data, n=5)
raises TypeError.
Keyword argument support would also introduce METH_KEYWORDS in the generated
CPython wrappers, which causes undefined behavior when cast to PyCFunction.
Deferred until Python 2.7 is dropped and the minimum can be raised to 3.12
where METH_FASTCALL avoids this issue natively.
No memory allocation in wrappers
Generated wrapper code never calls malloc, calloc, realloc, or free.
All memory is owned by Python. User C code may allocate internally, but must
free before returning.
No complex type (#53)
C99 _Complex and C++ std::complex<T> have incompatible ABIs and
different calling conventions across compilers. gcc/clang return C99
complex values via SSE registers; MSVC does not support C99 _Complex
at all and uses a C++ struct convention that passes via pointer.
The interleaved real/imag layout (AoS) also complicates SIMD vectorization -- most SIMD kernels prefer separate real and imaginary arrays (SoA) or custom layouts.
Users handle complex data in their C wrapper code with plain float*
or double* buffers and interleaved pairs. The kissfft example
demonstrates this: declare buffer with format 'f', validate
buf.n % 2 == 0, and cast to (kiss_fft_cpx*)buf.ptr in the C
wrapper. For SIMD-friendly layouts, split into separate real and
imaginary arrays (SoA or structure-of-arrays) in your C code.
This mirrors the Python 2 + Python 3 subset approach: where C99 and C++ conflict, c2py23 uses the common-denominator feature set that all platforms agree on.
GPU support (#40)
Under investigation. For CPU arrays, c2py23 already gets a raw pointer
via PEP 3118 at zero cost -- C function pointer dispatch via the
tp_as_buffer slot, no allocation on repeated calls, no dict lookup,
no release path.
For GPU arrays, DLPack provides the mechanism: a PyCapsule containing
a device pointer, device type, shape, and strides. NumPy's C source
shows __dlpack__() returns the same PyArray_DATA() pointer as
getbuffer for CPU arrays (device_type = kDLCPU), and device-typed
pointers for GPU tensors from CuPy, PyTorch, etc.
The open question is not buffer access -- it's whether you have a C99 function compiled for a GPU to call. All GPU libraries (cuBLAS, cuFFT, ROCm) require JIT compilation or pre-compiled device code. Until there is a GPU-compiled C function to wrap, DLPack vs PEP 3118 is a moot point.
Deferred for future research. Related: HPy (#49).
No async/await (#41)
Deferred until the Python 2.7 compatibility floor is raised (coroutines are 3.5+).
No named-tuple returns (#42)
Functions return positional tuples. Wrap in a namedtuple on the Python side if needed.
No numpy dependency (#15)
c2py23 uses PEP 3118 directly. buf.contiguous ('C'/'F') was removed
as a numpy-ism. Use buf.strides and buf.slow_axis / buf.fast_axis
instead.
No keyword arguments in dispatch expressions
when: and checks: expressions are compiled to C, not Python. They
support a limited grammar: comparisons, arithmetic, and/or/not,
attribute access (buf.n, buf.format), and subscript (buf.shape[1]).
Alternative Python Wrapper Generators
c2py23 is deliberately incomplete and inflexible -- it maps a narrow set of C99 function signatures onto Python buffers, with no copies, no allocations, and no type conversion overhead. Most alternatives below aim for broader coverage.
- ctypes -- built into Python, no build step, but no buffer-protocol shape/format checking
- cffi -- richer C type system, ABI and API modes, but Python 2.7 is sunsetting
- pybind11 -- C++ native, rich type conversion, Python 3.6+ only
- Cython -- Python-like syntax for C extensions, Python 2.7-3.x support
- f2py -- wraps Fortran 77/90 and C functions, generates C/API wrappers, numpy-aware
- SWIG -- multi-language wrapper generator (C/C++ to Python, Java, Ruby, etc.), mature and feature-rich
- HPy -- universal C extension API for CPython, PyPy, and GraalPy. HPy has first-class buffer support and is a serious option for multi-interpreter portability. See #49.