CFFI (C Foreign Function Interface for Python)
repository·main·Indexed 43 days ago
https://github.com/python-cffi/cffiA tool for calling C code from Python, enabling high-performance extensions and integration with existing C codebases. CFFI supports three modes: In-line ABI, Out-of-line ABI, and Out-of-line API. It provides utilities for declaring C types and functions via ffi.cdef(), loading libraries with ffi.dlopen(), and generating external modules using ffibuilder.set_source(). Version 2.2.0.dev0.
What's inside CFFI
- CFFI (C Foreign Function Interface) is a tool for calling C code from Python. It allows developers to interface Python with C libraries, enabling high-performance extensions and integration with existing C codebases.
Choose a CFFI mode for your wrapper module
mainCFFI provides three main ways to expose C libraries to Python, ranging in complexity and performance:
- In-line, ABI mode: The simplest method. You declare types and load the library directly in your main script using
ffi.dlopen(). Best for quick tasks or when you don't want a build step. - Out-of-line, ABI mode: Better for organization and faster import times. You use a separate build script to generate a Python module. The module contains the
ffiobject, but you still useffi.dlopen()to load the actual library. - Out-of-line, API mode: The most powerful and fastest method. It compiles a C extension module. This allows you to access the library at the C level rather than the binary level and provides the most flexibility.
import cffi # 1. In-line ABI mode ffi = cffi.FFI() ffi.cdef("C-like declarations") lib = ffi.dlopen("libpath") # 2. Out-of-line ABI mode (in a build script) ffibuilder = cffi.FFI() ffibuilder.set_source("package._foo", None) ffibuilder.cdef("C-like declarations") if __name__ == '__main__': ffibuilder.compile() # 3. Out-of-line API mode (in a build script) ffibuilder = cffi.FFI() ffibuilder.set_source("package._foo", r"""real C code""") ffibuilder.cdef("C-like declarations with '...' ") if __name__ == '__main__': ffibuilder.compile(verbose=True)- In-line, ABI mode: The simplest method. You declare types and load the library directly in your main script using
Manage thread safety when using CFFI
mainCFFI interacts with Python's threading model in specific ways that require careful handling of shared C data:
- GIL Behavior: On GIL-enabled builds, CFFI releases the GIL before calling into a C library. This allows for multithreaded speedups but means the GIL does not protect multithreaded shared use of C data structures.
- Thread-Safe C Libraries: If the underlying C library is thread-safe, no additional locking is required in Python.
- Non-Thread-Safe C Libraries: If the C library relies on global state or is not re-entrant, you must implement locking in Python (e.g., using
threading.Lock) to ensure only one thread accesses the library at a time. - Locking Strategies:
- Global Lock: Use a single
threading.Lockif the library is not re-entrant and uses global state. - Per-Object Lock: If the library is re-entrant but not thread-safe (i.e., threads can run simultaneously as long as they don't mutate the same object), use a lock associated with specific C objects.
- Non-blocking Acquire: To avoid deadlocks, use
l.acquire(blocking=False)to immediately raise an error if a lock is already held.
- Global Lock: Use a single
import threading l = threading.Lock() def work(): # Protect unsafe C calls with a lock l.acquire() try: lib.increment() finally: l.release() # Or using a context manager (preferred) def work_safe(): with l: lib.increment()Understand CFFI's automatic type conversion rules
mainCFFI follows an explicit design philosophy with very few automatic conversions.
- Integers: Various C integer types are automatically wrapped and unwrapped to regular Python integers.
- Characters vs. Strings: The
chartype may correspond to single-character strings. To ensure integer correspondence, usesigned charorunsigned charinstead. - Pointers to Strings:
const char *might automatically map to Python strings. If you require a pointer rather than a string, usechar *.
Understand the difference between the two FFI classes
mainCFFI provides two distinct types of
FFIobjects depending on how they are obtained:- The extended FFI class: Obtained via
import cffi; ffi = cffi.FFI(). This class includes extra methods required to prepare the FFI (used during the build phase, often referred to as anffibuilder). - The C-based FFI object: Obtained via
from package._foo import ffi(after compiling an out-of-line extension). This object is used for runtime interaction with C libraries.
Key differences for the C-based object:
- It is a type written in C; you cannot add arbitrary attributes to it.
- It does not have the underscore-prefixed internal Python attributes.
- The associated
libobjects are read-only (except for writes to global variables). - For accessing metadata, use
dir(lib)instead oflib.__dict__,lib.__class__,lib.__all__, orlib.__name__to ensure compatibility.
- The extended FFI class: Obtained via
Use multiple CFFI-made DLLs in one process
mainA single process can load multiple CFFI-made DLLs. However, be aware that all CFFI-made DLLs in a process share a single Python interpreter.
This means they share the same global state. If different DLLs include libraries that perform monkey-patching on the Python standard library, they may affect each other unexpectedly.
Use ellipsis (...) in cdef() to let the C compiler fill in details
mainWhen using C compiler (API mode), you can use the ellipsis
...syntax incdef()to allow the C compiler to resolve specific details like field offsets, exact integer sizes, or array lengths. This is useful when you don't want to manually specify every detail of a C structure or type.Supported ellipsis usages:
- Partial Structures/Unions: Use
struct { ...; }orunion { ...; }. The...;as the last field allows fields to be undeclared, out of order, or use non-standard alignment. Note: You can only access fields you explicitly declare. - Integer Types: Use
typedef int... foo_t;(orlong...,unsigned long long..., etc.). The compiler determines the exact size and signedness. This maps to a standard Python integer type. - Floating-Point Types: Use
typedef float... foo_t;ortypedef double... foo_t;. The compiler determines if it is afloatordouble. Note:long doubleis not supported via this syntax and must be declared explicitly. - Opaque Types: Use
typedef ... foo_t;to declare a type as opaque. This is useful for pointers where you don't need to inspect the internal structure. This only works for struct-like types, not primitive integers. - Arrays: Use
extern int n[...];for arrays with unspecified lengths. This supports multidimensional arrays likeextern int n[...][...];. In version 1.2+, you can useextern int m[][...];where the outermost dimension is unknown to the compiler but inner dimensions are known. - Enums: Use
enum foo { A, B, C, ... };to let the compiler determine the exact values and order of constants. - Macros/Constants: Use
#define FOO ...incdef()for integer constants. Alternatively, usestatic const int FOO;for more general types.
Limitations
- Misdeclaring simple integer/float function arguments (e.g., declaring
longasint) works because the C compiler performs the cast, but this does not work for function pointers or variadic functions. - You cannot misdeclare complex types like pointers (e.g.,
int *aslong *) or global arrays.
// Example of partial struct struct { int a; ...; }; // Example of opaque type typedef ... foo_t; // Example of integer type detection typedef int... foo_t; // Example of array with compiler-filled length extern int n[...];- Partial Structures/Unions: Use
Compare `cdata` primitive types by value
mainIn CFFI v1.10,cdataobjects of primitive types (integers, floats, chars) are compared and ordered by their actual value rather than their identity. For example,<cdata 'int' 42>will compare equal to the Python integer42.Understand the design principles of CFFI
mainCFFI is designed to call existing C libraries from Python based on several core principles:
- Minimal Learning Curve: The interface is based on
LuaJIT's FFI. It aims to allow users to call C code using only knowledge of C and Python, avoiding the need for domain-specific languages like Cython or SWIG. - Python-Centric Logic: Most logic is kept in Python to minimize the amount of C code a user needs to write compared to native C extensions.
- API vs. ABI approach:
- API Level (Preferred): You write C declarations, and the C compiler is called to validate and link to those constructs. This is the recommended way to work.
- ABI Level: You can work at the Application Binary Interface level (similar to
ctypes), but this is less robust on non-Windows platforms where C libraries may have a specified API but not a stable ABI.
- C Support: CFFI aims for completeness, supporting C89 constructs and macros. Note that some C99 constructs may not be supported. If a macro is complex or 'abused', you can manually wrap it in a saner C function.
- No C++ Support: CFFI does not support C++. To use C++ code, you should write a C wrapper around the C++ code and then call that C API via CFFI.
- Not for Embedding C: CFFI is intended for calling existing C libraries, not for embedding executable C code within Python (unlike
Weave).
- Minimal Learning Curve: The interface is based on
Understand multithreading behavior in CFFI
mainCFFI multithreading relies on Python's standard Global Interpreter Lock (GIL):
- Initialization: If two threads attempt to call a C function before Python is initialized, locking occurs. One thread performs initialization while the other blocks. Once initialization is complete, the second thread proceeds.
- Multiple DLLs: If threads call functions from different CFFI-made DLLs, the Python initialization itself is still serialized, but the specific initialization-time Python code for each DLL can run without waiting for the other's specific logic to complete.
- Execution: Once initialized, the GIL ensures that only one thread can execute Python code at a time per process.
Understand the internal structure of CPython C extension modules produced by recompile()
mainWhen using
recompile()to produce a CPython C extension module, the module contains internal structures used to lazily initialize types and functions.Type Descriptors
Types are stored in the global array
_cffi_types[]. Initially, entries are odd integers acting as opcodes. At runtime, these are converted intoCTypeDescrObject *instances.Function Globals
Functions are listed in the
_cffi_globalsarray. This is a sorted array of entries that are lazily converted into real Python builtin function objects. Each entry points to an index in_cffi_types[]using theOP_FUNCTIONopcode to describe the function's signature (return type and arguments).Opcode Mechanism
Opcodes are integers where the lowest byte contains type information and the remaining high bytes contain an argument (
arg) relevant to that type. Common opcodes used within_cffi_typesinclude:OP_PRIMITIVE: Theargis an index in a primitive type list.OP_POINTER: Theargis the index of the item type in_cffi_types.OP_ARRAY: Theargis the index of the item type in_cffi_types, followed by an opcode containing the array length.OP_OPEN_ARRAY: Similar toOP_ARRAYbut for unsized arrays (e.g.,int[]).OP_STRUCT_UNION: Theargis the index in_cffi_structs_unions.OP_ENUM: Theargis the index in_cffi_enums.OP_TYPENAME: Theargis the index in_cffi_typenames.OP_FUNCTION: Theargis the index of the result type in_cffi_types, followed by argument opcodes, terminated byOP_FUNCTION_END.OP_FUNCTION_END: Thearg's lowest bit indicates if a variadic argument (...) is present.OP_NOOP: A simple indirection where theargis the index to look further in.
Pass pointers to arrays in function arguments
mainIn CFFI, a function argument declared as
item *is treated identically toitem[]. This means you can pass Python lists or byte strings to these arguments.Important Notes:
- Single items: If you want to pass a single item to a pointer argument, you must wrap it in a list of length 1 (e.g.,
[[x, y]]for astruct point_s *). - Lifespan: When passing a Python object (like a byte string) to a
char *argument, the CFFI creates the required C data structure just before the call and frees it immediately after. The pointer is only valid for the duration of the call. - Persistent Pointers: If the C function needs to store the pointer for later use, do not pass a Python byte string directly. Instead, create a persistent buffer using
ffi.new("char[]", mystring)orffi.from_buffer()with abytearray.
# Passing a single struct to a pointer argument # If the C function is: void process_point(struct point_s *p); lib.process_point([{'x': 5, 'y': 10}]) # Creating a persistent buffer for a char pointer p = ffi.new("char[]", mystring) # This makes a copy that lives as long as 'p' exists- Single items: If you want to pass a single item to a pointer argument, you must wrap it in a list of length 1 (e.g.,