Python绑定C++虚成员函数不能调用

Posted

技术标签:

【中文标题】Python绑定C++虚成员函数不能调用【英文标题】:Python binding C++ virtual member function cannot be called 【发布时间】:2018-11-07 01:19:11 【问题描述】:

最近用C++写了一个Python 3的扩展,但是在python中调用C++的时候遇到了一些麻烦,不打算用第三方库。

我用的是,但是去掉virtual关键字就可以了。

跑到return PyObject_CallObject(pFunction, args);时崩溃了,但是我没找到原因。

这是我的代码:

class A 

    PyObject_HEAD
public:
    A()
    
        std::cout << "A::A()" << std::endl;
    

    ~A()
    
        std::cout << "A::~A()" << std::endl;
    

    virtual void test()
    
        std::cout << "A::test()" << std::endl;
    
;

class B : public A

public:
    B()
    
        std::cout << "B::B()" << std::endl;
    

    ~B()
    
        std::cout << "B::~B()" << std::endl;
    

    static PyObject *py(B *self) 
        self->test();
        return PyLong_FromLong((long)123456);
    
;

static void B_dealloc(B *self) 

    self->~B();
    Py_TYPE(self)->tp_free((PyObject *)self);


static PyObject *B_new(PyTypeObject *type, PyObject *args, PyObject *kwds)

    B *self = (B*)type->tp_alloc(type, 0);
    new (self)B;
    return (PyObject*)self;


static PyMethodDef B_methods[] = 
    "test", (PyCFunction)(B::py), METH_NOARGS, nullptr,
    nullptr
;

static struct PyModuleDef example_definition = 
    PyModuleDef_HEAD_INIT,
    "example",
    "example",
    -1,
    B_methods
;

static PyTypeObject ClassyType = 
    PyVarObject_HEAD_INIT(NULL, 0) "example.B", /* tp_name */
    sizeof(B),                                  /* tp_basicsize */
    0,                                          /* tp_itemsize */
    (destructor)B_dealloc,                      /* tp_dealloc */
    0,                                          /* tp_print */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_reserved */
    0,                                          /* tp_repr */
    0,                                          /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    0,                                          /* tp_hash  */
    0,                                          /* tp_call */
    0,                                          /* tp_str */
    0,                                          /* tp_getattro */
    0,                                          /* tp_setattro */
    0,                                          /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,   /* tp_flags */
    "B objects",                                /* tp_doc */
    0,                                          /* tp_traverse */
    0,                                          /* tp_clear */
    0,                                          /* tp_richcompare */
    0,                                          /* tp_weaklistoffset */
    0,                                          /* tp_iter */
    0,                                          /* tp_iternext */
    B_methods,                                  /* tp_methods */
    nullptr,                                    /* tp_members */
    0,                                          /* tp_getset */
    0,                                          /* tp_base */
    0,                                          /* tp_dict */
    0,                                          /* tp_descr_get */
    0,                                          /* tp_descr_set */
    0,                                          /* tp_dictoffset */
    nullptr,                                    /* tp_init */
    0,                                          /* tp_alloc */
    B_new,                                      /* tp_new */
;

PyMODINIT_FUNC PyInit_example(void)


    PyObject *m = PyModule_Create(&example_definition);

    if (PyType_Ready(&ClassyType) < 0)
        return NULL;

    Py_INCREF(&ClassyType);
    PyModule_AddObject(m, "B", (PyObject*)&ClassyType);

    return m;


PyObject* importModule(std::string name)

    PyObject* pModule = PyImport_ImportModule(name.c_str());    // module name
    if (pModule == nullptr)
    
        std::cout << "load module error!" << std::endl;
        return nullptr;
    

    return pModule;


PyObject* callFunction(PyObject* pModule, std::string name, PyObject* args = nullptr)

    PyObject* pFunction = PyObject_GetAttrString(pModule, name.c_str());    // function name
    if (pFunction == nullptr)
    
        std::cout << "call function error!" << std::endl;
        return nullptr;
    

    return PyObject_CallObject(pFunction, args);


