ctypes y cffi en Python: llamar a librerías C desde Python sin escribir extensiones

Python es un lenguaje de scripting de alto nivel, pero a veces necesitas acceder a funciones de una librería C del sistema, llamar a una DLL nativa o usar código C ya existente sin la complejidad de escribir una extensión en C. Los módulos ctypes y cffi permiten hacer exactamente eso: llamar a código C compilado desde Python puro, con cero compilación adicional en el caso más simple.

ctypes: cargar librerías del sistema

import ctypes
import sys

# Cargar la librería C estándar
if sys.platform == 'linux':
    libc = ctypes.CDLL('libc.so.6')
elif sys.platform == 'darwin':
    libc = ctypes.CDLL('libc.dylib')
else:
    libc = ctypes.cdll.msvcrt   # Windows

# Llamar a strlen
libc.strlen.restype = ctypes.c_size_t
libc.strlen.argtypes = [ctypes.c_char_p]

cadena = b"Hola Python"
longitud = libc.strlen(cadena)
print(f"strlen('{cadena.decode()}') = {longitud}")   # 11

restype y argtypes: definir la firma

Siempre debes especificar restype y argtypes. Sin ellos, ctypes asume que la función devuelve int y que los argumentos se pasan sin conversión, lo que puede causar crashes o resultados incorrectos en funciones que devuelven punteros o doubles.

import ctypes
import ctypes.util

# Cargar libm (funciones matemáticas)
libm_nombre = ctypes.util.find_library('m')
libm = ctypes.CDLL(libm_nombre)

# Definir la firma de pow(double, double) -> double
libm.pow.restype = ctypes.c_double
libm.pow.argtypes = [ctypes.c_double, ctypes.c_double]

resultado = libm.pow(2.0, 10.0)
print(f"2^10 = {resultado}")   # 1024.0

# Definir la firma de sqrt(double) -> double
libm.sqrt.restype = ctypes.c_double
libm.sqrt.argtypes = [ctypes.c_double]
print(f"sqrt(144) = {libm.sqrt(144.0)}")   # 12.0

Tipos ctypes más comunes

import ctypes

# Tipos básicos
c_int    = ctypes.c_int        # int (32 bits con signo)
c_long   = ctypes.c_long       # long
c_float  = ctypes.c_float      # float
c_double = ctypes.c_double     # double
c_char_p = ctypes.c_char_p     # char* (bytes en Python)
c_void_p = ctypes.c_void_p     # void*
c_bool   = ctypes.c_bool       # _Bool

# Pasar punteros
valor = ctypes.c_int(42)
puntero = ctypes.byref(valor)   # &valor en C

# Crear arrays
Arreglo5Int = ctypes.c_int * 5
arr = Arreglo5Int(10, 20, 30, 40, 50)
print(list(arr))   # [10, 20, 30, 40, 50]

Estructuras con ctypes

import ctypes

class PuntoC(ctypes.Structure):
    _fields_ = [
        ('x', ctypes.c_double),
        ('y', ctypes.c_double),
    ]

class RectanguloC(ctypes.Structure):
    _fields_ = [
        ('origen', PuntoC),
        ('ancho', ctypes.c_double),
        ('alto', ctypes.c_double),
    ]


p = PuntoC(x=3.0, y=4.0)
print(f"Punto: ({p.x}, {p.y})")

r = RectanguloC(origen=PuntoC(0.0, 0.0), ancho=10.0, alto=5.0)
print(f"Área: {r.ancho * r.alto}")

# Pasar estructura a una función C
# libmia.calcular_area.restype = ctypes.c_double
# libmia.calcular_area.argtypes = [ctypes.POINTER(RectanguloC)]
# area = libmia.calcular_area(ctypes.byref(r))

Librería C propia con ctypes

# Supón que tienes esta librería C compilada como libmia.so:
#
# // mia.c
# #include <math.h>
# double distancia(double x1, double y1, double x2, double y2) {
#     double dx = x2 - x1, dy = y2 - y1;
#     return sqrt(dx*dx + dy*dy);
# }
# long long factorial(int n) {
#     if (n <= 1) return 1;
#     return n * factorial(n - 1);
# }
#
# Compilar: gcc -shared -fPIC -o libmia.so mia.c -lm

import ctypes

libmia = ctypes.CDLL('./libmia.so')

libmia.distancia.restype  = ctypes.c_double
libmia.distancia.argtypes = [ctypes.c_double] * 4

libmia.factorial.restype  = ctypes.c_longlong
libmia.factorial.argtypes = [ctypes.c_int]

print(f"Distancia: {libmia.distancia(0, 0, 3, 4)}")   # 5.0
print(f"10! = {libmia.factorial(10)}")                 # 3628800

cffi: pegar la declaración C directamente del header

cffi tiene un enfoque diferente: le pegas el fragmento de código C que declares la función, y él genera el binding automáticamente. Es más ergonómico cuando tienes headers disponibles.

# pip install cffi
from cffi import FFI

ffi = FFI()

# Declarar la interfaz C (extracto del header)
ffi.cdef("""
    double distancia(double x1, double y1, double x2, double y2);
    long long factorial(int n);
""")

# Cargar la librería
libmia = ffi.dlopen('./libmia.so')

print(f"Distancia: {libmia.distancia(0, 0, 3, 4)}")   # 5.0
print(f"10! = {libmia.factorial(10)}")                 # 3628800

# Strings con cffi
ffi.cdef("size_t strlen(const char *s);")
libc = ffi.dlopen(None)
s = ffi.new("char[]", b"Hola desde cffi")
print(libc.strlen(s))   # 14

cffi modo ABI vs API

from cffi import FFI

ffi = FFI()

# Modo ABI (dinámico): sin compilación, usa dlopen
# ? rápido de prototipar, depende de ABI estable

# Modo API (estático): genera un módulo C que se compila una vez
# ? mejor rendimiento, verifica tipos en compilación
ffi.cdef("""
    int suma(int a, int b);
""")
ffi.set_source("_mia_cffi", """
    int suma(int a, int b) { return a + b; }
""")
# ffi.compile()   # genera _mia_cffi.cpython-313-x86_64-linux-gnu.so

¿ctypes o cffi?

  • Usa ctypes cuando: no quieres dependencias adicionales, la librería es del sistema (libssl, libm, libc) o el binding es sencillo.
  • Usa cffi cuando: tienes el header C disponible, hay muchas funciones y estructuras, o buscas mejor rendimiento con el modo API compilado.
  • Para proyectos donde el rendimiento es crítico y tienes control sobre el código C, considera escribir una extensión Python nativa con la C API o usar Cython o pybind11.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP