Skip to content

ctypes & CFFI: Calling C Code from Python

Overview

ctypes and CFFI enable calling C libraries: - ctypes: Built-in, low-level C interface - CFFI: Higher-level, more Pythonic - Performance: Call optimized C code directly - Integration: Use existing C libraries in Python


ctypes Basics

Loading C Libraries

import ctypes
import platform

# Load system library
if platform.system() == "Windows":
    libc = ctypes.CDLL("msvcrt")
elif platform.system() == "Darwin":  # macOS
    libc = ctypes.CDLL("libc.dylib")
else:  # Linux
    libc = ctypes.CDLL("libc.so.6")

# Call C function
result = libc.abs(-42)
print(result)  # 42

Argument and Return Types

import ctypes
import math

# Load math library
libm = ctypes.CDLL("libm.so.6")

# Specify argument and return types
libm.sqrt.argtypes = [ctypes.c_double]
libm.sqrt.restype = ctypes.c_double

# Call with proper types
result = libm.sqrt(16.0)
print(result)  # 4.0

Working with C Structs

import ctypes

# Define C struct
class Point(ctypes.Structure):
    _fields_ = [("x", ctypes.c_double),
                ("y", ctypes.c_double)]

# Create instance
p = Point(x=3.0, y=4.0)
print(p.x, p.y)  # 3.0 4.0

# Nested struct
class Rect(ctypes.Structure):
    _fields_ = [("top_left", Point),
                ("bottom_right", Point)]

r = Rect()
r.top_left.x = 0.0
r.top_left.y = 10.0

Arrays and Pointers

import ctypes
import numpy as np

# Create array
arr = (ctypes.c_int * 5)(1, 2, 3, 4, 5)
print(arr[0])  # 1

# Pointer to array
ptr = ctypes.pointer(arr)

# NumPy integration
np_arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
ptr = np_arr.ctypes.data_as(ctypes.POINTER(ctypes.c_double))

CFFI: More Pythonic Interface

Basic CFFI Usage

from cffi import FFI

ffi = FFI()

# Define C interface
ffi.cdef("""
    double sqrt(double x);
    int abs(int x);
""")

# Load library
C = ffi.dlopen("libm.so.6")

# Use C functions
print(C.sqrt(16.0))  # 4.0
print(C.abs(-42))    # 42

CFFI Structs and Functions

from cffi import FFI

ffi = FFI()

# Define struct and function
ffi.cdef("""
    struct Point {
        double x;
        double y;
    };

    double distance(struct Point* p1, struct Point* p2);
""")

# Implement in Python (or use existing C library)
@ffi.callback("double(struct Point*, struct Point*)")
def distance_callback(p1, p2):
    dx = p1.x - p2.x
    dy = p1.y - p2.y
    return (dx**2 + dy**2)**0.5

# Create structs
p1 = ffi.new("struct Point*")
p1.x = 0.0
p1.y = 0.0

p2 = ffi.new("struct Point*")
p2.x = 3.0
p2.y = 4.0

# Compute distance
dist = distance_callback(p1, p2)
print(dist)  # 5.0

NumPy Integration

Calling C with NumPy Arrays

import numpy as np
import ctypes

def process_array(arr):
    """Process NumPy array in C."""
    arr = np.asarray(arr, dtype=np.float64)

    # Get pointer to data
    data_ptr = arr.ctypes.data_as(ctypes.POINTER(ctypes.c_double))

    # Call C function with pointer
    # libm_c.process_float_array(data_ptr, len(arr))

    return arr

# Use it
data = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
result = process_array(data)

Real-World ML Example

Calling BLAS from ctypes

import numpy as np
import ctypes

# Load BLAS library
libblas = ctypes.CDLL("libblas.so.3")

# Define DGEMM (matrix multiply)
libblas.dgemm_.argtypes = [
    ctypes.c_char_p,  # TRANSA
    ctypes.c_char_p,  # TRANSB
    ctypes.POINTER(ctypes.c_int),  # M
    ctypes.POINTER(ctypes.c_int),  # N
    ctypes.POINTER(ctypes.c_int),  # K
    ctypes.POINTER(ctypes.c_double),  # ALPHA
    ctypes.c_void_p,  # A
    ctypes.POINTER(ctypes.c_int),  # LDA
    ctypes.c_void_p,  # B
    ctypes.POINTER(ctypes.c_int),  # LDB
    ctypes.POINTER(ctypes.c_double),  # BETA
    ctypes.c_void_p,  # C
    ctypes.POINTER(ctypes.c_int),  # LDC
]

def matrix_multiply_blas(A, B):
    """Multiply matrices using BLAS."""
    A = np.asarray(A, dtype=np.float64, order='F')
    B = np.asarray(B, dtype=np.float64, order='F')

    m, k = A.shape
    k, n = B.shape

    C = np.zeros((m, n), dtype=np.float64, order='F')

    one = ctypes.c_double(1.0)
    zero = ctypes.c_double(0.0)
    m_c = ctypes.c_int(m)
    n_c = ctypes.c_int(n)
    k_c = ctypes.c_int(k)

    libblas.dgemm_(
        b'N', b'N',
        ctypes.byref(m_c),
        ctypes.byref(n_c),
        ctypes.byref(k_c),
        ctypes.byref(one),
        A.ctypes.data_as(ctypes.c_void_p),
        ctypes.byref(m_c),
        B.ctypes.data_as(ctypes.c_void_p),
        ctypes.byref(k_c),
        ctypes.byref(zero),
        C.ctypes.data_as(ctypes.c_void_p),
        ctypes.byref(m_c),
    )

    return C

Summary: ctypes vs CFFI

Aspect ctypes CFFI
Pythonicity Low High
Type Safety Manual Automatic
Complexity Simple Medium
Performance Good Excellent
NumPy Integration Good Good