Spec-Zone.ru › Python 3.12

Изоляция модулей расширений

Аннотация

Традиционно состояние, принадлежащее модулям расширений Python, хранилось в переменных C static с областью видимости всего процесса. В этом документе описаны проблемы такого состояния на уровне процесса и показан более безопасный подход: состояние на уровне модуля.

В документе также описывается, как по возможности перейти к состоянию на уровне модуля. Этот переход подразумевает выделение памяти для такого состояния, возможный переход от статических типов к типам в куче и, что, пожалуй, важнее всего, доступ к состоянию модуля из кода.

Для кого предназначено руководство

Это руководство предназначено для сопровождающих расширения C-API, которые хотят сделать эти расширения безопаснее для использования в приложениях, где Python применяется как библиотека.

Предпосылки

Интерпретатор — это контекст, в котором выполняется код Python. Он содержит конфигурацию (например, путь импорта) и состояние времени выполнения (например, набор импортированных модулей).

Python поддерживает запуск нескольких интерпретаторов в одном процессе. Следует учитывать два варианта: интерпретаторы можно запускать:

  • последовательно, выполняя несколько циклов Py_InitializeEx()/Py_FinalizeEx(), а также
  • параллельно, управляя «подинтерпретаторами» с помощью Py_NewInterpreter()/Py_EndInterpreter().

Оба варианта (а также их сочетания) особенно полезны при встраивании Python в библиотеку. Библиотеки, как правило, не должны делать предположений о использующем их приложении, в том числе предполагать наличие единственного «главного интерпретатора Python» на уровне процесса.

Исторически модули расширений Python плохо справляются с этим сценарием использования. Во многих модулях расширений (и даже в некоторых модулях стандартной библиотеки) используется глобальное состояние на уровне процесса, поскольку переменные C static чрезвычайно просты в использовании. В результате данные, которые должны быть привязаны к конкретному интерпретатору, оказываются общими для нескольких интерпретаторов. Если разработчик расширения не проявит осторожность, очень легко создать граничные случаи, приводящие к сбоям при загрузке модуля в нескольких интерпретаторах одного процесса.

К сожалению, добиться состояния на уровне интерпретатора непросто. При разработке авторы расширений обычно не учитывают возможность одновременной работы нескольких интерпретаторов, а проверять такое поведение сейчас неудобно.

Переход к состоянию на уровне модуля

Вместо того чтобы сосредоточиться на состоянии на уровне интерпретатора, C API Python развивается в направлении поддержки более гранулярного состояния на уровне модуля. Это означает, что данные уровня C должны быть привязаны к объекту модуля. Каждый интерпретатор создаёт собственный объект модуля, благодаря чему данные остаются раздельными. Для проверки изоляции в одном интерпретаторе можно даже загрузить несколько объектов модуля, соответствующих одному расширению.

Состояние на уровне модуля позволяет просто учитывать время жизни и владение ресурсами: модуль расширения инициализируется при создании объекта модуля и очищается при его освобождении. В этом отношении модуль ничем не отличается от любого другого PyObject*; не нужно думать о хуках «при завершении работы интерпретатора» — или забывать о них.

Обратите внимание, что для разных видов «глобальных» данных могут быть разные сценарии использования: состояние на уровне процесса, интерпретатора, потока или задачи. Если состоянием по умолчанию является состояние на уровне модуля, то эти варианты по-прежнему возможны, но их следует считать исключительными случаями: если они вам нужны, уделите им особое внимание и тщательно протестируйте их. (В этом руководстве они не рассматриваются.)

Изолированные объекты модулей

При разработке модуля расширения важно помнить, что из одной общей динамической библиотеки можно создать несколько объектов модуля. Например:

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

Как правило, два модуля должны быть полностью независимы. Все объекты и состояние, относящиеся к модулю, должны быть инкапсулированы в объекте модуля, а не совместно использоваться другими объектами модулей, и очищаться при освобождении объекта модуля. Поскольку это лишь общее правило, возможны исключения (см. раздел Управление глобальным состоянием), но они потребуют более тщательного анализа и внимания к граничным случаям.

Некоторым модулям могли бы подойти менее строгие ограничения, однако изолированные модули позволяют формулировать чёткие ожидания и рекомендации, применимые в самых разных сценариях использования.

Неочевидные граничные случаи

Обратите внимание, что изолированные модули порождают некоторые неочевидные граничные случаи. В частности, обычно каждый объект модуля не разделяет свои классы и исключения с другими аналогичными модулями. Продолжая пример выше, обратите внимание, что old_binascii.Error и binascii.Error — разные объекты. В следующем коде исключение не перехватывается:

