Free — no signup required

Foreign Function Interfaces (FFI): Crossing Process Boundaries

4 min read

When the Network Is Too Slow

Network communication — even fast gRPC — has irreducible overhead: you must serialize data, hand it to the OS networking stack, transmit bytes, receive them on the other end, and deserialize. For most use cases, this overhead is negligible. But for some workloads, it is unacceptable.

Consider a Python web server that needs to apply a real-time image filter to every uploaded photo before storing it. The filter algorithm is implemented in a Rust library that processes images 50x faster than a pure Python implementation. Running the Rust code as a separate microservice and calling it over the network means serializing every image frame, sending megabytes of pixel data over a socket, and deserializing on the other side — for every single request. The network overhead would dominate the total latency.

The alternative: load the Rust library directly into the Python process and call its functions as if they were Python functions. Both pieces of code share the same memory — no serialization, no network, no OS context switches. This is what a Foreign Function Interface (FFI) enables.

FFI is a mechanism that allows code written in one programming language to call functions written in another language, within the same running process, sharing the same memory space.

The C ABI: The Universal Adapter

Here is the problem: Python and Rust have completely different internal representations of data, different calling conventions (how arguments are passed to functions), and different memory management models. They cannot directly call each other's functions.

The solution is to use C as an intermediary. The C Application Binary Interface (ABI) is the oldest and most universally supported binary interface in computing. It defines, at the machine-code level, exactly how function arguments are passed (which CPU registers, which stack positions), how return values are communicated, and how memory is laid out. Because C has been the dominant systems language for 50 years, virtually every modern language runtime knows how to speak C ABI.

The pattern works like this:

  1. Write the performance-critical code in Rust (or C++, or C).
  2. Expose a C-compatible interface — in Rust, this means marking functions with #[no_mangle] and extern "C", which tells the Rust compiler to use C calling conventions instead of Rust's own.
  3. Compile to a shared library — a .so file on Linux, .dylib on macOS, .dll on Windows. This is a compiled binary that can be loaded by any process at runtime.
  4. Load and call from Python — Python's built-in ctypes module (or the higher-level cffi library) can load any .so file and call its exported C-compatible functions.

A Concrete FFI Example

Here is the complete workflow for calling a Rust function from Python.

The Rust side (src/lib.rs):

// #[no_mangle] prevents Rust from renaming this function during compilation.
// extern "C" tells Rust to use C calling conventions.
#[no_mangle]
pub extern "C" fn add_integers(a: i32, b: i32) -> i32 {
    a + b
}

Compiled with cargo build --release, this produces target/release/libmath.so.

The Python side:

import ctypes

# Load the compiled shared library into this Python process
lib = ctypes.CDLL("./target/release/libmath.so")

# Declare the function signature — Python must know the C types
# This is the manual "translation layer" between Python's dynamic types and C's static types
lib.add_integers.argtypes = [ctypes.c_int32, ctypes.c_int32]
lib.add_integers.restype = ctypes.c_int32

# Call the Rust function as if it were a Python function
result = lib.add_integers(10, 20)
print(f"Result from Rust: {result}")  # Output: Result from Rust: 30

The argtypes and restype declarations are not optional. Without them, ctypes passes Python objects using its default assumptions, which may not match what the C function expects. The result is undefined behavior — typically a crash or silent data corruption.

Key Point: FFI is not just a performance optimization — it is also the mechanism that allows Python to use the entire ecosystem of C and C++ libraries (NumPy's core is C, TensorFlow's core is C++, OpenSSL is C). When you import numpy, Python is loading a compiled C extension via FFI under the hood. The ctypes approach shown here is the manual version of what Python's C extension API does automatically for well-packaged libraries.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free