1.12 Proporcionar una API de C para un módulo de extensión

Muchos módulos de extensión se limitan a proporcionar nuevas funciones y tipos para usarlos desde Python, pero a veces el código de un módulo de extensión puede resultar útil a otros módulos de extensión. Por ejemplo, un módulo de extensión puede proporcionar un tipo ``colección'' que funcione como una lista desordenada. Del mismo modo que la lista estándar de Python tiene una API de C que permite que los módulos de extensión creen y manipulen listas, este nuevo tipo colección debería estar dotado de un conjunto de funciones C para su manipulación directa desde otros módulos de extensión.

A primera vista parece fácil: sólo hay que escribir las funciones (sin declararlas, static, claro), proporcionar un fichero de cabecera apropiado y documentar la API de C. Y, de hecho, esto funcionaría si todos los módulos de extensión se enlazaran siempre estáticamente contra el intérprete de Python. Cuando se usan los módulos como bibliotecas compartidas, no obstante, los símbolos definidos en un módulo pueden no ser visibles desde otro módulo. Los detalles de visibilidad dependen del sistema operativo: algunos sistemas usan un espacio nominal global para el intérprete de Python y todos los módulos de extensión (por ejemplo, Windows), mientras otros requieren una lista explícita de símbolos importados en tiempo de compilación (por ejemplo, AIX) u ofrecen una opción de estrategias (la mayoría de los Unix). E incluso si los símbolos tienen visibilidad global, ¡puede que el módulo cuyas funciones desea llamar no esté todavía cargado!

La portabilidad, por consiguiente, requiere no hacer ninguna suposición sobre la visibilidad de los símbolos. Esto supone que todos los símbolos de los módulos de extensión deben declararse static, salvo la función de inicialización del módulo, para evitar conflictos con nombres de otros módulos de extensión (como se describió en la sección ). También significa que todos los símbolos que deberían ser accesibles desde otros módulos de extensión deben exportarse de un modo diferente.

Python proporciona un mecanismo especial para pasar la información a nivel de C (es decir, punteros) de un módulo de extensión a otro: Los CObjects. Un CObject es un tipo de dato de Python que almacena un puntero (void *). Los CObjects sólo se pueden crear y acceder a través de su API de C, pero se pueden pasar y traspasar como cualquier otro objeto de Python. En particular, se pueden asignar a un nombre del espacio nominal del módulo de extensión. Otros módulos de extensión pueden importar el módulo, recuperar el valor del nombre y recuperar el puntero del CObject.

Existen muchos modos de usar los CObjects para exportar el API C de un módulo de extensión. Cada nombre podría tener su propio CObject o podrían reunirse en una matriz todos los punteros del API de C y publicar su dirección en un CObject. Las tareas de guardar y recuperar los punteros pueden repartirse entre el módulo que proporciona el código y los módulos cliente.

El siguiente ejemplo muestra una solución que vuelca la mayor carga en el autor del módulo que exporta, lo que es apropiado en los módulos de bibliotecas de uso común. Almacena todos los punteros del API de C (¡uno en el ejemplo!) en una matriz de punteros void que pasa a ser el valor de un CObject. El fichero de cabecera correspondiente al módulo proporciona una macro que se ocupa de importar el módulo y recuperar sus punteros del API C; los módulos cliente sólo tienen que llamar a esta macro antes de acceder al API C.

El módulo servidor (servidor de código, no en el sentido estricto) es una modificación del módulo spam de la sección . La función spam.system() no llama a la función de biblioteca de C system() directamente, sino a una función PySpam_System(), que sería por supuesto más complicada en realidad (por, ejemplo, añadiendo ``spam'' a cada orden). Esta función, PySpam_System() también se exporta a otros módulos de extensión.

La función PySpam_System() es una simple función C, declarada static como todo el resto:

static int
PySpam_System(command)
    char *command;
{
    return system(command);
}

La función spam_system() tiene modificaciones triviales:

static PyObject *
spam_system(self, args)
    PyObject *self;
    PyObject *args;
{
    char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = PySpam_System(command);
    return Py_BuildValue("i", sts);
}

Al principio del módulo, justo tras la línea

#include "Python.h"

se deben añadir dos líneas:

#define SPAM_MODULE
#include "spammodule.h"

El #define se usa para indicar al fichero de cabecera que se está incluyendo en el módulo servidor, no en un módulo cliente. Por último, la función de inicialización del módulo debe ocuparse de inicializar la matriz de punteros del API de C:

void
initspam()
{
    PyObject *m, *d;
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;
    m = Py_InitModule("spam", SpamMethods);

    /* Inicializar la matriz de punteros del API de C */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* Crear un CObject que contenga la dirección de la matriz de punteros del API */
    c_api_object = PyCObject_FromVoidPtr((void *)PySpam_API, NULL);

    /* Crear un nombre para este objeto en el espacio nominal del módulo */
    d = PyModule_GetDict(m);
    PyDict_SetItemString(d, "_C_API", c_api_object);
}

Véase que PySpam_API se declara static; en caso contrario, ¡la matriz de punteros desaparecería al terminar la ejecución de initspam!

El grueso del trabajo está en el fichero de cabecera spammodule.h, que tiene este aspecto:

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif

/* Fichero de cabecera de spammodule */

/* Funciones del API de C */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (char *command)

/* Nº total de punteros del API de C */
#define PySpam_API_pointers 1


#ifdef SPAM_MODULE
/* Esta sección se utiliza al compilar spammodule.c */

static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;

#else
/* Esta sección se utiliza en los módulos que utilizan el API de spammodule */

static void **PySpam_API;

#define PySpam_System \
 (*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])

#define import_spam() \
{ \
  PyObject *module = PyImport_ImportModule("spam"); \
  if (module != NULL) { \
    PyObject *module_dict = PyModule_GetDict(module); \
    PyObject *c_api_object = PyDict_GetItemString(module_dict, "_C_API"); \
    if (PyCObject_Check(c_api_object)) { \
      PySpam_API = (void **)PyCObject_AsVoidPtr(c_api_object); \
    } \
  } \
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !defined(Py_SPAMMODULE_H */

Todo cuanto tiene que hacer un cliente para acceder a la función PySpam_System() es llamara a la función (más bien macro) import_spam() en su función de inicialización:

void
initclient()
{
    PyObject *m;

    Py_InitModule("client", ClientMethods);
    import_spam();
}

La mayor pega de esta solución es que el fichero spammodule.h es bastante complicado, Sin embargo, la estructura básica es la misma para cada función a exportar, por lo que sólo hay que aprenderlo una vez.

Por último, hay que mencionar que los CObjects ofrecen funciones adicionales, especialmente para reserva y liberación de memoria del puntero almacenado en un CObject. Los detalles están descritos en Manual de referencia del API Python/C en la sección ``CObjects'' y en la implementación de los CObjects (ficheros Include/cobject.h y Objects/cobject.c de la distribución de fuentes de Python).


Ver Sobre este documento... para obtener información sobre sugerencias.