>>> 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

Так и должно быть. Обратите внимание, что модули на чистом Python ведут себя так же: это часть принципов работы Python.

Цель — сделать модули расширений безопасными на уровне C, а не добиться интуитивного поведения для обходных решений. «Ручное» изменение sys.modules считается обходным решением.

Обеспечение безопасности модулей при работе с несколькими интерпретаторами

Управление глобальным состоянием

Иногда состояние, связанное с модулем Python, относится не к этому модулю, а ко всему процессу (или к чему-то ещё более «глобальному», чем модуль). Например:

  • Модуль readline управляет терминалом.
  • Модуль, работающий на печатной плате, должен управлять встроенным светодиодом.

В таких случаях модуль Python должен предоставлять доступ к глобальному состоянию, а не владеть им. По возможности напишите модуль так, чтобы несколько его экземпляров могли независимо обращаться к состоянию (наряду с другими библиотеками — как для Python, так и для других языков). Если это невозможно, рассмотрите возможность явной блокировки.

Если необходимо использовать глобальное состояние процесса, самый простой способ избежать проблем с несколькими интерпретаторами — явно запретить загружать модуль более одного раза в рамках процесса; см. раздел Отказ от поддержки: ограничение одним объектом модуля на процесс.

Управление состоянием на уровне модуля

Чтобы использовать состояние на уровне модуля, используйте многоэтапную инициализацию модуля расширения. Это указывает на то, что ваш модуль корректно поддерживает несколько интерпретаторов.

Задайте для PyModuleDef.m_size положительное значение, чтобы запросить указанное количество байтов памяти, выделяемой локально для модуля. Обычно этому параметру присваивается размер некоторой специфичной для модуля struct, в которой можно хранить всё состояние уровня C этого модуля. В частности, здесь следует хранить указатели на классы (в том числе исключения, но не статические типы) и настройки (например, csv’s field_size_limit), необходимые для работы кода C.

Примечание

Другой вариант — хранить состояние в __dict__ модуля, но при этом необходимо избегать сбоев в случае, если пользователи изменят __dict__ из кода Python. Обычно это означает необходимость проверок ошибок и типов на уровне C; их легко реализовать неправильно и трудно достаточно тщательно протестировать.

Однако если состояние модуля не требуется в коде C, хранить его только в __dict__ — хорошая идея.

Если состояние модуля содержит указатели PyObject, объект модуля должен хранить ссылки на эти объекты и реализовывать хуки уровня модуля m_traverse, m_clear и m_free. Они работают так же, как tp_traverse, tp_clear и tp_free класса. Их добавление потребует усилий и увеличит объём кода; это плата за возможность корректно выгружать модули.

Пример модуля с состоянием на уровне модуля доступен в файле xxlimited; пример инициализации модуля приведён в конце файла.

Отказ от поддержки: ограничение одним объектом модуля на процесс

Неотрицательное значение PyModuleDef.m_size означает, что модуль корректно поддерживает несколько интерпретаторов. Если ваш модуль пока этого не поддерживает, можно явно указать, что его разрешено загружать только один раз в рамках процесса. Например:

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
}

Доступ к состоянию модуля из функций

Доступ к состоянию из функций уровня модуля организован просто. Функции получают объект модуля в качестве первого аргумента; для извлечения состояния можно использовать 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
}

Примечание

PyModule_GetState может вернуть NULL, не устанавливая исключение, если состояние модуля отсутствует, то есть если PyModuleDef.m_size равно нулю. В собственном модуле вы контролируете m_size, поэтому этого легко избежать.

Типы в куче

Традиционно типы, определённые в коде C, являются статическими, то есть представляют собой структуры static PyTypeObject, определённые непосредственно в коде и инициализированные с помощью PyType_Ready().

Такие типы неизбежно являются общими для всего процесса. Чтобы совместно использовать их между объектами модулей, нужно учитывать всё состояние, которым они владеют или к которому обращаются. Для ограничения возможных проблем статические типы неизменяемы на уровне Python: например, нельзя задать str.myattribute = 123.

Особенность реализации CPython: совместно использовать действительно неизменяемые объекты между интерпретаторами можно, если они не предоставляют доступ к изменяемым объектам. Однако в CPython у каждого объекта Python есть изменяемая особенность реализации: счётчик ссылок. Изменения счётчика ссылок защищены GIL. Таким образом, код, который совместно использует любые объекты Python между интерпретаторами, неявно зависит от текущего GIL CPython, действующего на уровне всего процесса.

