Aislamiento de módulos de extensión

¿Quién debería leer esto?

Esta guía está escrita para los mantenedores de extensiones C-API que deseen hacer que esa extensión sea más segura para usar en aplicaciones donde Python se usa como biblioteca.

Trasfondo

Un intérprete (interpreter) es el contexto en el que se ejecuta el código de Python. Contiene la configuración (p. ej., la ruta de importación) y el estado de tiempo de ejecución (p. ej., el conjunto de módulos importados).

Python admite la ejecución de varios intérpretes en un solo proceso. Hay dos casos en los que pensar, los usuarios pueden ejecutar intérpretes:

Ambos casos (y combinaciones de ellos) serían más útiles al incorporar Python dentro de una biblioteca. Las bibliotecas generalmente no deben hacer suposiciones sobre la aplicación que las usa, lo que incluye asumir un «intérprete principal de Python» en todo el proceso.

Históricamente, los módulos de extensión de Python no manejan bien este caso de uso. Muchos módulos de extensión (e incluso algunos módulos stdlib) usan el estado global por-proceso, porque las variables C static son extremadamente fáciles de usar. Así, los datos que deberían ser específicos de un intérprete acaban siendo compartidos entre intérpretes. A menos que el desarrollador de la extensión tenga cuidado, es muy fácil introducir casos extremos que provocan bloqueos cuando un módulo se carga en más de un intérprete en el mismo proceso.

Desafortunadamente, el estado por-intérprete no es fácil de lograr. Los autores de extensiones tienden a no tener en cuenta múltiples intérpretes cuando desarrollan, y actualmente es engorroso probar el comportamiento.

Ingrese al estado por módulo

Instead of focusing on per-interpreter state, Python’s C API is evolving to better support the more granular per-module state. This means that C-level data should be attached to a module object. Each interpreter creates its own module object, keeping the data separate. For testing the isolation, multiple module objects corresponding to a single extension can even be loaded in a single interpreter.

El estado por módulo proporciona una manera fácil de pensar en la vida útil y la propiedad de los recursos: el módulo de extensión se inicializará cuando se cree un objeto de módulo y se limpiará cuando se libere. En este sentido, un módulo es como cualquier otro PyObject*; no hay ganchos de «apagado del intérprete» para pensar u olvidar.

Tenga en cuenta que hay casos de uso para diferentes tipos de «globales»: por proceso, por intérprete, por subproceso o por estado de tarea. Con el estado por módulo como predeterminado, estos aún son posibles, pero debe tratarlos como casos excepcionales: si los necesita, debe brindarles atención y pruebas adicionales. (Tenga en cuenta que esta guía no los cubre).

Objetos módulos aislados

El punto clave a tener en cuenta al desarrollar un módulo de extensión es que se pueden crear varios objetos de módulo a partir de una única biblioteca compartida. Por ejemplo:

>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii  # create a new module object
>>> old_binascii == binascii
False

Como regla general, los dos módulos deben ser completamente independientes. Todos los objetos y el estado específico del módulo deben encapsularse dentro del objeto del módulo, no compartirse con otros objetos del módulo y limpiarse cuando se desasigna el objeto del módulo. Dado que esto es solo una regla general, las excepciones son posibles (consulte Managing Global State), pero necesitarán más reflexión y atención en los casos extremos.

Si bien algunos módulos podrían funcionar con restricciones menos estrictas, los módulos aislados facilitan el establecimiento de expectativas y pautas claras que funcionan en una variedad de casos de uso.

Casos extremos sorprendentes

Tenga en cuenta que los módulos aislados crean algunos casos extremos sorprendentes. En particular, cada objeto de módulo normalmente no compartirá sus clases y excepciones con otros módulos similares. Continuando con example above, tenga en cuenta que old_binascii.Error y binascii.Error son objetos separados. En el código siguiente, se detecta la excepción not:

>>> old_binascii.Error == binascii.Error
False
>>> try:
...     old_binascii.unhexlify(b'qwertyuiop')
... except binascii.Error:
...     print('boo')
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
binascii.Error: Non-hexadecimal digit found

Esto se espera. Tenga en cuenta que los módulos de Python puro se comportan de la misma manera: es una parte de cómo funciona Python.

El objetivo es hacer que los módulos de extensión sean seguros en el nivel C, no hacer que los piratas informáticos se comporten de manera intuitiva. Mutar sys.modules «manualmente» cuenta como un hack.

Cómo hacer que los módulos sean seguros con varios intérpretes

Administrar el estado global

A veces, el estado asociado con un módulo de Python no es específico de ese módulo, sino de todo el proceso (o algo más «más global» que un módulo). Por ejemplo:

  • El módulo readline gestiona el terminal.

  • Un módulo que se ejecuta en una placa de circuito quiere controlar el LED integrado.