int main()

    // add module
    PyImport_AppendInittab("example", PyInit_example);

    // init python
    Py_Initialize();
    
        PyRun_SimpleString("import sys");
        PyRun_SimpleString("import os");
        PyRun_SimpleString("sys.path.append(os.getcwd() + '\\script')");    // add script path
    

    // import module
    PyImport_ImportModule("example");

    PyObject* pModule = importModule("Test");
    if (pModule != nullptr)
    
        PyObject* pReturn = callFunction(pModule, "main");
    

    PyErr_Print();

    Py_Finalize();

    system("pause");
    return 0;

【问题讨论】:

PyObject_CallObject 定义在哪里? @DiegoContreras PyObject_CallObject是python的API函数。 看起来你使用 CPython(我们也这样做)。 AFAIK,CPython 是用 C 编写的——至少它的 API 是 C API。所以,我不建议在 C++ 中进行扩展(又名 Wrapper “类”)。我最近遇到了一个烦人的错误,因为我忘记包含 PyObject_HEAD。 (这是继承的“C 风格”。Python 期望在每个PyObject 中都有PyObject_HEAD 的内容。)使用虚成员函数创建一个“继承”类,VMT(虚方法表)可能被放置在偏移处Python 期望 PyObject_HEAD 的地方 - 非常危险。 此外,Python 调用 C 函数。成员函数不适合 C 函数指针(静态成员函数除外)。 TLDR 可以在 Python 中绑定 C++ 对象,但不要使用 C++ 进行包装——这可能会崩溃。 另一个提示:可以将 C++ 对象作为 CPython 包装器 structs 的成员。这样做时,我意识到我必须在tp_alloc(type, 0); 之后使用放置new 构造这些成员(并注意通过显式析构函数调用来销毁它们)。我看到您在暴露的代码中使用整个包装器尝试了类似的东西。所以,我想你已经意识到了这一点。 【参考方案1】:

我假设 OP 正在使用 CPython API。 (我们使用 CPython,部分代码看起来非常相似/熟悉。)

顾名思义,它是用 C 编写的。

因此,当使用它为 C++ 类编写 Python 绑定时,开发人员必须意识到 CPython 及其 C API 并不“了解”任何关于 C++ 的信息。这必须仔细考虑(类似于为 C++ 类库编写 C 绑定)。

当我编写 Python Wrapper 类时,我总是使用structs (为了记住这一事实)。可以在 CPython 的包装器中使用 C++ 继承来类似于被包装的 C++ 类的继承(但这是我上述规则的唯一例外)。

structclass 在 C++ 中几乎是一样的,除了(唯一的)例外,默认情况下,struct 中的所有内容都是 public,而 class 中的 private。 SO: Class vs Struct for data only? 顺便说一句。 CPython将访问它的resp。 成员变量结构组件(例如ob_base)通过C指针强制转换(重新解释强制转换),甚至无法识别private-safety-attempts。

恕我直言,值得一提的是 POD(plain old data,也称为被动数据结构),因为这使得 C++ 包装类与C.SO: What are Aggregates and PODs and how/why are they special? 对此进行了全面的概述。

在 CPython 包装类中引入至少一个 virtual 成员函数会产生致命的后果。仔细阅读上面的链接可以清楚地说明这一点。但是,我决定通过一些示例代码来说明这一点:

#include <iomanip>
#include <iostream>

// a little experimentation framework:

struct _typeobject  ; // replacement (to keep it simple)
typedef size_t Py_ssize_t; // replacement (to keep it simple)

// copied from object.h of CPython:
/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA            \
    struct _object *_ob_next;           \
    struct _object *_ob_prev;

// copied from object.h of CPython:
/* Nothing is actually declared to be a PyObject, but every pointer to
 * a Python object can be cast to a PyObject*.  This is inheritance built
 * by hand.  Similarly every pointer to a variable-size Python object can,
 * in addition, be cast to PyVarObject*.
 */
typedef struct _object 
  _PyObject_HEAD_EXTRA
  Py_ssize_t ob_refcnt;
  struct _typeobject *ob_type;
 PyObject;

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD                   PyObject ob_base;

