Wie kann man PyCXXs Erstellung einer neuen Python-Erweiterungsklasse aufräumen / reparieren?

Ich bin fast fertig damit, einen C ++ - Python-Wrapper (PyCXX) umzuschreiben.

Das Original erlaubt Erweiterungsklassen für alte und neue Stile, erlaubt aber auch das Ableiten von Klassen für neue Stile:

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

Der Code ist verschachtelt und scheint Fehler zu enthalten Warum behandelt PyCXX Klassen neuen Stils auf die gleiche Weise?)

Meine Frage ist:Was ist die Begründung für den verschachtelten Mechanismus von PyCXX? Gibt es eine sauberere Alternative?

Ich werde versuchen, unten zu beschreiben, wo ich mich mit dieser Anfrage befinde. Zuerst werde ich versuchen zu beschreiben, was PyCXX gerade tut, dann werde ich beschreiben, was meiner Meinung nach vielleicht verbessert werden könnte.

Wenn die Python-Laufzeit auf @ trifd = Derived(), es tutPyObject_Call( ob ) where ob is the PyTypeObjectfor NewStyleClass. I will write obas NewStyleClass_PyTypeObject`.

Das PyTypeObject wurde in C ++ erstellt und mit @ registrierPyType_Ready

PyObject_Call ruft @ atype_call(PyTypeObject *type, PyObject *args, PyObject *kwds), Rückgabe einer initialisierten abgeleiteten Instanz, d. h.

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

Etwas wie das

(All dies kommt von http: //eli.thegreenplace.net/2012/04/16/python-object-creation-sequenc übrigens, danke Eli!)

type_call macht im Wesentlichen:

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

Und unser C ++ - Wrapper hat Funktionen in das @ eingefütp_new undtp_init Slots vonNewStyleClass_PyTypeObject etwas wie das

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

Hinweis, dass wir die Python Derived-Instanz und die entsprechende C ++ - Klasseninstanz zusammenbinden müssen. (Warum? Unten erklärt, siehe 'X'). Dazu verwenden wir:

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

Jetzt wirft diese Brücke eine Frage auf. Ich bin diesem Entwurf gegenüber sehr misstrauisch.

Notieren Sie, wie Speicher für dieses neue PyObject zugewiesen wurde:

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

Und dann tippen wir diesen Zeiger aufBridge, und verwenden Sie die 4 oder 8 sizeof(void*)) Bytes unmittelbar nach demPyObject, um auf die entsprechende Instanz der C ++ - Klasse zu verweisen (dies wird mit @ verknüpfextension_object_init wie oben zu sehen).

amit dies funktioniert, benötigen wi

ein)subtype->tp_alloc(subtype,0) muss ein zusätzliches @ zuweissizeof(void*) bytes b) DasPyObject benötigt keinen Speicher über @ hinasizeof(PyObject_HEAD), denn wenn dies der Fall wäre, würde dies in Konflikt mit dem obigen Zeiger @ stehe

Eine wichtige Frage, die ich an dieser Stelle habe, ist: Können wir garantieren, dass diePyObject dass die Python-Laufzeit für unsere @ erstellt hderived_instance überschneidet sich nicht mit Bridge'sExtObjBase* m_pycxx_object Feld

Ich werde versuchen, darauf zu antworten: Es liegt in den USA, zu bestimmen, wie viel Speicher zugewiesen wird. Wenn wir @ erstellNewStyleClass_PyTypeObject wir geben ein, wie viel Speicher wir wollen, dass diesePyTypeObject, um eine neue Instanz dieses Typs zuzuweisen:

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           = ...

Sie können sehen, wie es in @ getable->tp_basicsize

Aber jetzt scheint mir klar, dass PyObject-s von @ generieNewStyleClass_PyTypeObject erfordert niemals zusätzlichen zugewiesenen Speicher.

Was bedeutet, dass diese ganzeBridge Mechanismus ist nicht erforderlich.

Und PyCXXs ursprüngliche Technik zur Verwendung von PyObject als Basisklasse vonNewStyleClassCXXClass und Initialisieren dieser Basis, sodass das PyObject for @ der Python-Laufzeitumgebud = Derived() ist in der Tat diese Basis, diese Technik sieht gut aus. Weil es nahtloses Typecasting ermöglicht.

Wenn die Python-Laufzeit einen Slot von @ aufruNewStyleClass_PyTypeObject, es wird ein Zeiger auf ds PyObject als erster Parameter übergeben, und wir können einfach zurück zu @ tippNewStyleClassCXXClass. <- 'X' (oben angegeben)

So ist wirklich meine Frage: Warum machen wir das nicht einfach so?Ist es etwas Besonderes, wenn man von @ abstammNewStyleClass das erzwingt zusätzliche Zuweisung für das PyObject?

Mir ist klar, dass ich die Erstellungsreihenfolge bei einer abgeleiteten Klasse nicht verstehe. Elis Beitrag deckte das nicht ab.

ch vermute, dies kann mit der Tatsache zusammenhängen, dass

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

^ Dieser Variablenname ist "Untertyp". Ich verstehe das nicht und frage mich, ob dies den Schlüssel enthalten kann.

EDIT: Ich habe mir eine mögliche Erklärung überlegt, warum PyCXX sizeof (FinalClass) zur Initialisierung verwendet. Es könnte ein Relikt aus einer Idee sein, die versucht und verworfen wurde. d. h., wenn Pythons Aufruf "tp_new" genügend Speicherplatz für die FinalClass (die das PyObject als Basis hat) zuweist, kann möglicherweise eine neue FinalClass an dieser exakten Position mithilfe von "Placement New" oder einer gerissenen Neuinterpretation des Cast-Geschäfts generiert werden. Ich nehme an, dass dies versucht wurde, ein Problem darstellte, umgearbeitet wurde und das Relikt zurückblieb.

Antworten auf die Frage(4)

Ihre Antwort auf die Frage