尝试读取在 Python 中创建的对象时访问冲突传递给 C++ 端的 std::vector 然后返回给 Python

Posted

技术标签:

【中文标题】尝试读取在 Python 中创建的对象时访问冲突传递给 C++ 端的 std::vector 然后返回给 Python【英文标题】:Access violation when trying to read out object created in Python passed to std::vector on C++ side and then returned to Python 【发布时间】:2020-01-07 17:33:49 【问题描述】:

使用 VS 2019、Windows 10 上的 Python 3.7 64bit 和 pybind11 2.4.3 我遇到了以下问题:

当我在 Python 端创建一个带有 pybind11 py::class_ 的对象并将其直接传递给 C++ 端的一个方法时,将其存储在 std::vector 中,稍后尝试从 Python 中读取该对象会导致一个Access violation - no RTTI data。如果 Python 代码首先将创建的对象存储在 Python 变量中,然后将其传递给 C++,那么来自 Python 的后续访问将按预期工作。

我在使用 pybind11 和 C++ 方面没有太多经验,所以我可能犯了一个简单的错误,希望能提供有关如何设置 C++ 和 pybind11 用法的任何帮助,以便不需要 Python 变通方法来使用变量而且我没有遇到任何访问冲突。

这里是一些代码细节,C++代码是

#include <iostream>

#include <vector>

#include <pybind11/pybind11.h>

using namespace std;


class XdmItem;

class XdmValue 

public:

    virtual XdmItem* itemAt(int n);

    virtual int size();

    void addXdmItem(XdmItem* val);


protected:
    std::vector<XdmItem*> values;
;

void XdmValue::addXdmItem(XdmItem* val) 
    values.push_back(val);


XdmItem* XdmValue::itemAt(int n) 
    if (n >= 0 && (unsigned int)n < values.size()) 
        return values[n];
    
    return NULL;


int XdmValue::size() 
    return values.size();


class XdmItem : public XdmValue 

public:

    int size();

;

int XdmItem::size() 
    return 1;



namespace py = pybind11;

PYBIND11_MODULE(UseClassHierarchyAsPythonModule, m) 


    py::class_<XdmValue>(m, "PyXdmValue")
        .def(py::init<>())
        .def("size", &XdmValue::size)
        .def("item_at", &XdmValue::itemAt)
        .def("add_item", &XdmValue::addXdmItem);

    py::class_<XdmItem, XdmValue>(m, "PyXdmItem")
        .def(py::init<>())
        .def("size", &XdmItem::size);



#ifdef VERSION_INFO
    m.attr("__version__") = VERSION_INFO;
#else
    m.attr("__version__") = "dev";
#endif

完美运行的 Python 代码是

import UseClassHierarchyAsPythonModule

value = UseClassHierarchyAsPythonModule.PyXdmValue()

print(value, type(value))

print(value.size())

item = UseClassHierarchyAsPythonModule.PyXdmItem()

value.add_item(item)

print(value.size())

item0 = value.item_at(0)

print(item, type(item))

而以下代码导致Access violation - no RTTI data!

import UseClassHierarchyAsPythonModule

value = UseClassHierarchyAsPythonModule.PyXdmValue()

print(value, type(value))

print(value.size())

value.add_item(UseClassHierarchyAsPythonModule.PyXdmItem())

print(value.size())

item0 = value.item_at(0)

print(item, type(item))

它给了

  Message=Access violation - no RTTI data!
  Source=C:\SomePath\AccessViolation.py
  StackTrace:
  File "C:\SomePath\AccessViolation.py", line 13, in <module>
    item0 = value.item_at(0)

如果我启用本机代码调试,堆栈跟踪包括 pybind C++ 代码并且是

>   UseClassHierarchyAsPythonModule.pyd!pybind11::polymorphic_type_hook<XdmItem,void>::get(const XdmItem * src, const type_info * & type) Line 818  C++
    UseClassHierarchyAsPythonModule.pyd!pybind11::detail::type_caster_base<XdmItem>::src_and_type(const XdmItem * src) Line 851 C++
    UseClassHierarchyAsPythonModule.pyd!pybind11::detail::type_caster_base<XdmItem>::cast(const XdmItem * src, pybind11::return_value_policy policy, pybind11::handle parent) Line 871  C++
    UseClassHierarchyAsPythonModule.pyd!pybind11::cpp_function::initialize::__l2::<lambda>(pybind11::detail::function_call & call) Line 163 C++
    UseClassHierarchyAsPythonModule.pyd!pybind11::handle <lambda>(pybind11::detail::function_call &)::<lambda_invoker_cdecl>(pybind11::detail::function_call & call) Line 100   C++
    UseClassHierarchyAsPythonModule.pyd!pybind11::cpp_function::dispatcher(_object * self, _object * args_in, _object * kwargs_in) Line 624 C++
    [External Code] 
    AccessViolation.py!<module> Line 13 Python

知道我的 C++/pybind11 使用有什么问题吗?

【问题讨论】:

