¿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 thePyTypeObjectforNewStyleClass. I will writetransmisión exteriorasNewStyleClass_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.

Respuestas a la pregunta(2)

Su respuesta a la pregunta