Поскольку статические типы неизменяемы и являются глобальными для процесса, они не могут получить доступ к «состоянию своего модуля». Если какой-либо метод такого типа должен обращаться к состоянию модуля, тип необходимо преобразовать в тип, выделяемый в куче, или, кратко, тип в куче. Такие типы больше похожи на классы, создаваемые оператором class в Python.

Для новых модулей хорошее общее правило — по умолчанию использовать типы в куче.

Преобразование статических типов в типы в куче

Статические типы можно преобразовать в типы в куче, но учтите, что API типов в куче не разрабатывался для «без потерь» преобразования статических типов, то есть для создания типа, который работает в точности как заданный статический тип. Поэтому при переписывании определения класса с использованием нового API вы, вероятно, непреднамеренно измените некоторые детали (например, возможность сериализации с помощью pickle или унаследованные слоты). Всегда проверяйте важные для вас детали.

В частности, обратите внимание на следующие два момента (но учтите, что это не полный список):

  • В отличие от статических типов, объекты типов в куче по умолчанию изменяемы. Используйте флаг Py_TPFLAGS_IMMUTABLETYPE, чтобы запретить изменение.
  • Типы в куче по умолчанию наследуют tp_new, поэтому их может стать возможным создавать из кода Python. Это можно запретить с помощью флага Py_TPFLAGS_DISALLOW_INSTANTIATION.

Определение типов в куче

Типы в куче можно создать, заполнив структуру PyType_Spec — описанием, или «шаблоном», класса — и вызвав PyType_FromModuleAndSpec() для создания нового объекта класса.

Примечание

Типы в куче можно также создавать с помощью других функций, например PyType_FromSpec(), но PyType_FromModuleAndSpec() связывает класс с модулем, что позволяет обращаться к состоянию модуля из методов.

Как правило, класс следует хранить и там, и там: в состоянии модуля (для безопасного доступа из C) и в __dict__ модуля (для доступа из кода Python).

Протокол сборки мусора

Экземпляры типов в куче содержат ссылку на свой тип. Это гарантирует, что тип не будет уничтожен раньше всех своих экземпляров, но может привести к циклам ссылок, которые должен разрывать сборщик мусора.

Чтобы избежать утечек памяти, экземпляры типов в куче должны реализовывать протокол сборки мусора. То есть типы в куче должны:

  • иметь флаг Py_TPFLAGS_HAVE_GC;
  • определять функцию обхода с помощью Py_tp_traverse, которая посещает тип (например, используя Py_VISIT(Py_TYPE(self))).

Дополнительные замечания см. в документации к Py_TPFLAGS_HAVE_GC и tp_traverse.

API для определения типов в куче развивался постепенно, поэтому сейчас его использование несколько неудобно. В следующих разделах рассматриваются распространённые проблемы.

tp_traverse в Python 3.8 и более ранних версиях

Требование посещать тип из tp_traverse появилось в Python 3.9. Если вы поддерживаете Python 3.8 и более ранние версии, функция обхода не должна посещать тип, поэтому она должна быть сложнее:

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

К сожалению, Py_Version появилась только в Python 3.11. Вместо неё используйте:

  • PY_VERSION_HEX, если вы не используете стабильный ABI, или
  • sys.version_info (через PySys_GetObject() и PyArg_ParseTuple()).

Делегирование tp_traverse

Если функция обхода делегирует вызов tp_traverse базового класса (или другого типа), убедитесь, что Py_TYPE(self) посещается только один раз. Обратите внимание, что посещать тип в tp_traverse должны только типы в куче.

Например, если функция обхода содержит:

base->tp_traverse(self, visit, arg)

…а base может быть статическим типом, она также должна содержать:

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));
    }
}

В tp_new и tp_clear обрабатывать счётчик ссылок типа не требуется.

Определение tp_dealloc

Если для типа задана собственная функция tp_dealloc, она должна:

  • вызывать PyObject_GC_UnTrack() до того, как какие-либо поля станут недействительными, и
  • уменьшать счётчик ссылок типа.

Чтобы тип оставался действительным во время вызова tp_free, счётчик ссылок типа нужно уменьшать после освобождения экземпляра. Например:

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

Функция tp_dealloc по умолчанию делает это, поэтому, если ваш тип не переопределяет tp_dealloc, добавлять её не нужно.

Не переопределяйте tp_free

Для слота tp_free типа в куче должно быть задано значение PyObject_GC_Del(). Это значение используется по умолчанию; не переопределяйте его.