En estos casos, el módulo Python debería proporcionar acceso al estado global, en lugar de poseerlo. Si es posible, escriba el módulo para que varias copias del mismo puedan acceder al estado de forma independiente (junto con otras bibliotecas, ya sea para Python u otros lenguajes). Si eso no es posible, considere el bloqueo explícito.

Si es necesario usar el estado global del proceso, la forma más sencilla de evitar problemas con varios intérpretes es evitar explícitamente que un módulo se cargue más de una vez por proceso; consulte Opt-Out: Limiting to One Module Object per Process.

Administración del estado por módulo

Para usar el estado por módulo, use multi-phase extension module initialization. Esto indica que su módulo admite múltiples intérpretes correctamente.

Establezca PyModuleDef.m_size en un número positivo para solicitar tantos bytes de almacenamiento local para el módulo. Por lo general, esto se establecerá en el tamaño de algún struct específico del módulo, que puede almacenar todo el estado de nivel C del módulo. En particular, es donde debe colocar los punteros a las clases (incluidas las excepciones, pero excluyendo los tipos estáticos) y configuraciones (por ejemplo, field_size_limit de csv) que el código C necesita para funcionar.

Nota

Otra opción es almacenar el estado en el __dict__ del módulo, pero debe evitar fallas cuando los usuarios modifican __dict__ desde el código de Python. Esto generalmente significa verificación de errores y tipos en el nivel C, que es fácil equivocarse y difícil de probar lo suficiente.

Sin embargo, si el estado del módulo no es necesario en el código C, almacenarlo solo en __dict__ es una buena idea.

Si el estado del módulo incluye punteros PyObject, el objeto del módulo debe contener referencias a esos objetos e implementar los enlaces de nivel de módulo m_traverse, m_clear y m_free. Estos funcionan como tp_traverse, tp_clear y tp_free de una clase. Agregarlos requerirá algo de trabajo y hará que el código sea más largo; este es el precio de los módulos que se pueden descargar limpiamente.

Un ejemplo de un módulo con estado por módulo está actualmente disponible como xxlimited; ejemplo de inicialización del módulo que se muestra en la parte inferior del archivo.

Exclusión voluntaria: limitación a un objeto de módulo por proceso

Un PyModuleDef.m_size no negativo indica que un módulo admite varios intérpretes correctamente. Si este aún no es el caso de su módulo, puede hacer que su módulo se pueda cargar explícitamente solo una vez por proceso. Por ejemplo:

static int loaded = 0;

static int
exec_module(PyObject* module)
{
    if (loaded) {
        PyErr_SetString(PyExc_ImportError,
                        "cannot load module more than once per process");
        return -1;
    }
    loaded = 1;
    // ... rest of initialization
}

Acceso al estado del módulo desde las funciones

Acceder al estado desde funciones a nivel de módulo es sencillo. Las funciones obtienen el objeto del módulo como su primer argumento; para extraer el estado, puede usar PyModule_GetState:

static PyObject *
func(PyObject *module, PyObject *args)
{
    my_struct *state = (my_struct*)PyModule_GetState(module);
    if (state == NULL) {
        return NULL;
    }
    // ... rest of logic
}

Nota

PyModule_GetState puede retornar NULL sin establecer una excepción si no hay un estado de módulo, es decir, PyModuleDef.m_size era cero. En su propio módulo, tiene el control de m_size, por lo que es fácil de evitar.

Tipos Heap

Tradicionalmente, los tipos definidos en código C son estáticos; es decir, estructuras static PyTypeObject definidas directamente en el código e inicializadas mediante PyType_Ready().

Tales tipos son necesariamente compartidos a lo largo del proceso. Compartirlos entre objetos de módulo requiere prestar atención a cualquier estado que posean o al que accedan. Para limitar los posibles problemas, los tipos estáticos son inmutables en el nivel de Python: por ejemplo, no puede configurar str.myattribute = 123.

Detalles de implementación de CPython: Compartir objetos verdaderamente inmutables entre intérpretes está bien, siempre que no proporcionen acceso a objetos mutables. Sin embargo, en CPython, cada objeto de Python tiene un detalle de implementación mutable: el recuento de referencias. Los cambios en el refcount están protegidos por el GIL. Por lo tanto, el código que comparte cualquier objeto de Python entre intérpretes depende implícitamente del GIL actual de todo el proceso de CPython.

Debido a que son inmutables y globales de proceso, los tipos estáticos no pueden acceder a «su» estado de módulo. Si algún método de este tipo requiere acceso al estado del módulo, el tipo debe convertirse a tipo almacenado en memoria dinámica (heap) o tipo heap para abreviar. Estos se corresponden más estrechamente con las clases creadas por la instrucción class de Python.

