Como arrumar / corrigir a criação do PyCXX da nova classe de extensão Python?

Eu quase terminei de reescrever um wrapper C ++ Python (PyCXX).

O original permite classes de extensão de estilo antigo e novo, mas também permite derivar das classes de estilo novo:

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()

O código é complicado e parece conter erros (Por que o PyCXX lida com classes de novo estilo da maneira que faz?)

Minha pergunta é:Qual é a justificativa / justificativa para o mecanismo complicado do PyCXX? Existe uma alternativa mais limpa?

Tentarei detalhar abaixo onde estou com essa consulta. Primeiro, tentarei descrever o que o PyCXX está fazendo no momento, depois descreverei o que acho que poderia ser melhorado.

Quando o tempo de execução do Python encontrad = Derived(), fazPyObject_Call( ob ) where ob is thePyTypeObjectforNewStyleClass. I will writeobasNewStyleClass_PyTypeObject`.

Que PyTypeObject foi construído em C ++ e registrado usandoPyType_Ready

PyObject_Call invocarátype_call(PyTypeObject *type, PyObject *args, PyObject *kwds), retornando uma instância Derived inicializada, ou seja,

PyObject* derived_instance = type_call(NewStyleClass_PyTypeObject, NULL, NULL)

Algo assim.

(Tudo isso vindo de (http://eli.thegreenplace.net/2012/04/16/python-object-creation-sequence a propósito, obrigado Eli!)

type_call faz essencialmente:

type->tp_new(type, args, kwds);
type->tp_init(obj, args, kwds);

E nosso wrapper C ++ inseriu funções no diretóriotp_new etp_init slots deNewStyleClass_PyTypeObject algo assim:

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;
    }

Observe que precisamos vincular a instância do Python Derived e a instância da classe C ++ correspondente. (Por quê? Explicado abaixo, consulte 'X'). Para fazer isso, estamos usando:

struct Bridge
{
    PyObject_HEAD // <-- a PyObject
    ExtObjBase* m_pycxx_object;
}

Agora esta ponte levanta uma questão. Suspeito muito deste design.

Observe como a memória foi alocada para este novo PyObject:

        PyObject* pyob = subtype->tp_alloc(subtype,0);

E então digitamos esse ponteiro paraBridgee use os 4 ou 8 (sizeof(void*)) bytes imediatamente após oPyObject para apontar para a instância da classe C ++ correspondente (isso fica ligado emextension_object_init como pode ser visto acima).

Agora, para que isso funcione, é necessário:

a)subtype->tp_alloc(subtype,0) deve estar alocando um extrasizeof(void*) bytes b) OPyObject não requer nenhuma memória alémsizeof(PyObject_HEAD), porque se o fizesse, isso estaria em conflito com o ponteiro acima

Uma questão importante que tenho neste momento é: Podemos garantir que oPyObject que o tempo de execução do Python criou para o nossoderived_instance não se sobrepõe ao de BridgeExtObjBase* m_pycxx_object campo?

Vou tentar responder: é a US determinar a quantidade de memória alocada. Quando criamosNewStyleClass_PyTypeObject alimentamos quanta memória queremos issoPyTypeObject para alocar para uma nova instância deste 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           = ...

Você pode vê-lo entrando comotable->tp_basicsize

Mas agora parece claro para mim que PyObject-s gerado a partir deNewStyleClass_PyTypeObject nunca exigirá memória alocada adicional.

O que significa que esse todoBridge mecanismo é desnecessário.

E a técnica original do PyCXX para usar o PyObject como uma classe base deNewStyleClassCXXClasse inicializando essa base para que o PyObject do tempo de execução do Python parad = Derived() é de fato esta base, esta técnica está com boa aparência. Porque permite a conversão de tipos sem costura.

Sempre que o tempo de execução do Python chama um slot deNewStyleClass_PyTypeObject, ele passará um ponteiro para PyObject de d como o primeiro parâmetro, e podemos simplesmente voltar aNewStyleClassCXXClass. <- 'X' (mencionado acima)

Então, realmente, minha pergunta é: por que não fazemos isso?Existe algo de especial em derivar deNewStyleClass que força alocação extra para o PyObject?

Sei que não entendo a sequência de criação no caso de uma classe derivada. O post de Eli não abordou isso.

Eu suspeito que isso possa estar relacionado ao fato de que

    static PyObject* extension_object_new( PyTypeObject* subtype, ...

^ Este nome de variável é 'subtipo'. Eu não entendo isso, e me pergunto se isso pode conter a tecla.

EDIT: Pensei em uma explicação possível para o porquê PyCXX está usando sizeof (FinalClass) para inicialização. Pode ser uma relíquia de uma idéia que foi experimentada e descartada. ou seja, se a chamada tp_new do Python alocar espaço suficiente para o FinalClass (que tem o PyObject como base), talvez um novo FinalClass possa ser gerado nesse local exato usando 'placement new' ou algum negócio astuto de reinterpret_cast. Meu palpite é que isso pode ter sido tentado, encontrado para apresentar algum problema, contornado, e a relíquia foi deixada para trás.

questionAnswers(2)

yourAnswerToTheQuestion