void dump(std::ostream &out, const char *p, size_t size)

  const size_t n = 16;
  for (size_t i = 0; i < size; ++p) 
    if (i % n == 0) 
      out << std::hex << std::setw(2 * sizeof p) << std::setfill('0')
        << (size_t)p << ": ";
    
    out << ' '
      << std::hex << std::setw(2) << std::setfill('0')
      << (unsigned)*(unsigned char*)p;
    if (++i % n == 0) out << '\n';
  
  if (size % n != 0) out << '\n';


// the experiment:

static PyObject pyObj;

// This is correct:
struct Wrapper1 
  PyObject_HEAD
  int myExt;
;
static Wrapper1 wrap1;

// This is possible:
struct Wrapper1Derived: Wrapper1 
  double myExtD;
;
static Wrapper1Derived wrap1D;

// This is effectively not different from struct Wrapper1
// but things are private in Wrapper2
// ...and Python will just ignore this (using C pointer casts).
class Wrapper2 
  PyObject_HEAD
  int myExt;
;
static Wrapper2 wrap2;

// This is FATAL - introduces a virtual method table.
class Wrapper3 
  private:
    PyObject_HEAD
    int myExt;
  public:
    Wrapper3(int value): myExt(value)  
    virtual ~Wrapper3()  myExt = 0; 
;
static Wrapper3 wrap3123;

int main()

  std::cout << "Dump of PyObject pyObj:\n";
  dump(std::cout, (const char*)&pyObj, sizeof pyObj);
  std::cout << "Dump of Wrapper1 wrap1:\n";
  dump(std::cout, (const char*)&wrap1, sizeof wrap1);
  std::cout << "Dump of Wrapper1Derived wrap1D:\n";
  dump(std::cout, (const char*)&wrap1D, sizeof wrap1D);
  std::cout << "Dump of Wrapper2 wrap2:\n";
  dump(std::cout, (const char*)&wrap2, sizeof wrap2);
  std::cout << "Dump of Wrapper3 wrap3:\n";
  dump(std::cout, (const char*)&wrap3, sizeof wrap3);
  return 0;

编译并运行:

Dump of PyObject pyObj:
0000000000601640:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000000000601650:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Dump of Wrapper1 wrap1:
0000000000601600:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000000000601610:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000000000601620:  00 00 00 00 00 00 00 00
Dump of Wrapper1Derived wrap1D:
00000000006015c0:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00000000006015d0:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00000000006015e0:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Dump of Wrapper2 wrap2:
0000000000601580:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000000000601590:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00000000006015a0:  00 00 00 00 00 00 00 00
Dump of Wrapper3 wrap3:
0000000000601540:  d8 0e 40 00 00 00 00 00 00 00 00 00 00 00 00 00
0000000000601550:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000000000601560:  00 00 00 00 00 00 00 00 7b 00 00 00 00 00 00 00

Live Demo on coliru

pyObjwrap1wrap1Dwrap2 的转储仅包含 00s - 难怪,我制作了它们 staticwrap3 看起来有点不同,部分原因是构造函数 (7b == 123),部分原因是 C++ 编译器将 VMT 指针放入了 d8 0e 40 很可能属于的类实例中。 (我假设 VMT 指针具有任何函数指针的大小,但我真的不知道编译器如何在内部组织事物。)

想象一下,当 CPython 获取 wrap3 的地址,将其转换为 PyObject*,并写入偏移量为 0 并用于将 Python 对象链接到双链表中的 _ob_next 指针时会发生什么。 (希望是崩溃或其他让事情变得更糟的事情。)

依次想象在 OP 的 create 函数中会发生什么

static PyObject *B_new(PyTypeObject *type, PyObject *args, PyObject *kwds)

    B *self = (B*)type->tp_alloc(type, 0);
    new (self)B;
    return (PyObject*)self;

B 的放置构造函数覆盖PyObject 内部的初始化,这可能发生在tp_alloc() 中。

【讨论】:

您的 coliru 链接只是链接回问题

以上是关于Python绑定C++虚成员函数不能调用的主要内容,如果未能解决你的问题,请参考以下文章

C++ 快速绑定成员函数的方法

c++八股之多态(持续更新)

C++ 中,类的继承:父类当使用虚函数时候,子类对该函数进行重写的话,属于子类成员函数对虚函数的覆盖!

C++ 纯虚函数

C++学习:3多态

虚函数