C y Python: extender Python con módulos C usando la C API y ctypes

Python es un lenguaje fantástico para productividad, pero hay situaciones donde su rendimiento no basta: procesamiento numérico intensivo, operaciones sobre grandes volúmenes de datos o código de sistema que necesita acceso directo a hardware. La solución clásica es escribir la parte crítica en C y llamarla desde Python. Hay tres formas principales de hacerlo: ctypes, la Python C API y cffi.

ctypes: llamar a una biblioteca .so sin recompilar

ctypes es la opción más rápida de implementar: carga una biblioteca compartida ya compilada y llama a sus funciones directamente. No requiere modificar el código C ni Python.

La biblioteca C

/* operaciones.c */
#include <math.h>

int sumar(int a, int b) {
    return a + b;
}

double distancia(double x1, double y1, double x2, double y2) {
    double dx = x2 - x1;
    double dy = y2 - y1;
    return sqrt(dx*dx + dy*dy);
}

/* Compilar como biblioteca compartida */
/* gcc -shared -fPIC -o liboperaciones.so operaciones.c -lm */

Usar desde Python con ctypes

from ctypes import cdll, c_int, c_double

# Cargar la biblioteca
lib = cdll.LoadLibrary('./liboperaciones.so')

# Declarar tipos de argumentos y retorno (obligatorio para correctitud)
lib.sumar.argtypes = [c_int, c_int]
lib.sumar.restype  = c_int

lib.distancia.argtypes = [c_double, c_double, c_double, c_double]
lib.distancia.restype  = c_double

# Llamar
resultado = lib.sumar(10, 32)
print(resultado)  # 42

d = lib.distancia(0.0, 0.0, 3.0, 4.0)
print(d)  # 5.0

Declarar argtypes y restype es fundamental: sin ellos, ctypes asume que los argumentos son int de 32 bits y el retorno también, lo que provoca resultados incorrectos con doubles o punteros.

Strings y punteros con ctypes

from ctypes import c_char_p, create_string_buffer

lib.procesar_texto.argtypes = [c_char_p]
lib.procesar_texto.restype  = c_char_p

# Pasar string Python a C
resultado = lib.procesar_texto(b"hola mundo")
print(resultado.decode())

# Buffer mutable para que C escriba en él
buf = create_string_buffer(256)
lib.rellenar_buffer(buf, 256)
print(buf.value.decode())

Python C API: módulos nativos completos

La C API permite crear módulos Python completos en C, con soporte para clases, excepciones y el GIL. Es la forma en que están escritos NumPy, PIL y la mayor parte de la librería estándar de CPython.

Módulo C mínimo

/* mimodulo.c */
#define PY_SSIZE_T_CLEAN
#include <Python.h>

/* Función que Python llamará como mimodulo.sumar(a, b) */
static PyObject* py_sumar(PyObject* self, PyObject* args) {
    int a, b;
    if (!PyArg_ParseTuple(args, "ii", &a, &b))
        return NULL;   /* error de parseo: PyArg_ParseTuple ya pone la excepción */
    return PyLong_FromLong(a + b);
}

/* Tabla de métodos del módulo */
static PyMethodDef metodos[] = {
    {"sumar", py_sumar, METH_VARARGS, "Suma dos enteros"},
    {NULL, NULL, 0, NULL}   /* centinela */
};

/* Definición del módulo */
static struct PyModuleDef modulo = {
    PyModuleDef_HEAD_INIT,
    "mimodulo",   /* nombre */
    NULL,          /* docstring */
    -1,
    metodos
};

/* Función de inicialización: DEBE llamarse PyInit_NOMBRE */
PyMODINIT_FUNC PyInit_mimodulo(void) {
    return PyModule_Create(&modulo);
}

Compilar e instalar

# Obtener flags de compilación
python3-config --includes --ldflags

# Compilar manualmente
gcc -shared -fPIC 
    $(python3-config --includes) 
    $(python3-config --ldflags) 
    -o mimodulo.so mimodulo.c

# Usar desde Python
python3 -c "import mimodulo; print(mimodulo.sumar(3, 4))"

Para proyectos reales, se usa setup.py o pyproject.toml con setuptools y la clase Extension, que gestiona los flags automáticamente.

Gestión del reference counting

La parte más delicada de la C API es el conteo de referencias manual. Cada PyObject* tiene un contador; cuando llega a 0, el GC libera el objeto:

/* Py_INCREF: incrementar contador (cuando guardamos una referencia) */
/* Py_DECREF: decrementar (cuando la liberamos) */

PyObject* lista = PyList_New(3);
/* lista tiene refcount=1 */

PyObject* num = PyLong_FromLong(42);
PyList_SetItem(lista, 0, num);
/* SetItem roba la referencia de num: no llamar Py_DECREF(num) */

Py_DECREF(lista);  /* liberar la lista al terminar */

cffi: la alternativa moderna

cffi es más ergonómica que ctypes y más sencilla que la C API completa. Define la interfaz directamente en C:

from cffi import FFI
ffi = FFI()

ffi.cdef("""
    int sumar(int a, int b);
    double distancia(double x1, double y1, double x2, double y2);
""")

lib = ffi.dlopen('./liboperaciones.so')
print(lib.sumar(10, 32))        # 42
print(lib.distancia(0, 0, 3, 4)) # 5.0

cffi es la interfaz que usa PyPy internamente y tiene mejor soporte para estructuras C complejas que ctypes.

Cuándo usar cada opción

  • ctypes: tienes una .so ya compilada y necesitas llamarla rápido. Sin código extra en C.
  • cffi: mismas condiciones que ctypes pero con API más limpia y mejor soporte de structs.
  • C API: necesitas integración profunda con Python (clases, generadores, excepciones personalizadas) o máximo rendimiento con control del GIL.

Si te interesa el rendimiento en C puro, el artículo sobre gestión de memoria en C cubre cómo optimizar las reservas que luego expones a Python. Y si buscas una alternativa con interoperabilidad nativa con C sin el overhead del GIL, Zig 0.14 ofrece interoperabilidad directa con bibliotecas C sin FFI intermedia.

Imagen: Pexels / Myburgh Roux

COMPARTE ESTE ARTÍCULO

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