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.
