Desarrollo de extensiones de Python en C

Versión: 2.x, 3.x

Introducción

Una de las características muy atractivas de Python es la posibilidad de crear extensiones en C o C++ que se integran completamente y con gran facilidad en el lenguaje. Una extensión es, ni más ni menos, un módulo. Por ejemplo, módulos como cmath están escritos en C, generalmente para obtener un mayor rendimiento. Otros módulos utilizan código Python para proveer un API de mayor nivel u orientada a objetos; por ejemplo, el módulo estándar socket y su parte en C _socket; en donde, generalmente, este último está indocumentado. Proyectos como Zope utilizan extensiones escritas en C para optimizar su rendimiento.

Es importante aclarar: puede que desarrollar una extensión en C no sea lo que estás buscando, sino utilizar el módulo estándar ctypes para obtener el mismo resultado en menos tiempo y, seguramente, con mayor eficiencia.

Con el lanzamiento de Python 3 se han dejado atras una gran cantidad de funciones, módulos, sintaxis, etcétera. Y como no podía ser la excepción, la API de Python para C también ha cambiado, dejando de lado la compatibilidad con código anterior a la rama 3.x. Puedes ver una lista -no tan completa- de los cambios en este enlace.

El desarrollo de extensiones de Python en C/C++, junto con todas las posibilidades que el lenguaje ofrece, es un tema bastante amplio y, en algunas ocasiones, no tan simple. Este artículo pretende introducir al lector al asunto, con el fin de que pueda posteriormente ampliar sus conocimientos teniendo ya una base consolidada. Obviamente, conocimientos medios de C son requeridos para seguir esta guía en su totalidad. Crearemos un pequeño módulo al que llamaremos mymath, que contendrá cuatro simples funciones para realizar operaciones aritméticas (suma, resta, multiplicación y división).

El módulo

Puedes ir escribiendo el código a medida que avanza el artículo, o esperar al final de cada sección en la que mostraré el código final.

Estructura

Para introducir a los lectores a la estructura básica de una extensión escrita en C y sus respectivas funciones, pasaremos directamente al desarrollo de un módulo con operaciones aritméticas básicas:

  • Suma – add(a, b)
  • Resta – sub(a, b)
  • Multiplicación – mult(a, b)
  • División – div(a, b)

Por cuestiones de consistencia con el resto del código el nombre de las funciones está en inglés, como también el nombre del módulo.

Obviamente, todas estas operaciones pueden llevarse a cabo (y de lo más simple) directamente en Python. Su implementación tiene como finalidad destacar el uso de la API que el lenguaje provee para C.

Antes de comenzar con el código, crea un nuevo archivo llamado mymathmodule.c en la ubicación que desees. Generalmente, ésta es la convención de nombramiento para las extensiones: nombremodule.c. En caso que el nombre del módulo sea extenso, puede omitirse la última palabra. Por ejemplo, en lugar de formmanagermodule.c mejor utiliza formmanager.c; en lugar de databasemanagermodule.c, databasemanager.c (aunque tal vez sea mejor dbmanager.c).

Accediendo al API

Todas las funciones necesarias al momento de crear una extensión en C las provee el archivo Python.h. Acerca de éste y las librerías necesarias para compilar se debatirá en el próximo apartado «Compilando». Por lo tanto, para comenzar con el código añade al archivo mymathmodule.c la siguiente línea:

#include <Python.h>

Si incluyes otros archivos de cabecera en tu código, considera siempre añadir esta línea antes que las demás; Python.h define varios macros que, en varias ocasiones, determinan el comportamiento de otras librerías en muchos sistemas.

Todas las funciones de Python mantienen el prefijo Py o Py_.

Funciones

Luego de importar todas las funciones necesarias que provee el archivo de cabecera Python.h pasaremos a crear la primera función: sum(). Ésta tomará dos números enteros, los sumará y retornará su resultado.

