Предотвращение падения одного из множества независимых Python объектов до 0 и уничтожение указателя C, от которого зависят другие Python объекты - PullRequest
1 голос
/ 10 января 2020

Я работаю с устаревшей библиотекой C, которую обернул расширением Python C. Библиотека C имеет рекурсивную структуру данных Foo с API, подобным приведенному ниже:

Foo *Foo_create(void)  /* Create new Foo memory */

int Foo_push(Foo *parent, int field, Foo *child)  /* Add a child Foo to a parent Foo */

int Foo_destroy(Foo *foo) /* Framework will free all children, caller cannot reuse children after */

Foo *Foo_pop(Foo *parent, int field) /* User responsible for calling Foo_destroy on popped field */

У меня есть структура PyFoo, которая охватывает Foo, что-то вроде:

typedef struct {
    PyObject_HEAD
    Foo *foo;
    PyObject *parent;
} PyFoo;

Как и другие функции, которые оборачивают функции Foo_ * и увеличивают / уменьшают соответствующим образом.

Проблема, с которой я столкнулся, заключается в том, что два независимых объекта PyFoo с независимыми ссылками могут указывать на тот же Фу *. Если один из объектов PyFoo выходит из области видимости, он вызывает Foo_destroy, но пользователь может получить доступ ко второму объекту PyFoo и вызвать ошибку сегментации.

Я пытаюсь помешать пользователю моей библиотеки сделать в Python:

parent = Foo()     # Foo_create();  parent's refcount is 1
a = Foo()          # Foo_create();  a's refcount is 1
parent[1] = a      # Foo_push(parent, 1, a); parent's refcount is 2; a's refcount is 1

b = parent.pop(1)  # Foo_pop(parent, 1); 
# parent's refcount is 1; a's refcount is 1; b's refcount is 1
# a and b's are now independent PyFoo objects with reference count = 1
# HOWEVER both of the *foo pointers point to the same memory

# Delete a, dropping reference count to 0, which calls Foo_destroy
del a              # parents refcount is 1; a's refcount is 0; b's refcount is 1

# Access b, which may segfault, since Foo_destroy was called in the last call.
print(b)

Другими словами, a и b оба указывают на одну и ту же Foo память. Однако они являются независимыми Python объектами с независимыми счетами. Как только a выходит из области видимости, он уничтожает память, на которую указывает b. Доступ к b, вероятно, приведет к segfault.

Кажется, что это будет распространенной проблемой при написании Python Extensions.

Полагаю, мне нужен способ подсчета ссылок на указателе Foo. Например, a и b должны иметь одинаковую идентичность в приведенном выше примере. Или, возможно, мне нужна какая-то структура данных, которая подсчитывает количество PyFoos, которые используют один и тот же указатель Foo, а Foo_destroy вызывается только тогда, когда количество указателей Foo падает до 0.

Что такое идиоматизм c способ решить эту проблему?


Вот соответствующий сценарий в C:

Foo *parent = Foo_create();
Foo *a = Foo_create();
Foo_push(parent, 1, a);
Foo *b = Foo_pop(parent, 1);
/* a and b both point to same memory */
Foo_destroy(a);
/* better not access b after this */
a = NULL;
b = NULL;

Ответы [ 3 ]

1 голос
/ 11 января 2020

