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()
コードが複雑で、エラーが含まれているようです (なぜ PyCXX は新しいスタイルのクラスをそのように処理するのですか? )
私の質問は: PyCXX の複雑なメカニズムの論理的根拠/正当化は何ですか? よりクリーンな代替手段はありますか?
このお問い合わせに関して、私がどこにいるのかを以下に詳しく説明しようと思います。最初に、現時点で PyCXX が何を行っているかを説明してから、改善できる可能性があると思われることを説明します。
Python ランタイムが に遭遇するとd = Derived()
、PyObject_Call( ob ) where ob is the
PyTypeObject for
NewStyleClass . I will write
ob as
NewStyleClass_PyTypeObject` を実行します。
その PyTypeObject は C++ で構築され、次を使用して登録されています。PyType_Ready
PyObject_Call
を呼び出しtype_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
、初期化された派生インスタンスを返します。
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++ ラッパーは、次のような関数をtp_new
およびtp_init
スロットに挿入しました。NewStyleClass_PyTypeObject
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
4 または 8 ( ) バイトを使用して、対応する C++ クラス インスタンスを指します (これは、上記のように接続されます)。sizeof(void*)
PyObject
extension_object_init
これが機能するためには、次のものが必要です。
a)subtype->tp_alloc(subtype,0)
余分なsizeof(void*)
バイトを割り当てる必要があります b)PyObject
は を超えるメモリを必要としませんsizeof(PyObject_HEAD)
。
この時点での主な質問の 1 つはPyObject
、Python ランタイムが作成したが Bridge のフィールドとderived_instance
重複しないことを保証できるかということです。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
しかし今では、生成された PyObject-sNewStyleClass_PyTypeObject
が追加の割り当てられたメモリを必要としないことは明らかです。
これは、このBridge
メカニズム全体が不要であることを意味します。
そして、PyObject を の基底クラスとして使用し、NewStyleClassCXXClass
この基底を初期化して、Python ランタイムの PyObjectd = Derived()
が実際にこの基底になるようにする PyCXX の独自の手法は、見栄えがします。シームレスな型キャストが可能になるためです。
Python ランタイムが からスロットを呼び出すときは常にNewStyleClass_PyTypeObject
、最初のパラメーターとして d の PyObject へのポインターが渡され、型キャストして に戻すことができますNewStyleClassCXXClass
。<-- 'X' (上記参照)
だから本当に私の質問は、なぜ私たちはこれをやらないのですか? PyObject の余分な割り当てを強制することから派生することについて何か特別なことはありNewStyleClass
ますか?
派生クラスの場合の作成手順がわからないことに気づきました。エリの投稿はそれをカバーしていませんでした。
という事実が関係していると思われます
static PyObject* extension_object_new( PyTypeObject* subtype, ...
^ この変数名は 'subtype' です。これがわかりません。これが鍵を握っているのではないでしょうか。
編集: PyCXX が初期化に sizeof(FinalClass) を使用している理由について、1 つの考えられる説明を考えました。それは試行錯誤したアイデアの遺物かもしれません。つまり、Python の tp_new 呼び出しが FinalClass (ベースとして PyObject を持つ) に十分なスペースを割り当てる場合、'placement new' を使用するか、狡猾な reinterpret_cast ビジネスを使用して、その正確な場所に新しい FinalClass を生成できる可能性があります。私の推測では、これが試行され、何らかの問題を引き起こすことが判明し、回避され、遺物が置き去りにされた可能性があります。