Изолирование модулей расширения
Кто должен прочитать это
Это руководство предназначено для разработчиков модулей расширения 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 вы, вероятно, непреднамеренно измените несколько деталей (например, сохраняемость или унаследованные слоты). Всегда тестируйте детали, которые важны для вас.
Обратите особое внимание на следующие два момента (хотя это не исчерпывающий список):
- В отличие от статических типов, объекты типа кучи по умолчанию изменяемы. Используйте флаг
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.13/howto/isolating-extensions.html