¿Cómo ordenar / arreglar la creación de PyCXX de la nueva clase de extensión Python?
Casi he terminado de reescribir un contenedor de Python C ++ (PyCXX).
El original permite clases de extensión de estilo antiguas y nuevas, pero también permite derivar de las clases de estilo nuevo:
import test
// ok
a = test.new_style_class();
// also ok
class Derived( test.new_style_class() ):
def __init__( self ):
test_funcmapper.new_style_class.__init__( self )
def derived_func( self ):
print( 'derived_func' )
super().func_noargs()
def func_noargs( self ):
print( 'derived func_noargs' )
d = Derived()
El código es enrevesado y parece contener errores (¿Por qué PyCXX maneja las clases de estilo nuevo de la manera en que lo hace?)
Mi pregunta es:¿Cuál es la justificación / justificación del mecanismo complicado de PyCXX? ¿Hay una alternativa más limpia?
Intentaré detallar a continuación dónde estoy con esta consulta. Primero intentaré describir lo que está haciendo PyCXX en este momento, luego describiré lo que creo que podría mejorarse.
Cuando el tiempo de ejecución de Python se encuentrad = Derived()
, lo hacePyObject_Call( ob ) where ob is the
PyTypeObjectfor
NewStyleClass. I will write
transmisión exterioras
NewStyleClass_PyTypeObject`.
Que PyTypeObject ha sido construido en C ++ y registrado usandoPyType_Ready
PyObject_Call
invocarátype_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
, devolviendo una instancia derivada inicializada, es decir
PyObject* derived_instance = type_call(NewStyleClass_PyTypeObject, NULL, NULL)
Algo como esto.
(Todo esto viene de (http://eli.thegreenplace.net/2012/04/16/python-object-creation-sequence por cierto, gracias Eli!)
type_call hace esencialmente:
type->tp_new(type, args, kwds);
type->tp_init(obj, args, kwds);
Y nuestro contenedor C ++ ha insertado funciones en eltp_new
ytp_init
ranuras deNewStyleClass_PyTypeObject
algo como esto:
typeobject.set_tp_new( extension_object_new );
typeobject.set_tp_init( extension_object_init );
:
static PyObject* extension_object_new( PyTypeObject* subtype,
PyObject* args, PyObject* kwds )
{
PyObject* pyob = subtype->tp_alloc(subtype,0);
Bridge* o = reinterpret_cast<Bridge *>( pyob );
o->m_pycxx_object = nullptr;
return pyob;
}
static int extension_object_init( PyObject* _self,
PyObject* args, PyObject* kwds )
{
Bridge* self{ reinterpret_cast<Bridge*>(_self) };
// NOTE: observe this is where we invoke the constructor,
// but indirectly (i.e. through final)
self->m_pycxx_object = new FinalClass{ self, args, kwds };
return 0;
}
Tenga en cuenta que debemos vincular la instancia derivada de Python y su instancia de clase C ++ correspondiente. (¿Por qué? Explicado a continuación, ver 'X'). Para hacer eso estamos usando:
struct Bridge
{
PyObject_HEAD // <-- a PyObject
ExtObjBase* m_pycxx_object;
}
Ahora este puente plantea una pregunta. Sospecho mucho de este diseño.
Observe cómo se asignó la memoria para este nuevo PyObject:
PyObject* pyob = subtype->tp_alloc(subtype,0);
Y luego escribimos este puntero aBridge
, y use el 4 u 8 (sizeof(void*)
) bytes inmediatamente después delPyObject
para apuntar a la instancia de clase C ++ correspondiente (esto se engancha enextension_object_init
como se puede ver arriba).
Ahora para que esto funcione, necesitamos:
una)subtype->tp_alloc(subtype,0)
debe estar asignando un extrasizeof(void*)
bytes b) ElPyObject
no requiere memoria más allásizeof(PyObject_HEAD)
, porque si lo hiciera, esto estaría en conflicto con el puntero anterior
Una pregunta importante que tengo en este momento es: ¿Podemos garantizar que elPyObject
que el tiempo de ejecución de Python ha creado para nuestroderived_instance
no se superpone en Bridge'sExtObjBase* m_pycxx_object
¿campo?
Intentaré responderlo: es EE. UU. El que determina cuánta memoria se asigna. Cuando creamosNewStyleClass_PyTypeObject
alimentamos la cantidad de memoria que queremos estoPyTypeObject
para asignar una nueva instancia de este tipo:
template< TEMPLATE_TYPENAME FinalClass >
class ExtObjBase : public FuncMapper<FinalClass> , public ExtObjBase_noTemplate
{
protected:
static TypeObject& typeobject()
{
static TypeObject* t{ nullptr };
if( ! t )
t = new TypeObject{ sizeof(FinalClass), typeid(FinalClass).name() };
/* ^^^^^^^^^^^^^^^^^ this is the bug BTW!
The C++ Derived class instance never gets deposited
In the memory allocated by the Python runtime
(controlled by this parameter)
This value should be sizeof(Bridge) -- as pointed out
in the answer to the question linked above
return *t;
}
:
}
class TypeObject
{
private:
PyTypeObject* table;
// these tables fit into the main table via pointers
PySequenceMethods* sequence_table;
PyMappingMethods* mapping_table;
PyNumberMethods* number_table;
PyBufferProcs* buffer_table;
public:
PyTypeObject* type_object() const
{
return table;
}
// NOTE: if you define one sequence method you must define all of them except the assigns
TypeObject( size_t size_bytes, const char* default_name )
: table{ new PyTypeObject{} } // {} sets to 0
, sequence_table{}
, mapping_table{}
, number_table{}
, buffer_table{}
{
PyObject* table_as_object = reinterpret_cast<PyObject* >( table );
*table_as_object = PyObject{ _PyObject_EXTRA_INIT 1, NULL };
// ^ py_object_initializer -- NULL because type must be init'd by user
table_as_object->ob_type = _Type_Type();
// QQQ table->ob_size = 0;
table->tp_name = const_cast<char *>( default_name );
table->tp_basicsize = size_bytes;
table->tp_itemsize = 0; // sizeof(void*); // so as to store extra pointer
table->tp_dealloc = ...
Puedes verlo entrar comotable->tp_basicsize
Pero ahora me parece claro que PyObject-s generado a partir deNewStyleClass_PyTypeObject
nunca requerirá memoria asignada adicional.
Lo que significa que todo estoBridge
El mecanismo es innecesario.
Y la técnica original de PyCXX para usar PyObject como una clase base deNewStyleClassCXXClass
, e inicializando esta base para que PyObject de Python runtimed = Derived()
es, de hecho, esta base, esta técnica se ve bien. Porque permite una conversión de texto perfecta.
Cada vez que el tiempo de ejecución de Python llama a un espacio desdeNewStyleClass_PyTypeObject
, pasará un puntero al PyObject de d como primer parámetro, y podemos simplemente escribir de nuevo aNewStyleClassCXXClass
. <- 'X' (mencionado anteriormente)
Entonces, realmente mi pregunta es: ¿por qué no hacemos esto?¿Hay algo especial en derivar deNewStyleClass
que fuerza la asignación extra para el PyObject?
Me doy cuenta de que no entiendo la secuencia de creación en el caso de una clase derivada. La publicación de Eli no cubrió eso.
Sospecho que esto puede estar relacionado con el hecho de que
static PyObject* extension_object_new( PyTypeObject* subtype, ...
^ este nombre de variable es 'subtipo' No entiendo esto, y me pregunto si esto puede contener la clave.
EDITAR: Pensé en una posible explicación de por qué PyCXX está usando sizeof (FinalClass) para la inicialización. Podría ser una reliquia de una idea que fue probada y descartada. es decir, si la llamada tp_new de Python asigna suficiente espacio para FinalClass (que tiene el PyObject como base), tal vez se pueda generar una nueva FinalClass en esa ubicación exacta usando 'ubicación nueva', o algún negocio astuto reinterpret_cast. Mi conjetura es que esto podría haberse intentado, se ha encontrado que plantea algún problema, se solucionó y la reliquia quedó atrás.