如果.def("add_item", &amp;XdmValue::addXdmItem, py::keep_alive&lt;1, 2&gt;());怎么办? @CristiFati,谢谢,这解决了问题,我需要阅读它的作用才能理解它。 为了您自己,控制您的 C++ 类型的复制和分配。在XdmValue 的情况下,使复制ctor 受保护并删除赋值运算符。这可能无法解决您的问题,但可以防止以后出现一些头痛。 @UlrichEckhardt,感谢您的建议,我模拟了一个最小示例来检查 PyBind11 是否可以帮助处理现有的较大代码并遇到访问冲突问题,然后进一步减少该示例以在此处发布。但诚然,我不具备完全靠自己设计和实现好的 C++ 代码的 C++ 技能。 如果您是 C++ 新手并且可以完全控制设计,请查看 std::shared_ptr 的所有权管理(基本上是 C++ 端引用计数),它集成良好且易于使用pybind11. 【参考方案1】:

注意:到目前为止,我还不是 PyBind11 专家,我只是阅读了这个问题并试图找出可能的原因。 我的猜测是,不同之处在于,在它不起作用的情况下,Python 对象是在 add_item 调用之前创建的( >C++ 包装了一个)然后在调用之后它被垃圾收集(以及 C++ 包装的一个),产生 Undefined Behavior (一个无效的指针)。 相反,在它工作的情况下,对象不会被垃圾收集,因为它“保存”在 item 中(它的 refcount 大于 0),因此C++ 包装的对象也存在。紧跟在value.add_item(item) 之后的delete item 应该会重现错误行为。

根据[ReadTheDocs.PyBind11]: Functions - Keep alive:

通常,当 C++ 对象是任何类型的容器并且正在将另一个对象添加到容器中时,需要此策略。 keep_alive&lt;Nurse, Patient&gt; 表示索引为Patient 的参数至少应该保持活动状态,直到索引为Nurse 的参数被垃圾收集器释放。

因此,解决方案是使 UseClassHierarchyAsPythonModule.PyXdmItem 对象持续存在,直到容器被销毁(请注意,这可能会使对象在内存中的时间比预期的要长,可能有一种更简洁的方法来实现这一点),即在 add_item 中指定:

...

.def("add_item", &XdmValue::addXdmItem, py::keep_alive<1, 2>());

【讨论】:

【参考方案2】:

PyBind11 可以使用 RTTI 技巧自动转换(即polymorphic_type_hook;这与我在 cppyy 中所做的相同:您创建一个假基类,将给定地址转换为假基类,然后读取 RTTI 以获取实际名称,然后查找 Python 代理并根据需要将基数应用于派生偏移量)。如果 Python 代码首先创建对象,则稍后会通过地址找到它(这是为了保证对象身份),因此不会发生强制转换。

为了使自动转换正常工作,您确实需要一个虚拟析构函数来保证(根据 C++ 标准)在 dll 中正确放置 RTTI。我在您的基类 (XdmValue) 中没有看到。

(除此之外,特定于 Windows,我也总是从主应用程序中导出 RTTI 根节点以保证只有一个。但如果是这样,那应该是 Python 解释器在做,或者是第一个模块,所以我不做'不认为这适用于此。另外,我当然假设您在构建时启用了 RTTI。)

【讨论】:

感谢您的回答。 virtual ~XdmValue() 是否足以保证 RTTI 在 dll 中的正确放置?但似乎另一个答案确定了 PyBind11 将py::keep_alive 与您添加对象的任何类型的容器一起使用的要求。无论我是否添加虚拟析构函数,这似乎都可以修复示例。 是的,它会(通常你希望 C++ 通过基指针删除派生类)。是的,克里斯蒂法蒂的诊断很准确,我想得太复杂了。虽然我不同意这个答案,但还没有更好的答案:keep_alive 只是将一条生命线从 python XdmValue 对象放到 python XdmItem 对象。但是如果 C++ XdmValue 现在丢弃 C++ XdmItem,那么 python XdmItem 有一个悬空指针。相反,python XdmItem 至少应该放弃所有权,或者 addXdmItem 应该清除它。应该有pybind11调用策略,但是atm找不到。 好吧,在我的一生中,我不明白如何从 Python 中设置代理的 owned 变量。我确实发现了这个非常相似的案例:github.com/pybind/pybind11/issues/609,它基本上建议使用辅助函数(在这种情况下为 make_bell)并使用返回值策略。很不满意。 :(

以上是关于尝试读取在 Python 中创建的对象时访问冲突传递给 C++ 端的 std::vector 然后返回给 Python的主要内容,如果未能解决你的问题,请参考以下文章

从 C++ 访问在 python 中创建的 C++ 类

异常错误:访问冲突读取位置 0xDDDDDDDD

如何在颤动的另一个有状态小部件中访问在一个有状态小部件中创建的对象

Vector::push_back() 给出读取访问冲突

为啥你不能命名一个在函数中创建的对象,与它在 Python 中的类名完全相同?

ProgrammingError: 在一个线程中创建的 SQLite 对象只能在同一个线程中使用