Содержание страницы
Изоляция модулей расширений¶Isolating Extension Modules
Кому следует это прочитать¶Who should read this
Это руководство предназначено для мейнтейнеров расширений C-API, которые хотят сделать это расширение более безопасным в приложениях, где сам Python используется как библиотека.
Предыстория¶Background
Интерпретатор – это среда выполнения кода Python. Он содержит конфигурацию (например, путь импорта) и состояние времени выполнения (например, набор импортированных модулей).
Python поддерживает запуск нескольких интерпретаторов в одном процессе. Рассмотрим два случая: пользователи могут запускать интерпретаторы
последовательно, в несколько
Py_InitializeEx()/Py_FinalizeEx()циклов, ипараллельно, управляя «субинтерпретаторами» с помощью
Py_NewInterpreter()/Py_EndInterpreter().
Оба случая (и их комбинации) наиболее полезны при встраивании Python в библиотеку. Библиотеки обычно не должны строить предположения о приложении, которое их использует, в том числе предполагать наличие «главного интерпретатора Python» в рамках процесса.
Исторически сложилось, что расширения Python плохо обрабатывают этот вариант использования.
Многие расширения (и даже некоторые модули из stdlib) используют глобальное состояние процесса,
потому что C-переменные static чрезвычайно просты в использовании.
Таким образом, данные, которые должны быть специфичны для конкретного интерпретатора, в итоге
разделяются между интерпретаторами. Если разработчик расширения не будет внимателен, очень
легко ввести краевые случаи, ведущие к аварийным завершениям, когда модуль загружается
в нескольких интерпретаторах одного процесса.
К сожалению, состояние на интерпретатор достичь непросто. Авторы расширений обычно не думают о нескольких интерпретаторах при разработке, и в настоящее время тестировать такое поведение сложно.
Состояние модуля¶Enter Per-Module State
Вместо акцента на состоянии на уровне интерпретатора C API Python развивается в сторону более гранулярной поддержки состояния на уровне модуля. Это означает, что данные на уровне C привязываются к объекту модуля. Каждый интерпретатор создаёт собственный объект модуля, благодаря чему данные остаются разделёнными. Для проверки изоляции в одном интерпретаторе можно даже загрузить несколько объектов модуля, соответствующих одному расширению.
Состояние на модуль даёт простой способ мыслить о времени жизни и владении ресурсами: расширение инициализируется при создании объекта модуля и очищается при его освобождении. В этом отношении модуль – это просто любой другой PyObject*; не нужно думать – или забывать – о хуках «при завершении интерпретатора».
Обратите внимание, что существуют варианты использования разных видов «глобальных» переменных: состояние на процесс, на интерпретатор, на поток или на задачу. При состоянии на модуль как умолчании они всё ещё возможны, но их следует рассматривать как исключительные случаи: если они нужны, нужно уделить им дополнительное внимание и тестирование. (Заметим, что данное руководство их не рассматривает.)
Изолированные объекты модулей¶Isolated Module Objects
Ключевой момент, который следует помнить при разработке расширения: из одной разделяемой библиотеки может быть создано несколько объектов модуля. Например:
>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii # создать новый объект модуля
>>> old_binascii == binascii
False
Как правило, два модуля должны быть полностью независимы. Все объекты и состояние, специфичные для модуля, должны быть инкапсулированы внутри объекта модуля, не разделяться с другими объектами модулей и очищаться при освобождении объекта модуля. Поскольку это лишь общее правило, возможны исключения (см. раздел Управление глобальным состоянием), но они потребуют больше размышлений и внимания к краевым случаям.
Хотя некоторые модули могли бы обойтись менее строгими ограничениями, изолированные модули упрощают установление чётких ожиданий и правил, работающих для множества вариантов использования.
Неожиданные краевые случаи¶Surprising Edge Cases
Заметьте, что изолированные модули действительно создают некоторые неожиданные краевые случаи. В частности,
каждый объект модуля обычно не разделяет свои классы и
исключения с другими аналогичными модулями. Продолжая
пример выше,
заметьте, что 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 «вручную» считается
хаком.
Обеспечение безопасности модулей при нескольких интерпретаторах¶Making Modules Safe with Multiple Interpreters
Управление глобальным состоянием¶Managing Global State
Иногда состояние, связанное с модулем Python, не является специфичным для этого модуля, а относится ко всему процессу (или чему-то «более глобальному», чем модуль). Например:
Модуль
readlineуправляет терминалом.Модуль, работающий на плате, хочет управлять встроенным светодиодом.
В таких случаях модуль Python должен предоставлять доступ к глобальному состоянию, а не владеть им. Если возможно, напишите модуль так, чтобы несколько его копий могли обращаться к состоянию независимо (наряду с другими библиотеками, будь то для Python или других языков). Если это невозможно, рассмотрите явную блокировку.
Если необходимо использовать глобальное состояние процесса, самый простой способ избежать проблем с несколькими интерпретаторами – явно запретить загрузку модуля более одного раза на процесс – см. Отказ: ограничение до одного объекта модуля на процесс.
Управление состоянием на модуль¶Managing Per-Module State
Чтобы использовать состояние на модуль, используйте многофазную инициализацию расширения. Это сигнализирует, что ваш модуль корректно поддерживает несколько интерпретаторов.
Установите 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; пример инициализации модуля приведён в нижней части файла.
Отказ: ограничение одним объектом модуля на процесс¶Opt-Out: Limiting to One Module Object per Process
Неотрицательное значение 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;
// остальная часть инициализации
}
Доступ к состоянию модуля из функций¶Module State Access from Functions
Доступ к состоянию из функций уровня модуля не представляет сложности.
Функции получают объект модуля в качестве первого аргумента; для извлечения
состояния можно использовать PyModule_GetState:
static PyObject *
func(PyObject *module, PyObject *args)
{
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
// остальная часть логики
}
Примечание
PyModule_GetState может вернуть NULL без установки исключения, если состояния модуля нет, т.е. PyModuleDef.m_size был нулевым. В вашем модуле управление m_size принадлежит вам, так что это легко предотвратить.
Кучные типы¶Heap Types
Традиционно типы, определённые в коде на C, являются статическими; то есть это
структуры static PyTypeObject, определяемые непосредственно в коде и
инициализируемые с помощью PyType_Ready().
Такие типы обязательно являются общими для всего процесса. Совместное использование их
между объектами модулей требует внимания к любому состоянию, которым они владеют
или к которому обращаются. Чтобы ограничить возможные проблемы, статические типы неизменяемы на
уровне Python: например, нельзя задать str.myattribute = 123.
Особенность реализации CPython: Совместное использование по-настоящему неизменяемых объектов между интерпретаторами не вызывает проблем, пока они не предоставляют доступа к изменяемым объектам. Однако в CPython каждый объект Python содержит изменяемую деталь реализации: счётчик ссылок. Изменения счётчика ссылок защищены GIL. Таким образом, код, разделяющий любые объекты Python между интерпретаторами, неявно зависит от текущего, общепроцессного GIL в CPython.
Поскольку статические типы неизменяемы и глобальны для процесса, они не могут получить доступ
к «своему» состоянию модуля.
Если какой-либо метод такого типа требует доступа к состоянию модуля,
тип необходимо преобразовать в тип, размещаемый в куче, или, сокращённо, кучный тип.
Они больше соответствуют классам, создаваемым оператором class в Python.
Для новых модулей использование кучных типов по умолчанию является хорошим эмпирическим правилом.
Преобразование статических типов в кучные типы¶Changing Static Types to Heap Types
Статические типы можно преобразовать в кучные, но учтите, что API кучных типов не был разработан для преобразования «без потерь» из статических типов – то есть для создания типа, работающего в точности как данный статический тип. Поэтому при переписывании определения класса в новом API вы, вероятно, непреднамеренно измените некоторые детали (например, возможность сериализации или унаследованные слоты). Всегда тестируйте детали, важные для вас.
Обратите особое внимание на следующие два пункта (но учтите, что это не исчерпывающий список):
В отличие от статических типов, объекты кучных типов по умолчанию изменяемы. Используйте флаг
Py_TPFLAGS_IMMUTABLETYPE, чтобы предотвратить изменяемость.Кучные типы по умолчанию наследуют
tp_new, поэтому может стать возможным создание их экземпляров из кода Python. Вы можете предотвратить это с помощью флагаPy_TPFLAGS_DISALLOW_INSTANTIATION.
Определение кучных типов¶Defining Heap Types
Кучные типы можно создать, заполнив структуру PyType_Spec, которая является
описанием или «чертежом» класса, и вызвав
PyType_FromModuleAndSpec() для создания нового объекта класса.
Примечание
Другие функции, такие как PyType_FromSpec(), также могут создавать
кучные типы, но PyType_FromModuleAndSpec() связывает модуль
с классом, предоставляя доступ к состоянию модуля из методов.
Класс обычно следует хранить как в состоянии модуля (для
безопасного доступа из C), так и в __dict__ модуля (для доступа из
кода Python).
Протокол сборки мусора¶Garbage-Collection Protocol
Экземпляры кучных типов содержат ссылку на свой тип. Это гарантирует, что тип не будет уничтожен до того, как будут уничтожены все его экземпляры, но может привести к циклам ссылок, которые должен разорвать сборщик мусора.
Чтобы избежать утечек памяти, экземпляры кучных типов должны реализовывать протокол сборки мусора. А именно, кучные типы должны:
Иметь флаг
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 in Python 3.8 and lower
Требование посещать тип из 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¶Delegating tp_traverse
Если функция обхода делегирует tp_traverse
своего базового класса (или другого типа), убедитесь, что Py_TYPE(self) посещается
только один раз.
Обратите внимание, что только тип кучи должен посещать тип в tp_traverse.
Например, если ваша функция обхода включает:
base->tp_traverse(self, visit, arg)
…и base может быть статическим типом, тогда она также должна включать:
if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) {
// tp_traverse типа‑кучи уже посетил Py_TYPE(self)
} else {
if (Py_Version >= 0x03090000) {
Py_VISIT(Py_TYPE(self));
}
}
Нет необходимости обрабатывать счетчик ссылок типа в
tp_new и tp_clear.
Определение tp_dealloc¶Defining 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¶Not overriding tp_free
Слот tp_free типа кучи должен быть установлен в
PyObject_GC_Del().
Это значение по умолчанию; не переопределяйте его.
Избегание PyObject_New¶Avoiding 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)на то же самое, но использовать 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);
Доступ к состоянию модуля из классов¶Module State Access from Classes
Если у вас есть объект типа, определенный с PyType_FromModuleAndSpec(),
вы можете вызвать PyType_GetModule(), чтобы получить связанный модуль, а затем
PyModule_GetState(), чтобы получить состояние модуля.
Чтобы избавиться от утомительного шаблонного кода обработки ошибок, вы можете объединить
эти два шага с помощью PyType_GetModuleState(), получив:
my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (state === NULL) {
return NULL;
}
Доступ к состоянию модуля из обычных методов¶Module State Access from Regular Methods
Доступ к состоянию на уровне модуля из методов класса несколько сложнее, но возможен благодаря 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, // объект, на котором вызван метод
PyTypeObject *defining_class, // определяющий класс
PyObject *const *args, // C‑массив аргументов
Py_ssize_t nargs, // длина "args"
PyObject *kwnames) // NULL или словарь именованных аргументов
Получив определяющий класс, вызовите 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;
}
... // остальная логика
}
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},
}
Доступ к состоянию модуля из слотовых методов, геттеров и сеттеров¶Module State Access from Slot Methods, Getters and Setters
Примечание
Это новое в 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.
Время жизни состояния модуля¶Lifetime of the Module State
Когда объект модуля собирается сборщиком мусора, его состояние освобождается. Для каждого указателя на (часть) состояния модуля необходимо удерживать ссылку на объект модуля.
Обычно это не проблема, потому что типы, созданные с помощью
PyType_FromModuleAndSpec(), и их экземпляры хранят ссылку
на модуль.
Однако нужно быть осторожным с подсчётом ссылок, когда вы ссылаетесь
на состояние модуля из других мест, например, из колбэков внешних
библиотек.
Открытые вопросы¶Open Issues
Несколько вопросов, связанных с состоянием на модуль и кучевыми типами, всё ещё открыты.
Обсуждения по улучшению ситуации лучше всего вести в capi-sig списке рассылки.
Область видимости для класса¶Per-Class Scope
На данный момент (начиная с Python 3.11) невозможно привязать состояние к отдельным типам, не полагаясь на детали реализации CPython (которые могут измениться в будущем – возможно, по иронии судьбы, чтобы обеспечить правильное решение для области видимости на класс).
Преобразование в кучевые типы без потерь¶Lossless Conversion to Heap Types
API кучевых типов не был разработан для «без потерь» преобразования из статических типов; то есть для создания типа, который работает точно так же, как заданный статический тип.