Существует множество руководств о том, как создавать C расширений Python, которые вводят новый тип. Один пример: https://docs.python.org/3.5/extending/newtypes.html
Обычно это сводится к созданию такой структуры, как:
struct Example
{
PyObject_HEAD
// Extra members
};
, а затем регистрации ее в модуле путем неявного или явного определения функции сопоставления указателей. Связанные со сроком службы: tp_alloc, tp_new, tp_init, tp_free, tp_dealloc
.
Из того, что я понимаю, как это работает, PyObject_HEAD
расширяется до PyObject ob_base;
, что делает Example*
и PyObject*
конвертируемыми (я думаю, есть некоторые особые формулировка, если это первый член), поэтому весь код принимает PyObject*
и может работать с ним, как если бы использовалось struct Example: public PyObject{};
. Пока все хорошо.
Но теперь проблема заключается в сроке жизни, если Example
: После некоторого копания кажется, что происходит следующее:
tp_new
вызывается с " type_info "(отображение указателя на функцию) объекта для создания - это вызывает
tp_alloc
, который по умолчанию (в основном) malloc
- , затем
tp_init
вызывается с указателем памяти из tp_new
, который, например, заполняет счетчик ссылок - при уничтожении
tp_dealloc
называется - это вызывает
tp_free
(в основном free
)
Итак, что такое очевидно отсутствует вызов конструктора и деструктора, что на практике нормально, если структура представляет собой POD
Однако недавние стандарты C ++ ясно дали понять, что простого удаления объекта недостаточно, см., например, std::launder
и связанные с этим обсуждения.
Следовательно, компиляция такого C расширения как C ++ уже UB? Если нет, то для POD есть особое правило, так что они будут в безопасности, не так ли? Есть ли какие-либо ссылки для разъяснения?
Есть ли документация по безопасному способу создания типов, отличных от POD, эффективным способом? Т.е. не добавлять указатель к объекту Example
POD выше, который указывает на этот объект, не являющийся POD, который затем создается с помощью new
или аналогичного.
Из описания и ответа на Должен { tp_allo c, tp_dealloc} и {tp_new, tp_free} следует рассматривать как пары? Я бы выделил, что tp_new
мог бы сделать new
и вернуть его, или позвонить tp_alloc
и выполнить новое размещение на вернул память и вернул то. Для меня это звучит как требование «только столько дополнительной инициализации, сколько абсолютно необходимо». Затем tp_dealloc
вызовет деструктор и направит его на tp_free
. Звучит неплохо, но может быть проблема c, если выравнивание возвращаемой памяти tp_alloc
неверно?
Есть ли гарантии, что tp_new
и tp_dealloc
вызываются ровно один раз?
Некоторый псевдокод для программистов, отличных от Python, согласно приведенному выше описанию:
PyObject* tp_alloc(size_t n){ return malloc(n); }
PyObject* tp_new(PyTypeObject* typeInfo){ return typeinfo->tp_alloc(typeinfo->object_size); }
PyObject* tp_init(PyTypeObject* typeInfo, PyObject* o){ o->typeInfo = typeInfo; o->refCnt = 1; return o }
void tp_dealloc(PyObject* o){ o->typeInfo->tp_free(o); }
void tp_free(void* m){ free(m); }
//User code
struct Example
{
PyObject obj;
// Extra members
};
void register(){
PyTypeObject info = {.tp_alloc = tp_alloc, .tp_new = tp_new, .object_size = sizeof(Example), ...}
PythonRegister("Example", info);
}
Обратите внимание, что это упрощено. Затем Python будет использовать объект info
всякий раз, когда создается / используется тип имени «Пример». И вы можете переопределить все функции и преобразовать между Example*
и PyObject*
, хотя наследования нет, поскольку они «взаимопреобразуемы по указателю»:
один из них является объектом класса стандартного макета и другой - это первый нестатический c элемент данных этого объекта https://en.cppreference.com/w/cpp/language/static_cast
Моя идея заключалась в том, чтобы переопределить значение tp_new
по умолчанию примерно таким:
PyObject* Example_new(PyTypeObject* typeInfo){ return new(typeinfo->tp_alloc(typeinfo->object_size)) Example; }
Что я хотел знать, требуется ли это и действительно ли это вообще.