Para los módulos nuevos, usar tipos heap de forma predeterminada es una buena regla general.

Cambio de tipos estáticos a tipos heap

Los tipos estáticos se pueden convertir en tipos heap, pero tenga en cuenta que la API de tipo heap no se diseñó para la conversión «sin pérdidas» de tipos estáticos, es decir, para crear un tipo que funcione exactamente como un tipo estático determinado. Por lo tanto, al reescribir la definición de clase en una nueva API, es probable que cambie sin querer algunos detalles (por ejemplo, capacidad de selección o espacios heredados). Siempre pruebe los detalles que son importantes para usted.

Tenga cuidado con los siguientes dos puntos en particular (pero tenga en cuenta que esta no es una lista completa):

Definición de tipos heap

Los tipos heap se pueden crear completando una estructura PyType_Spec, una descripción o «modelo» de una clase y llamando a PyType_FromModuleAndSpec() para construir un nuevo objeto de clase.

Nota

Otras funciones, como PyType_FromSpec(), también pueden crear tipos heap, pero PyType_FromModuleAndSpec() asocia el módulo con la clase, lo que permite el acceso al estado del módulo desde los métodos.

La clase generalmente debe almacenarse en ambos, el estado del módulo (para acceso seguro desde C) y el __dict__ del módulo (para acceso desde código Python).

Protocolo de recolección de basura

Las instancias de tipos heap contienen una referencia a su tipo. Esto garantiza que el tipo no se destruya antes de que se destruyan todas sus instancias, pero puede generar ciclos de referencia que el recolector de elementos no utilizados debe interrumpir.

Para evitar pérdidas de memoria, las instancias de los tipos heap deben implementar el protocolo de recolección de elementos no utilizados. Es decir, los tipos heap deben:

  • Have the Py_TPFLAGS_HAVE_GC flag.

  • Define a traverse function using Py_tp_traverse, which visits the type (e.g. using Py_VISIT(Py_TYPE(self))).

Please refer to the the documentation of Py_TPFLAGS_HAVE_GC and tp_traverse for additional considerations.

The API for defining heap types grew organically, leaving it somewhat awkward to use in its current state. The following sections will guide you through common issues.

tp_traverse in Python 3.8 and lower

The requirement to visit the type from tp_traverse was added in Python 3.9. If you support Python 3.8 and lower, the traverse function must not visit the type, so it must be more complicated:

static int my_traverse(PyObject *self, visitproc visit, void *arg)
{
    if (Py_Version >= 0x03090000) {
        Py_VISIT(Py_TYPE(self));
    }
    return 0;
}

Unfortunately, Py_Version was only added in Python 3.11. As a replacement, use:

Delegating tp_traverse

If your traverse function delegates to the tp_traverse of its base class (or another type), ensure that Py_TYPE(self) is visited only once. Note that only heap type are expected to visit the type in tp_traverse.

Por ejemplo, si su función poligonal incluye:

base->tp_traverse(self, visit, arg)

…y base puede ser un tipo estático, entonces también debe incluir:

if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) {
    // a heap type's tp_traverse already visited Py_TYPE(self)
} else {
    if (Py_Version >= 0x03090000) {
        Py_VISIT(Py_TYPE(self));
    }
}

It is not necessary to handle the type’s reference count in tp_new and tp_clear.

Defining tp_dealloc

If your type has a custom tp_dealloc function, it needs to:

  • call PyObject_GC_UnTrack() before any fields are invalidated, and

  • decrement the reference count of the type.

To keep the type valid while tp_free is called, the type’s refcount needs to be decremented after the instance is deallocated. For example:

static void my_dealloc(PyObject *self)
{
    PyObject_GC_UnTrack(self);
    ...
    PyTypeObject *type = Py_TYPE(self);
    type->tp_free(self);
    Py_DECREF(type);
}

The default tp_dealloc function does this, so if your type does not override tp_dealloc you don’t need to add it.

Not overriding tp_free

The tp_free slot of a heap type must be set to PyObject_GC_Del(). This is the default; do not override it.

Avoiding PyObject_New

GC-tracked objects need to be allocated using GC-aware functions.

If you use use PyObject_New() or PyObject_NewVar():

  • Get and call type’s tp_alloc slot, if possible. That is, replace TYPE *o = PyObject_New(TYPE, typeobj) with:

    TYPE *o = typeobj->tp_alloc(typeobj, 0);
    

    Replace o = PyObject_NewVar(TYPE, typeobj, size) with the same, but use size instead of the 0.

  • If the above is not possible (e.g. inside a custom tp_alloc), call PyObject_GC_New() or PyObject_GC_NewVar():

    TYPE *o = PyObject_GC_New(TYPE, typeobj);
    
    TYPE *o = PyObject_GC_NewVar(TYPE, typeobj, size);
    

