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

Кто должен это прочитать

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

Фон

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

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

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

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

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

Ввод состояния каждого модуля

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

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

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

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

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

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

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

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

Удивительные крайние случаи

Обратите внимание, что изолированные модули создают некоторые удивительные крайние случаи. В частности, каждый объект модуля, как правило, не будет делиться своими классами и исключениями с другими подобными модулями. Продолжая разговор с example above, обратите внимание, что 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, так и для других языков). Если это невозможно, рассмотрите возможность явной блокировки.

Если необходимо использовать глобальное состояние процесса, то самый простой способ избежать проблем с несколькими интерпретаторами - это явно запретить загрузку модуля более одного раза на процесс - см. Opt-Out: Limiting to One Module Object per Process.

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

Чтобы использовать состояние для каждого модуля, используйте multi-phase extension module initialization. Это сигнализирует о том, что ваш модуль корректно поддерживает несколько интерпретаторов.

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

Примечание

Другой вариант - хранить состояние в __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.

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

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

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

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

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

  • В отличие от статических типов, объекты типа heap по умолчанию мутабельны. Чтобы предотвратить мутабельность, используйте флаг 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 и ниже, функция traverse не должна не посещать тип, поэтому она должна быть более сложной:

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. В качестве замены используйте:

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

Если ваша функция 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, его refcount должен быть уменьшен после деаллокации экземпляра. Например:

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 используйте size.

  • Если вышеописанное невозможно (например, внутри пользовательского 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() работает путем поиска в method resolution order (т.е. во всех суперклассах) первого суперкласса, у которого есть соответствующий модуль.

Примечание

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

Срок службы модуля Состояние

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

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

Открытые вопросы

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

Обсуждать улучшение ситуации лучше всего на capi-sig mailing list.

Область применения для каждого класса

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

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

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