How to Call C Code in Python with CFFI
This article explores how the C Foreign Function Interface
(cffi) enables Python applications to interact with
external C libraries. It covers the core architecture of
cffi, the distinctions between ABI and API access modes,
how data types are marshaled across language boundaries, and the
trade-offs between in-line and out-of-line execution models. By parsing
standard C declarations directly, cffi provides an
efficient, portable, and maintainable bridge between Python and native C
code.
The Foundation: Direct C Declarations
Traditional approaches to interfacing C with Python—such as
ctypes or writing manual CPython extension modules—require
either extensive manual struct recreation in pure Python or
boilerplate-heavy C code using the Python C API.
cffi simplifies this by using an internal C parser.
Developers pass standard C header code (types, function prototypes,
struct definitions, and macros) directly to the interface via
ffi.cdef().
from cffi import FFI
ffi = FFI()
ffi.cdef("""
int printf(const char *format, ...);
double sin(double x);
""")cffi parses this snippet, creates corresponding internal
representations, and automatically generates wrappers to marshal inputs
and outputs between Python and C without requiring custom
boilerplate.
Operating Modes: ABI vs. API
cffi operates across two primary dimensions: how it
accesses the library (ABI vs. API) and when it builds the bindings
(in-line vs. out-of-line).
ABI Mode (Application Binary Interface)
In ABI mode, cffi interacts directly with shared
libraries (.so, .dll, or .dylib)
at the binary level using dynamic loading tools like
dlopen:
C = ffi.dlopen("m") # Load standard C math library
result = C.sin(3.14159 / 2)- Pros: Does not require a C compiler on the host system at runtime or installation.
- Cons: Struct layout, macro expansions, and
typedefsizing must match the target platform exactly. Any mismatch in header assumptions leads to undefined behavior or segmentation faults.
API Mode (Application Programming Interface)
In API mode, cffi uses a C compiler to verify
definitions and generate a native C extension module. You define both
the declarations (cdef) and the C source code needed to
compile against the target library (set_source).
ffi.set_source(
"_math_example",
"""#include <math.h>""",
libraries=["m"]
)- Pros: Highly reliable. The host C compiler evaluates actual struct sizes, alignments, and preprocessor macros, eliminating errors caused by platform-specific ABI variations.
- Cons: Requires a functional C build environment during package installation.
Execution Models: In-line vs. Out-of-line
In-line Execution
In-line mode parses the definitions and compiles the code every time the Python script is imported or executed. While convenient for rapid prototyping and short development scripts, it introduces startup latency and requires compiler access each time the program runs (when using API mode).
Out-of-line Execution
Out-of-line mode compiles the bindings once during the project's
build process into a standalone .so or .pyd
module. The resulting module can be imported like any standard Python
module:
from _math_example import ffi, lib
result = lib.sin(1.57)This model is the recommended standard for production systems and packaging (such as building Python wheels), as end users do not need a C compiler if pre-built wheels are distributed.
Data Marshalling and Memory Management
cffi bridges the gap between managed Python objects and
raw C memory through dedicated conversion utilities:
- Type Allocation:
ffi.new("type", value)allocates C objects in memory. By default,cffimanages this memory through Python’s garbage collector, freeing the C memory once the wrapper object goes out of scope. - Pointers and Arrays: Direct access to native arrays and nested structs is handled using standard Python indexing and slicing.
- Casting and Buffers:
ffi.cast()allows reinterpreting memory addresses, andffi.buffer()exposes raw C memory blocks as standard Python buffer objects, avoiding unnecessary memory copies when passing large datasets.
Performance and PyPy Integration
While ctypes relies heavily on dynamic dispatch overhead
within CPython, cffi generates compact C wrappers in API
mode that compile into native machine code.
Furthermore, cffi was designed in tandem with the PyPy
JIT compiler. Under PyPy, calls made through cffi can often
be inlined directly into native assembly, effectively eliminating the
foreign function call overhead that typically penalizes cross-language
boundaries. Under standard CPython, out-of-line API mode performs
comparably to hand-crafted C extensions while maintaining a safer and
more readable codebase.