Изолирующие модули расширения¶
Кто должен это прочитать¶
Это руководство написано для разработчиков расширений C-API, которые хотели бы сделать это расширение более безопасным для использования в приложениях, где сам Python используется в качестве библиотеки.
Фон¶
Интерпретатор - это контекст, в котором выполняется код Python. Он содержит конфигурацию (например, путь импорта) и состояние выполнения (например, набор импортируемых модулей).
Python поддерживает запуск нескольких интерпретаторов в одном процессе. Следует подумать о двух случаях — пользователи могут запускать интерпретаторы:
последовательно, с несколькими циклами
Py_InitializeEx()
/Py_FinalizeEx()
, ипараллельно управляйте «вспомогательными интерпретаторами», используя
Py_NewInterpreter()
/Py_EndInterpreter()
.
Оба варианта (и их комбинации) были бы наиболее полезны при встраивании Python в библиотеку. Библиотеки, как правило, не должны делать предположений о приложении, которое их использует, включая предположение о «главном интерпретаторе Python» для всего процесса.
Исторически сложилось так, что модули расширения Python плохо справляются с этим вариантом использования. Многие модули расширения (и даже некоторые модули stdlib) используют глобальное состояние для каждого процесса, поскольку переменные C static
чрезвычайно просты в использовании. Таким образом, данные, которые должны быть специфичны для интерпретатора, в конечном итоге передаются другим интерпретаторам. Если разработчик расширения не будет осторожен, очень легко создать непредвиденные ситуации, которые приводят к сбоям, когда модуль загружается более чем в один интерпретатор в одном и том же процессе.
К сожалению, достичь состояния «для каждого интерпретатора» непросто. Авторы расширений, как правило, не учитывают наличие нескольких интерпретаторов при разработке, и в настоящее время тестирование поведения является обременительным.
Ввод состояния для каждого модуля¶
Вместо того, чтобы фокусироваться на состоянии каждого интерпретатора, C API Python развивается, чтобы лучше поддерживать более детализированное состояние каждого модуля. Это означает, что данные на уровне C привязываются к объекту модуля. Каждый интерпретатор создает свой собственный объект модуля, сохраняя данные отдельно. Для тестирования изоляции в один интерпретатор можно даже загрузить несколько объектов модуля, соответствующих одному расширению.
Состояние каждого модуля позволяет легко определить срок службы и владение ресурсами: модуль расширения инициализируется при создании объекта модуля и очищается при его освобождении. В этом отношении модуль ничем не отличается от любого другого: c:expr:PyObject *; в нем нет зацепок «выключение интерпретатора», о которых нужно подумать или забыть.
Обратите внимание, что существуют варианты использования для различных типов «глобальных параметров»: для каждого процесса, для каждого интерпретатора, для каждого потока или для каждой задачи. Если по умолчанию используется состояние для каждого модуля, это все еще возможно, но вы должны относиться к ним как к исключительным случаям: если они вам нужны, вы должны уделить им дополнительное внимание и протестировать. (Обратите внимание, что в этом руководстве они не рассматриваются).
Изолированные модульные объекты¶
Ключевым моментом, который следует иметь в виду при разработке модуля расширения, является то, что несколько объектов модуля могут быть созданы из одной общей библиотеки. Например:
>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii # create a new module object
>>> old_binascii == binascii
False
Как правило, два модуля должны быть полностью независимыми. Все объекты и состояния, относящиеся к модулю, должны быть инкапсулированы в объект module, не использоваться совместно с другими объектами модуля и очищаться при освобождении объекта module. Поскольку это просто эмпирическое правило, возможны исключения (см. Managing Global State), но они требуют большего внимания к крайним случаям.
В то время как некоторые модули могли бы обойтись менее строгими ограничениями, изолированные модули облегчают установление четких ожиданий и руководящих принципов, которые работают в различных вариантах использования.
Неожиданные крайние случаи¶
Обратите внимание, что изолированные модули действительно создают некоторые неожиданные пограничные ситуации. Наиболее примечательно, что каждый объект module обычно не использует свои классы и исключения совместно с другими подобными модулями. Продолжая с 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 или других языков). Если это невозможно, рассмотрите возможность явной блокировки.
Если необходимо использовать process-global state, самый простой способ избежать проблем с несколькими интерпретаторами — это явно запретить загрузку модуля более одного раза для каждого процесса - см. Opt-Out: Limiting to One Module Object per Process.
Управление состоянием каждого модуля¶
Чтобы использовать состояние для каждого модуля, используйте multi-phase extension module initialization. Это означает, что ваш модуль правильно поддерживает несколько интерпретаторов.
Установите PyModuleDef.m_size
в положительное число, чтобы запросить определенное количество байт памяти, локальной для модуля. Обычно это значение устанавливается равным размеру определенного модуля struct
, который может хранить все состояние модуля на уровне C. В частности, именно здесь вы должны поместить указатели на классы (включая исключения, но исключая статические типы) и настройки (например, csv
’s field_size_limit
), которые необходимы коду C для функционирования.
Примечание
Другой вариант - сохранить состояние в __dict__
модуля, но вы должны избегать сбоев, когда пользователи изменяют __dict__
из кода Python. Обычно это означает проверку на наличие ошибок и типов на уровне C, в которой легко ошибиться и которую трудно должным образом протестировать.
Однако, если состояние модуля не требуется в коде на C, хорошей идеей будет сохранить его только в __dict__
.
Если состояние модуля содержит указатели PyObject
, объект module должен содержать ссылки на эти объекты и реализовывать перехватчики на уровне модуля 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 имеет изменяемую деталь реализации: счетчик ссылок. Изменения в refcount защищены GIL. Таким образом, код, который совместно использует любые объекты Python в разных интерпретаторах, неявно зависит от текущего GIL CPython для всего процесса.
Поскольку они неизменяемы и глобальны для процессов, статические типы не могут получить доступ к «своему» состоянию модуля. Если какой-либо метод такого типа требует доступа к состоянию модуля, этот тип должен быть преобразован в тип, выделенный для кучи, или, сокращенно, в тип кучи. Они более точно соответствуют классам, созданным с помощью инструкции Python class
.
Для новых модулей использование типов кучи по умолчанию является хорошим эмпирическим правилом.
Изменение статических типов на типы кучи¶
Статические типы могут быть преобразованы в типы кучи, но обратите внимание, что API типов кучи не был разработан для преобразования статических типов «без потерь», то есть для создания типа, который работает точно так же, как данный статический тип. Таким образом, при переписывании определения класса в новом API вы, скорее всего, непреднамеренно измените некоторые детали (например, возможность выбора или унаследованные слоты). Всегда проверяйте важные для вас детали.
Обратите особое внимание на следующие два пункта (но обратите внимание, что это неполный список).:
В отличие от статических типов, объекты типа 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. В качестве замены используйте:
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.
Если вы используете, используйте : c:func: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
), вызовите: c:func:PyObject_GC_New или:c:func: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},
}
Доступ к состоянию модуля из методов Slot, получателей и установщиков¶
Примечание
Это новое в Python 3.11.
Методы Slot — быстрые эквиваленты 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 (которые могут измениться в будущем — возможно, по иронии судьбы, чтобы обеспечить правильное решение для каждого класса).
Преобразование в типы кучи без потерь¶
API heap type не был разработан для преобразования статических типов «без потерь», то есть для создания типа, который работает точно так же, как данный статический тип.