Избегайте PyObject_New

Объекты, отслеживаемые сборщиком мусора, необходимо выделять с помощью функций, учитывающих сборку мусора.

Если вы используете PyObject_New() или PyObject_NewVar():

  • По возможности получите и вызовите слот типа tp_alloc. То есть замените TYPE *o = PyObject_New(TYPE, typeobj) на:

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

    Замените o = PyObject_NewVar(TYPE, typeobj, size) на аналогичный код, но вместо 0 используйте размер.

  • Если это невозможно (например, внутри собственной tp_alloc), вызовите PyObject_GC_New() или PyObject_GC_NewVar():

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

Доступ к состоянию модуля из классов

Если объект типа определён с помощью PyType_FromModuleAndSpec(), можно вызвать PyType_GetModule(), чтобы получить связанный с ним модуль, а затем PyModule_GetState(), чтобы получить состояние модуля.

Чтобы избежать утомительного шаблонного кода обработки ошибок, можно объединить эти два шага с помощью PyType_GetModuleState():

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

Доступ к состоянию модуля из обычных методов

Доступ к состоянию уровня модуля из методов класса несколько сложнее, но возможен благодаря API, появившемуся в Python 3.9. Чтобы получить состояние, сначала нужно получить класс, в котором определён метод, а затем — состояние модуля из этого класса.

Самое сложное — получить класс, в котором был определён метод, или, кратко, «класс определения» этого метода. Класс определения может содержать ссылку на модуль, частью которого он является.

Не путайте класс определения с Py_TYPE(self). Если метод вызывается для подкласса вашего типа, Py_TYPE(self) будет указывать на этот подкласс, который может быть определён в другом модуле.

Примечание

Следующий код Python поясняет эту концепцию. Base.get_defining_class возвращает Base, даже если 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

Чтобы метод мог получить свой «класс определения», он должен использовать METH_METHOD | METH_FASTCALL | METH_KEYWORDS calling convention и соответствующую сигнатуру PyCMethod:

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

Получив класс определения, вызовите PyType_GetModuleState(), чтобы получить состояние связанного с ним модуля.

Например:

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},
}

Доступ к состоянию модуля из методов-слотов, геттеров и сеттеров

Примечание

Эта возможность появилась в Python 3.11.

Методы-слоты — быстрые аналоги специальных методов на C, например nb_add для __add__ или tp_new для инициализации, — имеют очень простой API, который не позволяет передавать класс определения, в отличие от PyCMethod. То же относится к геттерам и сеттерам, определённым с помощью PyGetSetDef.

Чтобы получить доступ к состоянию модуля в этих случаях, используйте функцию PyType_GetModuleByDef() и передайте ей определение модуля. Получив модуль, вызовите PyModule_GetState(), чтобы получить состояние:

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

Функция PyType_GetModuleByDef() выполняет поиск в порядке разрешения методов (то есть среди всех суперклассов) первого суперкласса, связанного с соответствующим модулем.

Примечание

В очень необычных случаях (цепочки наследования, проходящие через несколько модулей, созданных на основе одного определения) PyType_GetModuleByDef() может вернуть не модуль истинного класса определения. Однако функция всегда вернёт модуль с тем же определением, обеспечивающий совместимую раскладку памяти C.

Время жизни состояния модуля

При сборке объекта модуля сборщиком мусора освобождается и его состояние. Для каждого указателя на состояние модуля (или его часть) необходимо удерживать ссылку на объект модуля.

Обычно это не проблема, поскольку типы, созданные с помощью PyType_FromModuleAndSpec(), и их экземпляры удерживают ссылку на модуль. Однако при обращении к состоянию модуля из других мест, например из обратных вызовов внешних библиотек, необходимо внимательно следить за счётчиком ссылок.

Нерешённые вопросы

Некоторые вопросы, связанные с состоянием на уровне модуля и типами в куче, всё ещё остаются открытыми.

Обсуждать улучшение ситуации лучше всего в списке рассылки capi-sig.

Область видимости на уровне класса

В настоящее время (в Python 3.11) невозможно привязать состояние к отдельным типам, не полагаясь на особенности реализации CPython (которые могут измениться в будущем — возможно, как ни парадоксально, чтобы обеспечить надлежащее решение для области видимости на уровне класса).

Преобразование типов в кучу без потерь

API типов в куче не разрабатывался для «без потерь» преобразования статических типов, то есть для создания типа, который работает в точности как заданный статический тип.

© 2001–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.12/howto/isolating-extensions.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API