Не уверен насчет "идиоматического c пути", но в cppyy (http://cppyy.org) я отслеживаю python объектов (по типу) для сохранения идентичности и pybind11 (https://pybind11.readthedocs.io) делает что-то похожее, так что это работоспособная идея.

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

Для реализации сохраняем карту ha sh указателя C на объект Python. При возврате земли Foo* в Python проверьте, существует ли она уже на карте, и при необходимости используйте повторно. Когда ref-count достигнет 0, также удалите объект с карты. Обратите внимание, что вам НЕ нужно увеличивать счетчик ссылок или сохранять слабые ссылки, поскольку карта ha sh никогда не покидает C -land.

Кроме того, если вы контролируете уничтожение Foo в C -land, тогда я рекомендую обратный вызов для установки Python прокси Foo* на NULL и проверки на NULL во всех функциях доступа (cppyy тоже делает что-то подобное, если C ++ обеспечивает обратные вызовы) .

РЕДАКТИРОВАТЬ : добавление ссылок на код здесь, в противном случае у меня заканчиваются символы.

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

Я собираю ссылки для каждого типа по соображениям производительности (делает карты меньше) см. fCppObjects здесь: https://bitbucket.org/wlav/cpycppyy/raw/c6e7662bab1623e6cb15ddf59e94423a6081d66f/src/CPPScope.h

Когда возвращается новый прокси-сервер, несущий указатель на C ++, указанный объект регистрируется через MemoryRegulator, и когда объект удаляется, он незарегистрирован: https://bitbucket.org/wlav/cpycppyy/raw/c6e7662bab1623e6cb15ddf59e94423a6081d66f/src/MemoryRegulator.h https://bitbucket.org/wlav/cpycppyy/raw/c6e7662bab1623e6cb15ddf59e94423a6081d66f/src/MemoryRegulator.cxx

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

Использование flags из соображений производительности в нескольких угловых случаях.

Поиск / регистрация происходит в разных местах, поскольку объекты могут пересекать границу по разным причинам (построение, возврат функции, доступ к переменной). Функция возвращает здесь: https://bitbucket.org/wlav/cpycppyy/raw/c6e7662bab1623e6cb15ddf59e94423a6081d66f/src/ProxyWrappers.cxx

ищите вызов в BindCppObjectNoCast.

Уничтожение происходит, когда объект исчезает, см. Прокси-класс: https://bitbucket.org/wlav/cpycppyy/raw/c6e7662bab1623e6cb15ddf59e94423a6081d66f/src/CPPInstance.cxx и, в частности, op_dealloc_nofree (помощник для удаления стороны C ++, но не (пока) Python), который вызывается из обычного tp_dealloc.

Для pybind11, функции называются register_instance и deregister_instance, которые вы можете найти здесь: https://raw.githubusercontent.com/pybind/pybind11/master/include/pybind11/detail/class.h

Регистрация происходит в одной мультикарте под названием registered_instances, которая находится здесь : https://raw.githubusercontent.com/pybind/pybind11/master/include/pybind11/detail/internals.h

Поиск находится в get_object_handle, найденном здесь: https://raw.githubusercontent.com/pybind/pybind11/master/include/pybind11/cast.h, который выполняет сопоставление ptr и типа.

Ie. почти то же самое, что и cppyy (только менее эффективный).

1 голос
/ 10 января 2020

Я подозреваю, что у вас нет информации для "автоматического" использования одного и того же PyFoo объекта, и вы вполне можете в конечном итоге продублировать большую часть внутренней Foo структуры в PyFoo, если попытаетесь сохранить it.

Один довольно простой вариант, который приходит мне в голову, это иметь внутреннее dict отображение Foo* на PyFoo объект. Поэтому вы создаете новый PyFoo только при необходимости, но в противном случае повторно используете существующий объект. Очевидно, что Foo* не является Python объектом, поэтому его нельзя сохранить непосредственно в dict, но вы можете достаточно легко сохранить его как целое число, используя PyLong_FromVoidPtr. Используйте WeakValueDictionary, чтобы удерживать PyFoo с, чтобы вы не поддерживали их только благодаря тому, что находитесь в словаре.

Схема вашего переноса Foo_pop будет выглядеть примерно так:

PyObject* PyFoo_pop(args...) {
    Foo* popped = Foo_pop(args...);
    PyObject* pf = PyObject_GetItem(internal_dictionary_of_pyfoos,
                                  PyLong_FromVoidPtr(popped));
    if (pf == NULL) {
       pf = create_a_new_PyFoo(popped);
    }
    return pf;
}

create_a_new_PyFoo, очевидно, необходимо добавить PyFoo в словарь при его создании.

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


WeakValueDictionary: как вы скажем, к нему доступ через интерфейс Python. Код, по сути, является просто C версией того, что вы будете делать в Python. Примерно:

PyObject *weakref_mod = PyImport_ImportModule("weakref");
PyObject *weakvaluedict = PyObject_GetAttrString(weakref_mod, "WeakValueDictionary");
PyObject *wd_instance = PyObject_CallFunctionObjArgs(weakvaluedict, NULL);

(не проверено и игнорируется проверка ошибок). Обратите внимание, что это не прямой подкласс dict, я думаю, поэтому используйте PyObject_GetItem, а не PyDict_GetItem (который ведет себя немного иначе и возвращает что-то с увеличенной ссылкой)


PyFoo: Обратите внимание, что C API-типы нуждаются в небольших модификациях для слабой ссылки. Пример приведен в документах , но примерно для них требуется PyObject* для хранения списка слабых ссылок и tp_weakreflistoffset, установленный в объекте типа. Это, очевидно, добавляет немного накладных расходов.

0 голосов
/ 11 января 2020

del a не сбрасывает счетчик ссылок до 0 сам по себе; он только уменьшает счетчик ссылок, потому что он удаляет одну ссылку. b по-прежнему ссылается на объект, поэтому счетчик ссылок остается на 1, и не должно быть вызова Foo_destroy.

...