I've nearly finished rewriting a C++ Python wrapper (PyCXX).
The original allows old and new style extension classes, but also allows one to derive from the new-style classes:
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()
The code is convoluted, and appears to contain errors (Why does PyCXX handle new-style classes in the way it does?)
My question is: What is the rationale/justification for PyCXX's convoluted mechanism? Is there a cleaner alternative?
I will attempt to detail below where I am at with this enquiry. First I will try and describe what PyCXX is doing at the moment, then I will describe what I think could maybe be improved.
When the Python runtime encounters d = Derived()
, it does PyObject_Call( ob ) where ob is the
PyTypeObjectfor
NewStyleClass. I will write
obas
NewStyleClass_PyTypeObject`.
That PyTypeObject has been constructed in C++ and registered using PyType_Ready
PyObject_Call
will invoke type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
, returning an initialised Derived instance i.e.
PyObject* derived_instance = type_call(NewStyleClass_PyTypeObject, NULL, NULL)
Something like this.
(All of this coming from (http://eli.thegreenplace.net/2012/04/16/python-object-creation-sequence by the way, thanks Eli!)
type_call does essentially:
type->tp_new(type, args, kwds);
type->tp_init(obj, args, kwds);
And our C++ wrapper has inserted functions into the tp_new
and tp_init
slots of NewStyleClass_PyTypeObject
something like this:
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;
}
Note that we need to bind together the Python Derived instance, and it's corresponding C++ class instance. (Why? Explained below, see 'X'). To do that we are using:
struct Bridge
{
PyObject_HEAD // <-- a PyObject
ExtObjBase* m_pycxx_object;
}
Now this bridge raises a question. I'm very suspicious of this design.
Note how memory was allocated for this new PyObject:
PyObject* pyob = subtype->tp_alloc(subtype,0);
And then we typecast this pointer to Bridge
, and use the 4 or 8 (sizeof(void*)
) bytes immediately following the PyObject
to point to the corresponding C++ class instance (this gets hooked up in extension_object_init
as can be seen above).
Now for this to work we require:
a) subtype->tp_alloc(subtype,0)
must be allocating an extra sizeof(void*)
bytes
b) The PyObject
doesn't require any memory beyond sizeof(PyObject_HEAD)
, because if it did then this would be conflicting with the above pointer
One major question I have at this point is:
Can we guarantee that the PyObject
that the Python runtime has created for our derived_instance
does not overlap into Bridge's ExtObjBase* m_pycxx_object
field?
I will attempt to answer it: it is US determining how much memory gets allocated. When we create NewStyleClass_PyTypeObject
we feed in how much memory we want this PyTypeObject
to allocate for a new instance of this type:
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 = ...
You can see it going in as table->tp_basicsize
But now it seems clear to me that PyObject-s generated from NewStyleClass_PyTypeObject
will never require additional allocated memory.
Which means that this whole Bridge
mechanism is unnecessary.
And PyCXX's original technique for using PyObject as a base class of NewStyleClassCXXClass
, and initialising this base so that the Python runtime's PyObject for d = Derived()
is in fact this base, this technique is looking good. Because it allows seamless typecasting.
Whenever Python runtime calls a slot from NewStyleClass_PyTypeObject
, it will be passing a pointer to d's PyObject as the first parameter, and we can just typecast back to NewStyleClassCXXClass
. <-- 'X' (referenced above)
So really my question is: why don't we just do this? Is there something special about deriving from NewStyleClass
that forces extra allocation for the PyObject?
I realise I don't understand the creation sequence in the case of a derived class. Eli's post didn't cover that.
I suspect this may be connected with the fact that
static PyObject* extension_object_new( PyTypeObject* subtype, ...
^ this variable name is 'subtype' I don't understand this, and I wonder if this may hold the key.
EDIT: I thought of one possible explanation for why PyCXX is using sizeof(FinalClass) for initialisation. It might be a relic from an idea that got tried and discarded. i.e. If Python's tp_new call allocates enough space for the FinalClass (which has the PyObject as base), maybe a new FinalClass can be generated on that exact location using 'placement new', or some cunning reinterpret_cast business. My guess is this might have been tried, found to pose some problem, worked around, and the relic got left behind.
PyCXX is not convoluted. It does have two bugs, but they can be easily fixed without requiring significant changes to the code.
When creating a C++ wrapper for the Python API, one encounters a problem. The C++ object model and the Python new-style object model are very different. One fundamental difference is that C++ has a single constructor that both creates and initializes the object. While Python has two stages; tp_new
creates the object and performs minimal intialization (or just returns an existing object) and tp_init
performs the rest of the initialization.
PEP 253, which you should probably read in its entirety, says:
The difference in responsibilities between the tp_new() slot and the tp_init() slot lies in the invariants they ensure. The tp_new() slot should ensure only the most essential invariants, without which the C code that implements the objects would break. The tp_init() slot should be used for overridable user-specific initializations. Take for example the dictionary type. The implementation has an internal pointer to a hash table which should never be NULL. This invariant is taken care of by the tp_new() slot for dictionaries. The dictionary tp_init() slot, on the other hand, could be used to give the dictionary an initial set of keys and values based on the arguments passed in.
...
You may wonder why the tp_new() slot shouldn't call the tp_init() slot itself. The reason is that in certain circumstances (like support for persistent objects), it is important to be able to create an object of a particular type without initializing it any further than necessary. This may conveniently be done by calling the tp_new() slot without calling tp_init(). It is also possible hat tp_init() is not called, or called more than once -- its operation should be robust even in these anomalous cases.
The entire point of a C++ wrapper is to enable you to write nice C++ code. Say for example that you want your object to have a data member that can only be initialized during its construction. If you create the object during tp_new
, then you cannot reinitialize that data member during tp_init
. This will probably force you to hold that data member via some kind of a smart pointer and create it during tp_new
. This makes the code ugly.
The approach PyCXX takes is to separate object construction into two:
tp_new
creates a dummy object with just a pointer to the C++ object which is createdtp_init
. This pointer is initially null.tp_init
allocates and constructs the actual C++ object, then updates the pointer in the dummy object created intp_new
to point to it. Iftp_init
is called more than once it raises a Python exception.
I personally think that the overhead of this approach for my own applications is too high, but it's a legitimate approach. I have my own C++ wrapper around the Python C/API that does all the initialization in tp_new
, which is also flawed. There doesn't appear to be a good solution for that.
Here is a small C example that shows how Python allocates memory for object of classes derived from C types:
typedef struct
{
PyObject_HEAD
int dummy[100];
} xxx_obj;
It also needs a type object:
static PyTypeObject xxx_type =
{
PyObject_HEAD_INIT(NULL)
};
And a module initialization function that initializes this type:
extern "C"
void init_xxx(void)
{
PyObject* m;
xxx_type.tp_name = "_xxx.xxx";
xxx_type.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE;
xxx_type.tp_new = tp_new; // IMPORTANT
xxx_type.tp_basicsize = sizeof(xxx_obj); // IMPORTANT
if (PyType_Ready(&xxx_type) < 0)
return;
m = Py_InitModule3("_xxx", NULL, "");
Py_INCREF(&xxx_type);
PyModule_AddObject(m, "xxx", (PyObject *)&xxx_type);
}
What is missing is the implementation of tp_new
: The Python docs require that:
The
tp_new
function should callsubtype->tp_alloc(subtype, nitems)
to allocate space for the object
So lets do that and add a few printouts.
static
PyObject *tp_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds)
{
printf("xxx.tp_new():\n\n");
printf("\t subtype=%s\n", subtype->tp_name);
printf("\t subtype->tp_base=%s\n", subtype->tp_base->tp_name);
printf("\t subtype->tp_base->tp_base=%s\n", subtype->tp_base->tp_base->tp_name);
printf("\n");
printf("\t subtype->tp_basicsize=%ld\n", subtype->tp_basicsize);
printf("\t subtype->tp_base->tp_basicsize=%ld\n", subtype->tp_base->tp_basicsize);
printf("\t subtype->tp_base->tp_base->tp_basicsize=%ld\n", subtype->tp_base->tp_base->tp_basicsize);
return subtype->tp_alloc(subtype, 0); // IMPORTANT: memory allocation is done here!
}
Now run a very simple Python program to test it. This program creates a new class derived from xxx
, and then creates an object of type derived
.
import _xxx
class derived(_xxx.xxx):
def __init__(self):
super(derived, self).__init__()
d = derived()
To create an object of type derived, Python will call its tp_new
, which in turn will call its base class' (xxx
) tp_new
. This call generates the following output (exact numbers depends on the machine architecture):
xxx.tp_new():
subtype=derived
subtype->tp_base=_xxx.xxx
subtype->tp_base->tp_base=object
subtype->tp_basicsize=432
subtype->tp_base->tp_basicsize=416
subtype->tp_base->tp_base->tp_basicsize=16
The subtype
argument to tp_new
is the type of the object being created (derived
), it derives from our C type (_xxx.xxx
), which in turns derives from object
. The base object
is of size 16, which is just PyObject_HEAD
, the xxx
type has an additional 400 bytes for its dummy
member for a total of 416 bytes and the derived
Python class adds additional 16 bytes.
Because subtype->tp_basicsize
accounts for the sizes of all three levels of the hierarchy (object
, xxx
and derived
) for a total of 432 bytes, the right amount of memory is being allocated.
来源:https://stackoverflow.com/questions/27662074/how-to-tidy-fix-pycxxs-creation-of-new-style-python-extension-class