static PyObject *
mymath_add(PyObject *self, PyObject *args)
{

Ésta es la estructura para cualquier tipo de función que quieras incluír en tu módulo, de la cual se puede destacar lo siguiente:

  • Todas las variables (aquellas que se encuentren fuera de una función) o funciones que crees dentro del archivo deberán ser estáticas (static), con excepción del punto de entrada del mismo el cual veremos más adelante.
  • PyObject* representa un determinado objeto de Python (en este caso una función).
  • mymath_add es simplemente una convención (modulo_funcion), que realmente no determina el nombre que tendrá la función al momento de importar el módulo, aunque de seguro preferirás utilizarla.
  • Los argumentos self y args son dos objetos que representan la instancia del módulo y la lista de argumentos, respectivamente.

    long a, b, ret;

Declaramos las variables enteras que utilizaremos para guardar los primeros dos argumentos (a y b) y el valor de retorno (ret). Si bien este artículo se basa en una extensión escrita en C, programadores de C++ deberían también crear sus variables al comienzo de la función.

Las siguienes dos líneas se encargan de extraer los argumentos alojados en args.

    if (!PyArg_ParseTuple(args, "ll", &a, &b))
        return NULL;

PyArg_ParseTuple está declarada de la siguiente manera:

int PyArg_ParseTuple(PyObject *args, const char *format, ...)

En donde args es el mismo argumento que Python se encarga de pasar a nuestra función, aquel que contiene los argumentos. format indica qué tipo de valores se almacenarán en las siguientes direcciones de memoria. Por ejemplo:

int cantidad;
PyArg_ParseTuple(args, "i", &cantidad);

Toma el primer argumento de args y lo almacena en cantidad, como un valor entero (i).

char nombre[20];
PyArg_ParseTuple(args, "s", &nombre);

En este caso se toma el primer argumento de args y lo almacena en nombre, como una cadena (s).

PyArg_ParseTuple(args, "is", &cantidad, &nombre);

Este último ejemplo sería una correcta llamada para una hipotética función enviar_dinero(2000, "Jorge"), en el que se extraen múltiples valores.

Puede verse una lista completa con los valores que pueden introducirse en format en este enlace.

Continuando con la función, en este caso se toman dos valores de la lista de argumentos, ambos del tipo long (l); lo que indica que debe ser llamada como, por ejemplo, mymath.add(20, 30). En caso de ser llamada erróneamente (por ejemplo, mymath.add(20, 30, 50) o mymath.add(20, "30"), PyArg_ParseTuple se encargará de lanzar una excepción (probablemente TypeError o ValueError), que será guardada en una variable global y se propagará hasta que sea manejada por alguna función, tanto de C como de Python. En lenguajes como C en donde no existe el término excepciones, se utiliza el valor de retorno de cada función para determinar si se han producido errores. Al programar extensiones se deben mantener ambos métodos. Si algo sale mal, la función deberá lanzar la excepción apropiada y retornar NULL. De esta manera, funciones que llamen a ésta leerán el valor de retorno y devolverán NULL para que la excepción siga propagándose, o la manejarán y devolverán el valor apropiado; así sucesivamente. Más sobre errores y excepciones en este enlace.

Es por lo explicado anteriormente que nuestra funcion determina si ha ocurrido un error al llamar a PyArg_ParseTuple y, de ser así, retorna NULL.

Una vez leídos los argumentos, pasamos a realizar nuestro trabajo:

   ret = a + b;

Sumamos los argumentos y lo almacenamos en una variable. Por último, retornamos el resultado:

    return Py_BuildValue("l", ret);

La función Py_BuildValue permite crear un objeto (recuerda que todo es un objeto en Python: funciones, enteros, cadenas), utilizando el mismo método de formateo que PyArg_ParseTuple. Algunos ejemplos:

int cantidad;
char nombre[] = "Jorge";

Py_BuildValue("i", cantidad);
Py_BuildValue("s", nombre);

Si bien este método es totalmente válido en Python 3, tal vez prefieran el que emplea la documentación:

    return PyLong_FromLong(ret);

Esta función crea un objeto long de Python a partir de una variable del tipo long de C.

Sin más, el código completo de la función:

static PyObject *
mymath_sumar(PyObject *self, PyObject *args)
{
    long a, b, ret;

    if (!PyArg_ParseTuple(args, "ll", &a, &b))
        return NULL;

    ret = a + b;
    return Py_BuildValue("l", ret);
}

Listando los métodos

Nuestra extensión debe proveer una estructura (debajo de todas las funciones; en C el orden es importante) en donde Python pueda leer qué metodos estamos exportando junto con su nombre, documentación y otros.

static PyMethodDef MyMathMethods[] = {

En donde MyMathMethos utiliza la convención ModuloMethods. Luego, un conjunto de datos para cada una de las funciones:

    {"add", mymath_add, METH_VARARGS,
     "Suma los argumentos y retorna el resultado."},

En donde "add" es el nombre de la función. mymath_add, una referencia a la función que creamos anteriormente. METH_VARARGS, la convención de llamada (calling convention) de la función (los argumentos serán pasados como una tupla). Para argumentos pasados por nombre (keyword arguments) debe utilizarse METH_KEYWORDS y extraer los argumentos con la función PyArg_ParseTupleAndKeywords. El cuarto valor es una cadena documentando la función, aquella que se desplegará al hacer:

>>> import mymath
>>> print(help(mymath.add))
Help on built-in function add in module mymath:

add(...)
    Suma los argumentos y retorna el resultado.

Por último, algunos valores extras para satisfacer a Python durante la compilación:

    {NULL, NULL, 0, NULL}
};

Por lo tanto, la estructura sería la siguiente:

static PyMethodDef ModuloMethods[] = {
    {"function1",  modulo_function1, METH_VARARGS,
     "Function documentation 1."},
    {"function2",  modulo_function2, METH_VARARGS,
     "Function documentation 2."},
    /* ... más funciones ... */
    {NULL, NULL, 0, NULL}
};

Y la estructura final para nuestra extensión:

static PyMethodDef MyMathMethods[] = {
    {"add", mymath_add, METH_VARARGS,
     "Suma los argumentos y retorna el resultado."},
    {NULL, NULL, 0, NULL}
};

El punto de entrada

Ésta es una de las principales diferencias entre Python 2 y 3 a la hora de desarrollar extensiones en C: el punto de entrada, aquel que será llamado cuando el módulo sea importado para inicializar todos los componentes necesarios (de esto se encarga Python, sólo debes preocuparte por llamar a una función).

Antes de pasar al punto de entrada, quienes usen Python 3 tienen un trabajo extra. Si usas Python 2, sigue con el texto luego del separador de más abajo.

Extra para Python 3

Debemos crear una esctructura que contiene información sobre el módulo, así como creamos una para las funciones (MyMathMethods). Dicha estructura debe llevar un nombre con la convención NombreModule, en donde Nombre es el nombre del módulo (MyMath en este caso):

static struct PyModuleDef MyMathModule = {
   PyModuleDef_HEAD_INIT,

Este código representa la estructura básica para proveer información al intérprete sobre el módulo. A continuación, el nombre del módulo pasado como una cadena:

   "mymath",   /* nombre del módulo */

Junto con su documentación:

    NULL,  /* documentación del módulo */

En este caso no proveemos ningún tipo de documentación. Sin embargo, puede lograrse de la siguiente manera:

#define MODULE_DOC "Módulo con operaciones aritméticas básicas."

/* functiones y estructura para los métodos*/

static struct PyModuleDef MyMathModule = {
   PyModuleDef_HEAD_INIT,
   "mymath",
   MODULE_DOC,

Cadena que se mostrará al hacer:

>>> import mymath
>>> print(help(mymath))
Help on module mymath:

NAME
    mymath - Módulo con operaciones aritméticas básicas.

...

Volviendo al ejemplo, por último:

   -1,
   MyMathMethods
};

En donde -1 indica el estado del módulo (véase este enlace) y MyMathMethods son las funciones que incorpora el módulo junto con toda su información, definidas previamente.

Código completo:

static struct PyModuleDef MyMathModule = {
   PyModuleDef_HEAD_INIT,
   "mymath",
   NULL,
   -1,
   MyMathMethods
};


El punto de entrada es una función (la única no estática) del tipo PyMODINIT_FUNC (que define distintos tipos dependiendo de la plataforma):

PyMODINIT_FUNC

Seguido del nombre de la función. En Python 2, la función debe llevar la convención initmodule, en donde module es el nombre del módulo. En Python 3, se debe utilizar PyInit_module.

Python 2

initmymath(void)
{

Python 3

PyInit_mymath(void)
{

Por último, para inicializar el módulo programadores de Python 2 deberán llamar a Py_InitModule pasándo como argumentos el nombre del módulo (una cadena) y la estructura definida como PyMethodDef:

    (void) Py_InitModule("mymath", MyMathMethods);
}

También puede utilizarse Py_InitModule3 para especificar la documentación:

#define MODULE_DOC "Módulo con operaciones aritméticas básicas."
(void) Py_InitModule("mymath", MyMathMethods, MODULE_DOC);

Preferentemente MODULE_DOC debe definirse al comienzo del archivo.

Para Python 3 simplemente deben llamar a PyModule_Create, pasándo como único argumento un puntero a la estructura del módulo:

    return PyModule_Create(&MyMathModule);
}

Código completo:

Python 2

PyMODINIT_FUNC
initmymath(void)
{
    (void) Py_InitModule("mymath", MyMathMethods);
}

Python 3

PyMODINIT_FUNC
PyInit_mymath(void)
{
    return PyModule_Create(&MyMathModule);
}

Código completo

Python 2

#include <Python.h>

static PyObject *
mymath_add(PyObject *self, PyObject *args)
{
    long a, b, ret;

    if (!PyArg_ParseTuple(args, "ll", &a, &b))
        return NULL;

    ret = a + b;
    return Py_BuildValue("l", ret);
}

static PyMethodDef MyMathMethods[] = {
    {"add", mymath_add, METH_VARARGS,
     "Suma los argumentos y retorna el resultado."},
    {NULL, NULL, 0, NULL}
};

PyMODINIT_FUNC
initmymath(void)
{
    (void) Py_InitModule("mymath", MyMathMethods);
}

Python 3

#include <Python.h>

#define MODULE_DOC "Módulo con operaciones aritméticas básicas."

static PyObject *
mymath_add(PyObject *self, PyObject *args)
{
    long a, b, ret;

    if (!PyArg_ParseTuple(args, "ll", &a, &b))
        return NULL;

    ret = a + b;
    return PyLong_FromLong(ret);
}

static PyMethodDef MyMathMethods[] = {
    {"add", mymath_add, METH_VARARGS,
     "Suma los argumentos y retorna el resultado."},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef MyMathModule = {
   PyModuleDef_HEAD_INIT,
   "mymath",
   MODULE_DOC,
   -1,
   MyMathMethods
};

PyMODINIT_FUNC
PyInit_mymath(void)
{
    return PyModule_Create(&MyMathModule);
}

Compilando

Nuestra extensión ya está completa, lista para ser compilada. Una vez hecho esto utilizando el siguiente método, obtendremos un archivo .pyd (en Windows) o .so (en Unix).

Preparando el setup

Se trata de crear un archivo llamado setup.py junto al archivo de fuente de la extensión (mymathmodule.c) con lo siguiente:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from distutils.core import setup, Extension

# Nombre el módulo y archivos que contienen el código de fuente.
module = Extension("mymath", sources=["mymathmodule.c"])

# Nombre del paquete, versión, descripción y una lista con las extensiones.
setup(name="MyMath",
      version="1.0",
      description="Basic math functions.",
      ext_modules=[module])

Python 2 y 3 no presentan diferencias.

La idea es usar distutils para compilar nuestra extensión como cualquier otro paquete de Python. De esta manera mantenemos un mismo método para todas las plataformas. Basándote en este código, puedes luego escribir el archivo setup.py para tus extensiones, cambiado valores como "mymath", "mymathmodule.c", etc.

Unix vs. Windows

En ambos sistemas se utiliza el método de carga dinámica, con archivos .so en Unix (shared object) y .dll en Windows (dynamic link library); aunque en este último se utiliza la extensión .pyd para evitar confusiones.

El método de compilación que se provee en este artículo (utilizando distutils) puede lograrse perfectamente en Windows, usando el compilador MinGW. Te recomiendo que visites el artículo Compilar extensiones de Python con distutils y MinGW para preparar tu entorno y solucionar los problemas más comunes.

Usuarios de Visual Studio también pueden lograrlo, aunque con algunas diferencias, siguiendo esta guía de la documentación oficial.

Ejecutando

Una vez que hayas terminado con tu archivo setup.py, abre la terminal o línea de comandos, y dirígete a su ubicación (utilizando el comando cd, por ejemplo, cd usuario/proyectos/extc). Luego, ejecuta:

python setup.py build

Este comando comenzará con el proceso de compilado de nuestra extensión. Asegúrate que python esté enlazado con el intérprete correspondiente a la versión que has utilizado para escribir tu extensión de C. Esto generará una salida similar a la siguiente (en Linux):

running build
running build_ext
building 'mymath' extension
creating build
creating build/temp.linux-x86_64-2.7
gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fPIC -I/usr/include/python2.7 -c mymathmodule.c -o build/temp.linux-x86_64-2.7/mymathmodule.o
creating build/lib.linux-x86_64-2.7
gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -Wl,-z,relro build/temp.linux-x86_64-2.7/mymathmodule.o -o build/lib.linux-x86_64-2.7/mymath.so

Usuarios de Ubuntu probablemente tengan que instalar el paquete python-dev previo a la compilación, para evitar errores como Python.h: No such file or directory.

sudo apt-get install python-dev

O preferiblemente especificando la versión:

sudo apt-get install pythonx.y-dev

Ejemplos:

sudo apt-get install python2.7-dev

sudo apt-get install python3.2-dev

En Windows, aquellos que obtengan errores de la familia undefined reference to, les vuelvo a proveer este artículo para compilar con MinGW.

Volviendo a la compilación, una vez finalizada se habrá creado la carpeta build, generalmente con dos más dentro de ella (lib y temp). Podrás encontrar tu archivo mymath.so o mymath.pyd dentro de lib. No tomes los nombres estrictamente, varían según el sistema. En mi caso, al utilizar Python 3 en Ubuntu el archivo se genera como mymath.cpython-32mu.so. Si te sucede lo mismo, simplemente renómbralo a mymath.so.

Importando la extensión

Para finalizar, probaremos la extensión creando un nuevo archivo llamado test.py con lo siguiente:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import mymath

print("Resultado: %d." % mymath.add(5, 6))

Mueve el archivo mymath.so o mymath.pyd junto a éste y ejecuta:

python test.py

O, en Windows, tal vez:

C:\PythonXY\python.exe test.py

Algunos ejemplos:

C:\Python27\python.exe test.py
C:\Python33\python.exe test.py

Y deberá imprimir:

Resultado: 11.

Asegúrate que estás ejecutando el archivo con el intérprete de la versión que has utilizado para compilar la extensión. De no ser así, probablemente obtengas una excepción del tipo ImportError.

Convenciones

Ten en cuenta las siguientes convenciones para el código C al escribir extensiones de Python:

  • Utiliza lower_case_with_underscores (por ejemplo, mymath_add) para nombrar a las funciones y variables, y CapWords para estructuras (por ejemplo, MyMathModule).
  • Para aquellos con experiencia en programación con la API de Windows: no usen la notación húngara (por ejemplo, bActivated o dwGold).
  • Utiliza 4 espacios para indentar el código.

Más en PEP 7 – Style guide for C code.

Conclusión

Espero que esta guía te haya sido de utilidad para incorporar una pequeña base sobre la programación de extensiones en C, a partir de la cual puedas extender tus conocimientos en el ámbito. Si tu pregunta es ¿qué hay de las demás funciones: sub, mult y div?, ya deberías poder implementarlas por tu cuenta. Si no es así, ¡házmelo saber en los comentarios!

Descargas

Ambos archivos contienen el código de fuente en C, el archivo setup.py y el script para importar la extensión test.py.

Enlaces interesantes

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

6 comentarios.

  1. Hola, quisiera saber si esto es el metodo que usan las librerias de python, ya que he visto que importan codigo desde C y tienen una sintaxis parecida a la explicada en este articulo.

      • ¿Y cual de las dos formas sería la mejor en cuanto a rendimiento?… Lo que pasa es que estoy trabajando en una librería cuyo principal objetivo es ser lo mas rápida y eficiente posible.

        • Recursos Python says:

          Yo creo que ambas van a tener el mismo rendimiento, pero habría que hacer las pruebas. De cualquier manera mi recomendación es que uses Cython.

          Saludos

  2. Hola y gracias por este artículo. Quisiera saber si puedes ayudarme; he seguido los pasos, pero al momento de ejecutar el comando python setup.py build, me muestra el siguiente error:
    gcc: error: unrecognized command line option ‘-mno-cygwin’
    ¿Tienes alguna idea de a quué se debe y cómo puedo solucionarlo?

    • Recursos Python says:

      Hola, ¿cómo estás? Tuve un problema similar al intentar compilar el módulo de compresión smaz con MinGW. La solución es un sencillo retoque en un archivo del paquete distutils que podes ver en este artículo. ¡Espero que te sirva!

Deja una respuesta