Skip to content

FFI & PyO3 Interop

Rust ↔ C ↔ Python Interoperability

1. Foreign Function Interface (FFI)

#[repr(C)] Layout

rust
// Rust struct với C-compatible layout
#[repr(C)]
pub struct Point {
    pub x: f64,
    pub y: f64,
}

// Memory layout giống hệt C:
// struct Point {
//     double x;
//     double y;
// };

Exporting Functions to C

rust
// lib.rs
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn point_distance(p1: *const Point, p2: *const Point) -> f64 {
    unsafe {
        let p1 = &*p1;
        let p2 = &*p2;
        let dx = p2.x - p1.x;
        let dy = p2.y - p1.y;
        (dx * dx + dy * dy).sqrt()
    }
}
toml
# Cargo.toml
[lib]
crate-type = ["cdylib"]  # Creates .so/.dll/.dylib

Calling C from Rust

rust
// Declare external C functions
extern "C" {
    fn printf(format: *const i8, ...) -> i32;
    fn malloc(size: usize) -> *mut u8;
    fn free(ptr: *mut u8);
}

fn main() {
    unsafe {
        let msg = b"Hello from Rust!\n\0";
        printf(msg.as_ptr() as *const i8);
    }
}

Safety Guidelines

PatternSafe?Notes
extern "C" fnExporting is safe
Calling CMust wrap in unsafe
Raw pointersValidate before deref
Callbacks⚠️Beware of panics

2. PyO3: Python Extensions

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                     PYTHON APPLICATION                          │
├─────────────────────────────────────────────────────────────────┤
│   import my_rust_module                                         │
│   result = my_rust_module.fast_function(data)                   │
└───────────────────────────┬─────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                     PyO3 BINDINGS                               │
│   • Type conversion (PyObject ↔ Rust types)                     │
│   • GIL management                                              │
│   • Exception handling                                          │
└───────────────────────────┬─────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                     RUST CODE                                   │
│   • Pure Rust logic (no Python dependencies)                    │
│   • Full performance, memory safety                             │
└─────────────────────────────────────────────────────────────────┘

Basic PyO3 Setup

toml
# Cargo.toml
[package]
name = "my_rust_module"
version = "0.1.0"
edition = "2021"

[lib]
name = "my_rust_module"
crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.20", features = ["extension-module"] }
rust
// src/lib.rs
use pyo3::prelude::*;

/// Formats the sum of two numbers as string
#[pyfunction]
fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
    Ok((a + b).to_string())
}

/// A Python module implemented in Rust
#[pymodule]
fn my_rust_module(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
    Ok(())
}

Build & Use

bash
# Install maturin
pip install maturin

# Build and install
maturin develop --release

# Use in Python
python -c "import my_rust_module; print(my_rust_module.sum_as_string(5, 3))"
# Output: "8"

3. GIL Management

The Problem

                    PYTHON GIL (Global Interpreter Lock)
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   Thread 1          Thread 2          Thread 3                  │
│   ┌───────┐         ┌───────┐         ┌───────┐                 │
│   │Python │         │Python │         │Python │                 │
│   │ code  │         │ code  │         │ code  │                 │
│   └───┬───┘         └───┬───┘         └───┬───┘                 │
│       │                 │                 │                     │
│       └────────────────────────────────────                     │
│                        │                                        │
│                   ┌────┴────┐                                   │
│                   │   GIL   │ ← Only ONE thread at a time       │
│                   └─────────┘                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Releasing GIL in Rust

rust
use pyo3::prelude::*;

/// CPU-intensive function that releases the GIL
#[pyfunction]
fn compute_heavy(data: Vec<f64>) -> PyResult<f64> {
    // Release the GIL during Rust computation
    Python::with_gil(|py| {
        py.allow_threads(|| {
            // This runs WITHOUT the GIL!
            // Other Python threads can run
            heavy_computation(&data)
        })
    })
}

fn heavy_computation(data: &[f64]) -> f64 {
    data.iter().map(|x| x.sin().cos().tan()).sum()
}

When to Release GIL

OperationRelease GIL?Reason
Pure Rust compute✅ YesNo Python objects accessed
File I/O✅ YesBlocking operation
Network I/O✅ YesBlocking operation
Accessing PyObject❌ NoGIL required
Calling Python❌ NoGIL required

4. Type Conversions

Python → Rust

rust
use pyo3::prelude::*;
use pyo3::types::{PyList, PyDict};

#[pyfunction]
fn process_list(py: Python, list: &PyList) -> PyResult<Vec<i64>> {
    list.iter()
        .map(|item| item.extract::<i64>())
        .collect()
}

#[pyfunction]
fn process_dict(dict: &PyDict) -> PyResult<()> {
    for (key, value) in dict.iter() {
        let key: String = key.extract()?;
        let value: i64 = value.extract()?;
        println!("{}: {}", key, value);
    }
    Ok(())
}

Rust → Python

rust
use pyo3::prelude::*;

#[pyfunction]
fn create_list(py: Python) -> PyResult<Py<PyList>> {
    let list = PyList::new(py, &[1, 2, 3, 4, 5]);
    Ok(list.into())
}

#[pyclass]
struct Point {
    x: f64,
    y: f64,
}

#[pymethods]
impl Point {
    #[new]
    fn new(x: f64, y: f64) -> Self {
        Point { x, y }
    }
    
    fn distance(&self, other: &Point) -> f64 {
        let dx = self.x - other.x;
        let dy = self.y - other.y;
        (dx * dx + dy * dy).sqrt()
    }
}

5. Error Handling

rust
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;

#[pyfunction]
fn divide(a: f64, b: f64) -> PyResult<f64> {
    if b == 0.0 {
        Err(PyValueError::new_err("Division by zero"))
    } else {
        Ok(a / b)
    }
}

// Custom exception
pyo3::create_exception!(my_module, CustomError, pyo3::exceptions::PyException);

#[pyfunction]
fn risky_operation() -> PyResult<()> {
    Err(CustomError::new_err("Something went wrong"))
}

6. Complete Example: Fast CSV Parser

rust
use pyo3::prelude::*;
use std::fs::File;
use std::io::{BufRead, BufReader};

#[pyfunction]
fn parse_csv_fast(path: String) -> PyResult<Vec<Vec<String>>> {
    Python::with_gil(|py| {
        py.allow_threads(|| {
            let file = File::open(&path)
                .map_err(|e| PyErr::new::<pyo3::exceptions::PyIOError, _>(e.to_string()))?;
            
            let reader = BufReader::new(file);
            let mut rows = Vec::new();
            
            for line in reader.lines() {
                let line = line
                    .map_err(|e| PyErr::new::<pyo3::exceptions::PyIOError, _>(e.to_string()))?;
                let fields: Vec<String> = line.split(',')
                    .map(|s| s.trim().to_string())
                    .collect();
                rows.push(fields);
            }
            
            Ok(rows)
        })
    })
}

#[pymodule]
fn fast_csv(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(parse_csv_fast, m)?)?;
    Ok(())
}
python
# Usage in Python
import fast_csv
import time

start = time.time()
data = fast_csv.parse_csv_fast("large_file.csv")
print(f"Parsed {len(data)} rows in {time.time() - start:.2f}s")

🎯 Best Practices

PracticeReason
Release GIL for CPU workAllow Python parallelism
Use #[repr(C)] for FFIMemory layout compatibility
Handle errors properlyDon't panic across FFI boundary
Batch operationsReduce FFI call overhead