Изоляция модулей расширения
Кому следует прочитать это руководство
Это руководство предназначено для сопровождающих расширений 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 означает, что модуль корректно поддерживает несколько интерпретаторов. Если ваш модуль пока этого не умеет, можно явно разрешить его загрузку только один раз за процесс. Например:
// A process-wide flag
static int loaded = 0;
// Mutex to provide thread safety (only needed for free-threaded Python)
static PyMutex modinit_mutex = {0};
static int
exec_module(PyObject* module)
{
PyMutex_Lock(&modinit_mutex);
if (loaded) {
PyMutex_Unlock(&modinit_mutex);
PyErr_SetString(PyExc_ImportError,
"cannot load module more than once per process");
return -1;
}
loaded = 1;
PyMutex_Unlock(&modinit_mutex);
// ... rest of initialization
}
Если функция PyModuleDef.m_clear вашего модуля может подготовить его к повторной инициализации, она должна сбросить флаг loaded. В этом случае модуль не будет поддерживать одновременное существование нескольких экземпляров, но, например, его можно будет загрузить после завершения работы среды выполнения Python (Py_FinalizeEx()) и повторной инициализации (Py_Initialize()).
Доступ к состоянию модуля из функций
Доступ к состоянию из функций уровня модуля несложен. Функции получают объект модуля в качестве первого аргумента; извлечь состояние можно с помощью 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(), и их экземпляры хранят ссылку на модуль. Однако при подсчёте ссылок необходимо проявлять осторожность, обращаясь к состоянию модуля из других мест, например из обратных вызовов внешних библиотек.
Нерешённые вопросы
Некоторые вопросы, связанные с состоянием на уровне модуля и типами в куче, остаются нерешёнными.
Обсуждения по улучшению ситуации лучше всего вести на форуме discuss, в теме c-api.
Область видимости на уровне класса
В настоящее время (в Python 3.11) невозможно связать состояние с отдельными типами, не полагаясь на особенности реализации CPython (которые могут измениться в будущем — возможно, как ни парадоксально, чтобы обеспечить правильное решение для области видимости на уровне класса).
Преобразование типов в куче без потерь
API типов в куче не предназначен для преобразования статических типов «без потерь», то есть для создания типа, работающего в точности как заданный статический тип.
© 2001 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/howto/isolating-extensions.html