Почему PyCXX обрабатывает классы нового стиля таким же образом?

Я выделяю некоторый код оболочки C ++ Python, который позволяет потребителю создавать классы Python старого и нового стиля из C ++.

Оригинальный код прибывает из PyCXX, со старым и новым стилем занятий Вот а также Вот. Однако я существенно переписал код, и в этом вопросе я буду ссылаться на свой собственный код, поскольку он позволяет мне представить ситуацию с максимальной ясностью, которую я могу. Я думаю, что было бы очень мало людей, способных понять исходный код без нескольких дней проверки … Для меня это заняло несколько недель, и я до сих пор не уверен в этом.

Старый стиль просто происходит от PyObject,

template<typename FinalClass>
class ExtObj_old : public ExtObjBase<FinalClass>
// ^ which : ExtObjBase_noTemplate : PyObject
{
public:
// forwarding function to mitigate awkwardness retrieving static method
// from base type that is incomplete due to templating
static TypeObject& typeobject() { return ExtObjBase<FinalClass>::typeobject(); }

static void one_time_setup()
{
typeobject().set_tp_dealloc( [](PyObject* t) { delete (FinalClass*)(t); } );

typeobject().supportGetattr(); // every object must support getattr

FinalClass::setup();

typeobject().readyType();
}

// every object needs getattr implemented to support methods
Object getattr( const char* name ) override { return getattr_methods(name); }
// ^ MARKER1

protected:
explicit ExtObj_old()
{
PyObject_Init( this, typeobject().type_object() ); // MARKER2
}

Когда вызывается one_time_setup (), он принудительно (посредством доступа к базовому классу typeobject()) создание связанных PyTypeObject для этого нового типа.

Позже, когда создается экземпляр, он использует PyObject_Init

Все идет нормально.

Но новый класс стилей использует гораздо более сложную технику. Я подозреваю, что это связано с тем, что новые классы стилей допускают деривацию.

И это мой вопрос, почему обработка класса нового стиля реализована так, как она есть? Почему нужно создавать эту дополнительную структуру PythonClassInstance? Почему он не может делать то же самое, что и обработка классов в старом стиле? Т.е. просто введите convert из базового типа PyObject? И если он этого не делает, значит ли это, что он не использует свой базовый тип PyObject?

Это огромный вопрос, и я буду продолжать вносить поправки в пост, пока не убедлюсь, что он хорошо отражает проблему. Это не очень подходит для формата SO, я сожалею об этом. Однако некоторые инженеры мирового уровня часто посещают этот сайт (например, на один из моих предыдущих вопросов ответил ведущий разработчик GCC), и я ценю возможность обратиться к их опыту. Поэтому, пожалуйста, не спешите голосовать, чтобы закрыть.

Единовременная настройка нового класса стилей выглядит следующим образом:

template<typename FinalClass>
class ExtObj_new : public ExtObjBase<FinalClass>
{
private:
PythonClassInstance* m_class_instance;
public:
static void one_time_setup()
{
TypeObject& typeobject{ ExtObjBase<FinalClass>::typeobject() };

// these three functions are listed below
typeobject.set_tp_new(      extension_object_new );
typeobject.set_tp_init(     extension_object_init );
typeobject.set_tp_dealloc(  extension_object_deallocator );

// this should be named supportInheritance, or supportUseAsBaseType
// old style class does not allow this
typeobject.supportClass(); // does: table->tp_flags |= Py_TPFLAGS_BASETYPE

typeobject.supportGetattro(); // always support get and set attr
typeobject.supportSetattro();

FinalClass::setup();

// add our methods to the extension type's method table
{ ... typeobject.set_methods( /* ... */); }

typeobject.readyType();
}

protected:
explicit ExtObj_new( PythonClassInstance* self, Object& args, Object& kwds )
: m_class_instance{self}
{ }

Таким образом, новый стиль использует пользовательскую структуру PythonClassInstance:

struct PythonClassInstance
{
PyObject_HEAD
ExtObjBase_noTemplate* m_pycxx_object;
}

PyObject_HEAD, если я копаюсь в Python object.h, это просто макрос для PyObject ob_base; — никаких дальнейших осложнений, как #if #else. Поэтому я не понимаю, почему это не может быть просто

struct PythonClassInstance
{
PyObject ob_base;
ExtObjBase_noTemplate* m_pycxx_object;
}

или даже:

struct PythonClassInstance : PyObject
{
ExtObjBase_noTemplate* m_pycxx_object;
}

В любом случае, похоже, что его цель — пометить указатель на конец PyObject. Это будет связано с тем, что среда выполнения Python часто будет запускать функции, которые мы поместили в его таблицу функций, и первым параметром будет PyObject, отвечающий за вызов. Таким образом, это позволяет нам получить связанный объект C ++.

Но мы также должны сделать это для класса старого стиля.

Вот функция, ответственная за это:

ExtObjBase_noTemplate* getExtObjBase( PyObject* pyob )
{
if( pyob->ob_type->tp_flags & Py_TPFLAGS_BASETYPE )
{
/*
New style class uses a PythonClassInstance to tag on an additional
pointer onto the end of the PyObject
The old style class just seems to typecast the pointer back up
to ExtObjBase_noTemplate

ExtObjBase_noTemplate does indeed derive from PyObject
So it should be possible to perform this typecast
Which begs the question, why on earth does the new style class feel
the need to do something different?
This looks like a really nice way to solve the problem
*/
PythonClassInstance* instance = reinterpret_cast<PythonClassInstance*>(pyob);
return instance->m_pycxx_object;
}
else
return static_cast<ExtObjBase_noTemplate*>( pyob );
}

Мой комментарий выражает мое замешательство.

И здесь для полноты мы вставляем лямбда-батут в таблицу указателей на функции PyTypeObject, чтобы среда исполнения Python могла его запустить:

table->tp_setattro = [] (PyObject* self, PyObject* name, PyObject* val) -> int
{
try {
ExtObjBase_noTemplate* p = getExtObjBase( self );

return ( p -> setattro(Object{name}, Object{val}) );
}
catch( Py::Exception& ) { /* indicate error */
return -1;
}
};

(В этой демонстрации я использую tp_setattro, обратите внимание, что есть еще около 30 других слотов, которые вы можете увидеть, если посмотрите на документацию по PyTypeObject)

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

Итак, мы извлекаем «базовый тип для связанного объекта C ++» и вызываем его виртуальное setattro (просто используя setattro в качестве примера здесь). Производный класс будет переопределен setattro, и будет вызвано это переопределение.

Класс старого стиля обеспечивает такое переопределение, которое я обозначил MARKER1 — он находится в верхнем списке для этого вопроса.

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


PS для справки, я должен включить следующие методы из нового стилевого класса:

    static PyObject* extension_object_new( PyTypeObject* subtype, PyObject* args, PyObject* kwds )
{
PyObject* pyob = subtype->tp_alloc(subtype,0);
PythonClassInstance* o = reinterpret_cast<PythonClassInstance *>( pyob );
o->m_pycxx_object = nullptr;
return pyob;
}

^ для меня это выглядит абсолютно неправильно.
Похоже, что он выделяет память, повторно приводит к некоторой структуре, которая может превышать выделенное количество, а затем обнуляется прямо в конце этого.
Я удивлен, что это не вызвало никаких сбоев.
Я не вижу никаких признаков где-либо в исходном коде, что эти 4 байта принадлежат.

    static int extension_object_init( PyObject* _self, PyObject* _args, PyObject* _kwds )
{
try
{
Object args{_args};
Object kwds{_kwds};

PythonClassInstance* self{ reinterpret_cast<PythonClassInstance*>(_self) };

if( self->m_pycxx_object )
self->m_pycxx_object->reinit( args, kwds );
else
// NOTE: observe this is where we invoke the constructor, but indirectly (i.e. through final)
self->m_pycxx_object = new FinalClass{ self, args, kwds };
}
catch( Exception & )
{
return -1;
}
return 0;
}

^ обратите внимание, что нет никакой реализации для переустановки, кроме по умолчанию

virtual void    reinit ( Object& args  , Object& kwds    ) {
throw RuntimeError( "Must not call __init__ twice on this class" );
}static void extension_object_deallocator( PyObject* _self )
{
PythonClassInstance* self{ reinterpret_cast< PythonClassInstance* >(_self) };
delete self->m_pycxx_object;
_self->ob_type->tp_free( _self );
}

РЕДАКТИРОВАТЬ: Я рискну предположение, благодаря информации от Yhg1s на канале IRC.

Возможно, это потому, что когда вы создаете новый класс в старом стиле, гарантированно он будет полностью перекрывать структуру PyObject.

Следовательно, это безопасно для наследования от PyObject и передачи указателя на базовый PyObject в Python, что и делает класс старого стиля (MARKER2)

С другой стороны, новый класс стиля создает объект {PyObject + возможно что-то еще}.
то есть было бы небезопасно делать тот же трюк, поскольку среда выполнения Python заканчивала бы записью после окончания выделения базового класса (который является только PyObject).

Из-за этого нам нужно заставить Python выделять класс и возвращать нам указатель, который мы храним.

Поскольку мы больше не используем базовый класс PyObject для этого хранилища, мы не можем использовать удобный прием повторного ввода типов для извлечения связанного объекта C ++.
Это означает, что нам нужно пометить дополнительные байты sizeof (void *) до конца PyObject, который фактически выделяется, и использовать это для указания на связанный с нами экземпляр объекта C ++.

Однако здесь есть некоторое противоречие.

struct PythonClassInstance
{
PyObject_HEAD
ExtObjBase_noTemplate* m_pycxx_object;
}

^ если это действительно структура, которая выполняет вышеприведенное, то это говорит о том, что новый экземпляр класса стиля действительно вписывается точно в PyObject, то есть он не перекрывается с m_pycxx_object.

И если это так, то, конечно, весь этот процесс не нужен.

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

http://eli.thegreenplace.net/2012/04/16/python-object-creation-sequence
http://realmike.org/blog/2010/07/18/introduction-to-new-style-classes-in-python
Создайте объект с помощью Python C API

1

Решение

для меня это выглядит абсолютно неправильно. Похоже, что он выделяет память, повторно приводит к некоторой структуре, которая может превышать выделенное количество, а затем обнуляется прямо в конце этого. Я удивлен, что это не вызвало никаких сбоев. Я не вижу никаких признаков где-либо в исходном коде, что эти 4 байта принадлежат

PyCXX выделяет достаточно памяти, но делает это случайно. Это похоже на ошибку в PyCXX.

Объем памяти, выделяемый Python для объекта, определяется первым вызовом следующей статической функции-члена PythonClass<T>:

static PythonType &behaviors()
{
...
p = new PythonType( sizeof( T ), 0, default_name );
...
}

Конструктор PythonType устанавливает tp_basicsize объекта типа Python для sizeof(T), Таким образом, когда Python выделяет объект, который он знает, выделить как минимум sizeof(T) байт. Это работает потому что sizeof(T) оказывается, что больше sizeof(PythonClassInstance) (T происходит от PythonClass<T> который вытекает из PythonExtensionBase, что достаточно велико).

Тем не менее, это упускает из виду. На самом деле следует выделить только sizeof(PythonClassInstance) , Это похоже на ошибку в PyCXX — он выделяет слишком много, а не слишком мало места для хранения PythonClassInstance объект.

И это мой вопрос, почему обработка класса нового стиля реализована так, как она есть? Почему нужно создавать эту дополнительную структуру PythonClassInstance? Почему он не может делать то же самое, что и обработка классов в старом стиле?

Вот моя теория, почему новые классы стилей отличаются от классов старых стилей в PyCXX.

До Python 2.2, где были представлены новые классы стилей, не было tp_init член int тип объекта. Вместо этого вам нужно было написать фабричную функцию, которая создала бы объект. Вот как PythonExtension<T> должен работать — фабричная функция преобразует аргументы Python в аргументы C ++, просит Python выделить память, а затем вызывает конструктор с использованием размещения new.

Python 2.2 добавил новые классы стилей и tp_init член. Python сначала создает объект, а затем вызывает tp_init метод. Сохранение старого способа потребовало бы, чтобы объекты сначала имели фиктивный конструктор, который создает «пустой» объект (например, инициализирует все элементы как нулевые), а затем, когда tp_init называется, был бы дополнительный этап инициализации. Это делает код ужаснее.

Кажется, что автор PyCXX хотел избежать этого. PyCXX работает, сначала создавая манекен PythonClassInstance объект, а затем, когда tp_init называется, создает актуальный PythonClass<T> объект, используя его конструктор.

Означает ли это, что он не использует свой базовый тип PyObject??

Это кажется правильным, PyObject Базовый класс, кажется, нигде не используется. Все интересные методы PythonExtensionBase использовать виртуальный self() метод, который возвращает m_class_instance и полностью игнорировать PyObject Базовый класс.

Я предполагаю (только предположение, хотя), что это PythonClass<T> был добавлен в существующую систему, и было проще PythonExtensionBase вместо очистки кода.

3

Другие решения


По вопросам рекламы [email protected]