【发布时间】:2015-02-24 01:23:05
【问题描述】:
我几乎完成了对 C++ Python 包装器 (PyCXX) 的重写。
原来允许新旧样式扩展类,但也允许从新样式类派生:
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()
代码很复杂,似乎包含错误 (Why does PyCXX handle new-style classes in the way it does?)
我的问题是:PyCXX 复杂机制的基本原理/理由是什么?有没有更清洁的替代品?
我将尝试在下面详细说明我在此查询中所处的位置。首先,我将尝试描述 PyCXX 目前正在做什么,然后我将描述我认为可以改进的地方。
当 Python 运行时遇到 d = Derived() 时,它会遇到 PyObject_Call( ob ) where ob is thePyTypeObjectforNewStyleClass. I will writeobasNewStyleClass_PyTypeObject`。
PyTypeObject 已经用 C++ 构建并使用 PyType_Ready 注册
PyObject_Call 将调用 type_call(PyTypeObject *type, PyObject *args, PyObject *kwds),返回一个初始化的 Derived 实例,即
PyObject* derived_instance = type_call(NewStyleClass_PyTypeObject, NULL, NULL)
类似的东西。
(所有这些都来自(http://eli.thegreenplace.net/2012/04/16/python-object-creation-sequence 顺便说一句,谢谢 Eli!)
type_call 本质上是:
type->tp_new(type, args, kwds);
type->tp_init(obj, args, kwds);
我们的 C++ 包装器已将函数插入到 NewStyleClass_PyTypeObject 的 tp_new 和 tp_init 插槽中,如下所示:
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;
}
请注意,我们需要将 Python Derived 实例和它对应的 C++ 类实例绑定在一起。 (为什么?解释如下,见“X”)。为此,我们正在使用:
struct Bridge
{
PyObject_HEAD // <-- a PyObject
ExtObjBase* m_pycxx_object;
}
现在这座桥提出了一个问题。我很怀疑这种设计。
注意内存是如何为这个新的 PyObject 分配的:
PyObject* pyob = subtype->tp_alloc(subtype,0);
然后我们将该指针类型转换为Bridge,并使用紧跟在PyObject 之后的4 或8 个(sizeof(void*)) 字节指向相应的C++ 类实例(这在extension_object_init 中连接为可以在上面看到)。
现在我们需要:
a) subtype->tp_alloc(subtype,0) 必须分配额外的 sizeof(void*) 字节
b) PyObject 不需要超出sizeof(PyObject_HEAD) 的任何内存,因为如果需要,那么这将与上述指针冲突
此时我的一个主要问题是:
我们能否保证 Python 运行时为我们的 derived_instance 创建的 PyObject 不会与 Bridge 的 ExtObjBase* m_pycxx_object 字段重叠?
我将尝试回答:由美国决定分配多少内存。当我们创建NewStyleClass_PyTypeObject 时,我们输入我们希望这个PyTypeObject 为这种类型的新实例分配多少内存:
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 = ...
您可以看到它以table->tp_basicsize 的形式出现
但现在我似乎很清楚,从 NewStyleClass_PyTypeObject 生成的 PyObject-s 永远不需要额外分配的内存。
这意味着整个Bridge机制是不必要的。
而 PyCXX 的原始技术使用 PyObject 作为NewStyleClassCXXClass 的基类,并初始化这个基类,以便d = Derived() 的 Python 运行时的 PyObject 实际上是这个基类,这种技术看起来不错。因为它允许无缝类型转换。
每当 Python 运行时从 NewStyleClass_PyTypeObject 调用一个槽时,它都会将一个指向 d 的 PyObject 的指针作为第一个参数传递,我们可以直接类型转换回 NewStyleClassCXXClass。
所以我的问题是:为什么我们不这样做呢? 从NewStyleClass 派生有什么特别之处会强制为 PyObject 分配额外的空间?
我意识到我不理解派生类的创建顺序。 Eli 的帖子没有涉及到这一点。
我怀疑这可能与以下事实有关
static PyObject* extension_object_new( PyTypeObject* subtype, ...
^ 这个变量名是'subtype' 我不明白这一点,我想知道这是否是关键。
编辑:我想到了为什么 PyCXX 使用 sizeof(FinalClass) 进行初始化的一种可能解释。它可能是一个经过尝试和放弃的想法的遗物。即,如果 Python 的 tp_new 调用为 FinalClass(它以 PyObject 作为基础)分配了足够的空间,也许可以使用“placement new”或一些巧妙的 reinterpret_cast 业务在该确切位置生成一个新的 FinalClass。我的猜测是这可能已经尝试过了,发现会造成一些问题,解决了问题,然后遗物就被遗忘了。
【问题讨论】:
标签: c++ initialization python-c-api new-style-class pycxx