article thumbnail
Python FFI
Call native C from python
#python, #programming

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.

Mental model

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.

Example 1: ctypes with the C standard library

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:

Example 2: cffi with a tiny custom C API

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:

How do you know what to map

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

A tiny mapping table

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.

Debugging moves that save hours

When to choose ctypes vs cffi

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