In our previous article,Understanding PHP FFI, we showed how you can use PHP to talk directly to C functions. In this article we will explore how you can do the same thing with Python.
Python can talk to native code using an FFI, short for Foreign Function Interface. In plain terms, FFI lets Python call C functions in a shared library without writing a CPython extension. You ship a .so on Linux, a .dylib on macOS, or a .dll on Windows, and then drive it from Python code.
This is a fast lane for three reasons:
There are two popular tools in Python land:
Below is a quick intro that is still nerd friendly, with two runnable examples, and a practical checklist for mapping functions correctly.
A shared library exports symbols. Each symbol is a function or global. Python FFI opens the library file, finds the symbol by name, and calls it using a calling convention. To be safe, Python needs to know the argument types, the return type, and sometimes who owns memory. You provide that metadata and Python handles the rest.
We will call strlen from the C standard library to count bytes. Then we will call cos from the math library. On Linux, cos lives in libm; on macOS it lives in libSystem; on Windows it is in msvcrt.
import ctypes
import ctypes.util
# Locate the C standard library in a cross platform way
libc_name = ctypes.util.find_library("c")
libm_name = ctypes.util.find_library("m") # may be None on some platforms
libc = ctypes.CDLL(libc_name) # C calling convention
strlen = libc.strlen
strlen.argtypes = [ctypes.c_char_p]
strlen.restype = ctypes.c_size_t
print(strlen(b"hello ffi")) # 9
# Math: cos(double) -> double
# Fallback: sometimes cos is in libc already, so try that if libm not found
libm = ctypes.CDLL(libm_name) if libm_name else libc
cos = libm.cos
cos.argtypes = [ctypes.c_double]
cos.restype = ctypes.c_double
print(cos(3.14159)) # roughly -1.0
Notes:
Now a slightly more structured example using cffi. We will pretend we have a header with a small API.
C header we want to bind:
// file: metrics.h
typedef struct {
unsigned long count;
double sum;
} metrics_t;
void metrics_add(metrics_t* m, double value);
double metrics_mean(const metrics_t* m);
In real life you would compile a shared library that implements this header. For this demo we will just declare the types and call into a prebuilt lib you already have, or dlopen None to resolve from the current process for simple functions. The point is to show the ergonomics of cffi.
from cffi import FFI
ffi = FFI()
ffi.cdef(
"typedef struct { unsigned long count; double sum; } metrics_t;"
"void metrics_add(metrics_t* m, double value);"
"double metrics_mean(const metrics_t* m);"
)
# If you have a shared library, pass its path to dlopen.
# For illustration, assume it is ./libmetrics.so or metrics.dll
C = ffi.dlopen("./libmetrics.so") # adjust for your platform
m = ffi.new("metrics_t *")
for v in [10.0, 20.0, 30.0]:
C.metrics_add(m, v)
print(int(m.count), float(m.sum)) # 3 and 60.0
print(C.metrics_mean(m)) # 20.0
Why cffi:
The source of truth is the C header files and the official docs for the library. You need the function names, prototypes, type definitions, constants, and error semantics.
Here is a practical checklist.
1) Find the library
2) Confirm exported symbols
3) Read the headers
4) Map C types to Python FFI types
5) Calling convention
6) Memory ownership
7) Error reporting
8) Threading and GIL
This cheatsheet covers common C to ctypes mappings.
C type | ctypes type |
---|---|
char | c_char |
char * | c_char_p (input only) |
void * | c_void_p |
int | c_int |
unsigned int | c_uint |
long | c_long |
unsigned long | c_ulong |
long long | c_longlong |
unsigned long long | c_ulonglong |
float | c_float |
double | c_double |
size_t | c_size_t |
For structs, build a subclass of ctypes.Structure with fields. For function pointers, use ctypes.CFUNCTYPE. In cffi, declare the prototype in cdef and use ffi.callback for Python implemented callbacks.
FFI is how you keep Python fun while unlocking native speed. Start with a single function that you care about, write the types explicitly, and build up from there. Once you see a Python script call into a serious C library and return in microseconds, you will not want to go back.
Ready to dive deeper? Check out our advanced section