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 переменные чрезвычайно просты в использовании. Таким образом, данные, которые должны быть специфичными для интерпретатора, оказываются общими для разных интерпретаторов. Если разработчик расширения не внимателен, очень легко ввести граничные случаи, которые приводят к сбоям, когда модуль загружается в более чем одном интерпретаторе в одном процессе.

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

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

Вместо того, чтобы сосредотачиваться на состоянии по интерпретатору, API Python C развивается, чтобы лучше поддерживать более мелкозернистое состояние по модулю. Это означает, что данные на уровне 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, действующей на весь процесс.

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

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

Изменение статических типов на типы кучи

Статические типы могут быть преобразованы в типы кучи, но обратите внимание, что 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

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

Если вы используете 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