Spec-Zone.ru › Python 3.11

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

Аннотация

Традиционно, состояние, принадлежащее модулям расширения 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-уровня модуля. В частности, это то место, где вы должны поместить указатели на классы (включая исключения, но исключая статические типы) и настройки (например, field_size_limit csv), которые нужны коду 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 вы, вероятно, случайно измените несколько деталей (например, сериализуемость или унаследованные слоты). Всегда тестируйте детали, которые важны для вас.

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

  • В отличие от статических типов, объекты типа кучи по умолчанию изменяемы. Используйте флаг 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–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/howto/isolating-extensions.html

Spec-Zone.ru

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