Acceso al estado del módulo desde las clases

Si tiene un objeto de tipo definido con PyType_FromModuleAndSpec(), puede llamar a PyType_GetModule() para obtener el módulo asociado y luego a PyModule_GetState() para obtener el estado del módulo.

Para ahorrar un tedioso código repetitivo de manejo de errores, puede combinar estos dos pasos con PyType_GetModuleState(), lo que da como resultado:

my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (state == NULL) {
    return NULL;
}

Acceso al estado del módulo desde métodos regulares

Acceder al estado de nivel de módulo desde los métodos de una clase es algo más complicado, pero es posible gracias a la API introducida en Python 3.9. Para obtener el estado, primero debe obtener la clase de definición y luego obtener el estado del módulo.

El obstáculo más grande es obtener la clase en la que se definió un método, o la «clase de definición» de ese método para abreviar. La clase de definición puede tener una referencia al módulo del que forma parte.

Do not confuse the defining class with Py_TYPE(self). If the method is called on a subclass of your type, Py_TYPE(self) will refer to that subclass, which may be defined in different module than yours.

Nota

El siguiente código de Python puede ilustrar el concepto. Base.get_defining_class retorna Base incluso si type(self) == Sub:

class Base:
    def get_type_of_self(self):
        return type(self)

    def get_defining_class(self):
        return __class__

class Sub(Base):
    pass

For a method to get its «defining class», it must use the METH_METHOD | METH_FASTCALL | METH_KEYWORDS calling convention and the corresponding PyCMethod signature:

PyObject *PyCMethod(
    PyObject *self,               // object the method was called on
    PyTypeObject *defining_class, // defining class
    PyObject *const *args,        // C array of arguments
    Py_ssize_t nargs,             // length of "args"
    PyObject *kwnames)            // NULL, or dict of keyword arguments

Una vez que tenga la clase de definición, llame a PyType_GetModuleState() para obtener el estado de su módulo asociado.

Por ejemplo:

static PyObject *
example_method(PyObject *self,
        PyTypeObject *defining_class,
        PyObject *const *args,
        Py_ssize_t nargs,
        PyObject *kwnames)
{
    my_struct *state = (my_struct*)PyType_GetModuleState(defining_class);
    if (state == NULL) {
        return NULL;
    }
    ... // rest of logic
}

PyDoc_STRVAR(example_method_doc, "...");

static PyMethodDef my_methods[] = {
    {"example_method",
      (PyCFunction)(void(*)(void))example_method,
      METH_METHOD|METH_FASTCALL|METH_KEYWORDS,
      example_method_doc}
    {NULL},
}

Acceso al estado del módulo desde métodos de Slot, Getters y Setters

Nota

Esto es nuevo en Python 3.11.

Slot methods—the fast C equivalents for special methods, such as nb_add for __add__ or tp_new for initialization—have a very simple API that doesn’t allow passing in the defining class, unlike with PyCMethod. The same goes for getters and setters defined with PyGetSetDef.

Para acceder al estado del módulo en estos casos, utilice la función PyType_GetModuleByDef() y pase la definición del módulo. Una vez que tenga el módulo, llame a PyModule_GetState() para obtener el estado:

PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &module_def);
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
    return NULL;
}

PyType_GetModuleByDef() works by searching the method resolution order (i.e. all superclasses) for the first superclass that has a corresponding module.

Nota

In very exotic cases (inheritance chains spanning multiple modules created from the same definition), PyType_GetModuleByDef() might not return the module of the true defining class. However, it will always return a module with the same definition, ensuring a compatible C memory layout.

Vida útil del estado del módulo

Cuando un objeto de módulo se recolecta como basura, se libera su estado de módulo. Para cada puntero a (una parte de) el estado del módulo, debe tener una referencia al objeto del módulo.

Por lo general, esto no es un problema, porque los tipos creados con PyType_FromModuleAndSpec() y sus instancias contienen una referencia al módulo. Sin embargo, debe tener cuidado en el recuento de referencias cuando hace referencia al estado del módulo desde otros lugares, como devoluciones de llamada para bibliotecas externas.

Problemas abiertos

Varios problemas relacionados con el estado por módulo y los tipos heap todavía están abiertos.

Las discusiones sobre cómo mejorar la situación se llevan a cabo mejor en el capi-sig mailing list.

Alcance por clase

Actualmente (a partir de Python 3.11) no es posible adjuntar estado a tipos individuales sin depender de los detalles de implementación de CPython (que pueden cambiar en el futuro, tal vez, irónicamente, para permitir una solución adecuada para el alcance por clase).

Conversión sin pérdidas a tipos heap

La API de tipo heap no se diseñó para la conversión «sin pérdidas» de tipos estáticos; es decir, crear un tipo que funcione exactamente como un tipo estático determinado.