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)

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"]
)

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:

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.