【问题标题】:How can you bind exceptions with custom fields and constructors in pybind11 and still have them function as python exception?如何在 pybind11 中将异常与自定义字段和构造函数绑定,并且仍然让它们作为 python 异常起作用?
【发布时间】:2020-09-17 02:34:21
【问题描述】:

这似乎是 pybind11 中的一个已知限制。我通读了所有文档,无论错误报告似乎适用,以及我可以在 pybind11 gitter 中找到的所有内容。我在 C++ 中有一个自定义异常类,其中包含自定义构造函数和字段。这种类的一个非常基本的例子,修剪空间是在这里:

class BadData : public std::exception
{
  public:
    // Constructors
    BadData()
      : msg(),
        stack(),
        _name("BadData")
    {}

    BadData(std::string _msg, std::string _stack)
      : msg(_msg),
        stack(_stack),
        _name("BadData")
    {}

    const std::string&
    getMsg() const
    {
      return msg;
    }

    void
    setMsg(const std::string& arg)
    {
      msg = arg;
    }

    // Member stack
    const std::string&
    getStack() const
    {
      return stack;
    }

    void
    setStack(const std::string& arg)
    {
      stack = arg;
    }
  private:
    std::string msg;
    std::string stack;
    std::string _name;

我目前有将它绑定到 python 中的 python 绑定代码,但它是自定义生成的,由于它的简单性和编译速度,我们更愿意使用 pybind11。

将异常绑定到 pybind11 的默认机制如下所示

py::register_exception<BadData>(module, "BadData");

这将创建 C++ 异常和 python 异常之间的自动转换,将 c++ 异常的 what() 值转换为 python 异常的 message。但是,来自 c++ 异常的所有额外数据都将丢失,如果您尝试在 python 中抛出异常并在 c++ 中捕获它,则无法将其与任何额外数据一起抛出。

您可以使用 attr 将额外数据绑定到 python 对象,我什至尝试扩展 pybind11:exception 类,以便更轻松地向异常添加自定义字段。

  template <typename type>
  class exception11 : public ::py::exception<type>
  {
   public:
    exception11(::py::handle scope, const char *name, PyObject *base = PyExc_Exception)
      : ::py::exception<type>(scope, name, base)
    {}

    template <typename Func, typename... Extra>
    exception11 &def(const char *name_, Func&& f, const Extra&... extra) {
      ::py::cpp_function cf(::py::method_adaptor<type>(std::forward<Func>(f)),
                            ::py::name(name_),
                            ::py::is_method(*this),
                            ::py::sibling(getattr(*this, name_, ::py::none())),
                            extra...);
      this->attr(cf.name()) = cf;
      return *this;
    }
  };

这为异常添加了一个def 函数,类似于class_ 所做的事情。使用它的幼稚方法不起作用

    exception11< ::example::data::BadData>(module, "BadData")
      .def("getStack", &::example::data::BadData::getStack);

因为在 c++ 和 python 中BadData 之间没有自动转换。您可以尝试通过绑定 lambda 来解决此问题:

    .def("getStack", [](py::object& obj) {
      ::example::data::BadData *cls = obj.cast< ::example::data::BadData* >();
      return cls->getStack();
    });

那里的obj.cast 也失败了,因为没有自动转换。基本上,由于没有地方存储 c++ 实例,因此我找不到这种方法的真正可行的解决方案。另外我根本找不到在自定义构造函数中绑定的方法,这使得在 python 上的可用性非常弱。

下一次尝试是基于 pybind11 中的建议,您可以将 python 异常类型用作元类作为普通的 class_ 并让 python 将其识别为有效异常。我尝试了这种方法的多种变体。

py::class_< ::example::data::BadData >(module, "BadData", py::dynamic_attr(), py::reinterpret_borrow<py::object>(PyExc_Exception))
py::class_< ::example::data::BadData >(module, "BadData", py::dynamic_attr(), py::cast(PyExc_Exception))
py::class_< ::example::data::BadData >(module, "BadData", py::dynamic_attr(), py::cast(PyExc_Exception->ob_type))
py::class_< ::example::data::BadData>(module, "BadData", py::metaclass((PyObject *) &PyExc_Exception->ob_type))

还有更多我没有保存。但总体结果是 1) 它被完全忽略或 2) 编译失败或 3) 它编译然后在尝试创建实例时立即出现段错误或 ImportError'd。也可能有一个在模块导入时出现段错误。这一切都模糊在一起。也许有一些神奇的公式可以使这样的事情起作用,但我找不到它。从我对 pybind11 内部的阅读来看,我不相信这样的事情实际上是可能的。从原始 python 类型继承似乎不是它允许你做的事情。

我尝试的最后一件事似乎很聪明。我做了一个python异常类型

  static py::exception<::example::data::BadData> exc_BadData(module, "BadDataBase");

然后让我的 pybind11 class_ 从那里继承。

  py::class_< ::example::data::BadData >(module, "BadData", py::dynamic_attr(), exc_BadData)

但这在导入时也会出现段错误。所以我基本上回到了原点。

【问题讨论】:

    标签: python c++11 pybind11 python-bindings


    【解决方案1】:

    所以我想出了一种方法来实际执行此操作,但它涉及 1) 对 pybind11 代码本身进行一些黑客攻击,以及 2) 为绑定的 python 类型引入一些大小效率低下的问题。在我看来,尺寸问题是相当无关紧要的。是的,最好让所有东西都大小合适,但为了便于使用,我会占用一些额外的内存字节。但是,鉴于这种低效率,我不会将此作为 PR 提交给 pybind11 项目。虽然我认为权衡是值得的,但我怀疑将其作为大多数人的默认设置是否合乎需要。有可能,我想在 C++ 中将这个功能隐藏在 #define 后面,但这似乎是长期的超级混乱。可能有一个更好的长期答案,它会涉及到一定程度的模板元编程(在 class_ 的 python 容器类型上参数化),我只是没有达到。

    我在这里提供我的更改,作为与 git 中当前 master 分支的差异(哈希 a54eab92d265337996b8e4b4149d9176c2d428a6)。

    基本方法是

    1. 修改 pybind11 以允许为 class_ 实例指定异常基类。
    2. 修改 pybind11 的内部容器,使其具有 python 异常类型所需的额外字段
    3. 编写少量自定义绑定代码来处理在python中正确设置错误。

    对于第一部分,我在 type_record 中添加了一个新属性来指定类是否为异常,并添加了关联的 process_attribute 调用来解析它。

    diff --git a/src/pybind11/include/pybind11/attr.h b/src/pybind11/include/pybind11/attr.h
    index 58390239..b5535558 100644
    --- a/src/pybind11/include/pybind11/attr.h
    +++ b/src/pybind11/include/pybind11/attr.h
    @@ -73,6 +73,9 @@ struct module_local { const bool value; constexpr module_local(bool v = true) :
     /// Annotation to mark enums as an arithmetic type
     struct arithmetic { };
    
    +// Annotation that marks a class as needing an exception base type.
    +struct is_except {};
    +
     /** \rst
         A call policy which places one or more guard variables (``Ts...``) around the function call.
    
    @@ -211,7 +214,8 @@ struct function_record {
     struct type_record {
         PYBIND11_NOINLINE type_record()
             : multiple_inheritance(false), dynamic_attr(false), buffer_protocol(false),
     -          default_holder(true), module_local(false), is_final(false) { }
     -          default_holder(true), module_local(false), is_final(false),
     -          is_except(false) { }
    
         /// Handle to the parent scope
         handle scope;
    @@ -267,6 +271,9 @@ struct type_record {
         /// Is the class inheritable from python classes?
         bool is_final : 1;
    
     -    // Does the class need an exception base type?
     -    bool is_except : 1;
     -      PYBIND11_NOINLINE void add_base(const std::type_info &base, void *(*caster)(void *)) {
             auto base_info = detail::get_type_info(base, false);
             if (!base_info) {
    @@ -451,6 +458,11 @@ struct process_attribute<is_final> : process_attribute_default<is_final> {
         static void init(const is_final &, type_record *r) { r->is_final = true; }
     };
    
    +template <>
    +struct process_attribute<is_except> : process_attribute_default<is_except> {
     -    static void init(const is_except &, type_record *r) { r->is_except = true; }
    +};
    
    

    我修改了 internals.h 文件,为异常类型添加了一个单独的基类。我还为 make_object_base_type 添加了一个额外的 bool 参数。

    diff --git a/src/pybind11/include/pybind11/detail/internals.h b/src/pybind11/include/pybind11/detail/internals.h
    index 6224dfb2..d84df4f5 100644
    --- a/src/pybind11/include/pybind11/detail/internals.h
    +++ b/src/pybind11/include/pybind11/detail/internals.h
    @@ -16,7 +16,7 @@ NAMESPACE_BEGIN(detail)
     // Forward declarations
     inline PyTypeObject *make_static_property_type();
     inline PyTypeObject *make_default_metaclass();
    -inline PyObject *make_object_base_type(PyTypeObject *metaclass);
    +inline PyObject *make_object_base_type(PyTypeObject *metaclass, bool is_except);
    
     // The old Python Thread Local Storage (TLS) API is deprecated in Python 3.7 in favor of the new
     // Thread Specific Storage (TSS) API.
    @@ -107,6 +107,7 @@ struct internals {
         PyTypeObject *static_property_type;
         PyTypeObject *default_metaclass;
         PyObject *instance_base;
    +    PyObject *exception_base;
     #if defined(WITH_THREAD)
         PYBIND11_TLS_KEY_INIT(tstate);
         PyInterpreterState *istate = nullptr;
    @@ -292,7 +293,8 @@ PYBIND11_NOINLINE inline internals &get_internals() {
             internals_ptr->registered_exception_translators.push_front(&translate_exception);
             internals_ptr->static_property_type = make_static_property_type();
             internals_ptr->default_metaclass = make_default_metaclass();
    -        internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass);
    +        internals_ptr->instance_base = make_object_base_type(internals_ptr->default_metaclass, false);
    +        internals_ptr->exception_base = make_object_base_type(internals_ptr->default_metaclass, true);
    

    然后在class.h 中添加了必要的代码来生成异常基类型。第一个警告在这里。由于 PyExc_Exception 是一种垃圾收集类型,因此我必须确定检查该类型上 GC 标志的 assert 调用的范围。我目前没有看到此更改有任何不良行为,但这肯定会使保修失效。我强烈建议始终将py:dynamic_attr() 标志传递给您正在使用py:except 的任何类,因为这会打开所有必要的花里胡哨来正确处理GC(我认为)。更好的解决方案可能是在 make_object_base_type 中打开所有这些东西,而不必调用 py::dynamic_attr

    diff --git a/src/pybind11/include/pybind11/detail/class.h b/src/pybind11/include/pybind11/detail/class.h
    index a05edeb4..bbb9e772 100644
    --- a/src/pybind11/include/pybind11/detail/class.h
    +++ b/src/pybind11/include/pybind11/detail/class.h
    @@ -368,7 +368,7 @@ extern "C" inline void pybind11_object_dealloc(PyObject *self) {
     /** Create the type which can be used as a common base for all classes.  This is
         needed in order to satisfy Python's requirements for multiple inheritance.
         Return value: New reference. */
    -inline PyObject *make_object_base_type(PyTypeObject *metaclass) {
    +inline PyObject *make_object_base_type(PyTypeObject *metaclass, bool is_except=false) {
         constexpr auto *name = "pybind11_object";
         auto name_obj = reinterpret_steal<object>(PYBIND11_FROM_STRING(name));
    
    @@ -387,7 +387,12 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) {
    
         auto type = &heap_type->ht_type;
         type->tp_name = name;
    -    type->tp_base = type_incref(&PyBaseObject_Type);
    +    if (is_except) {
    +      type->tp_base = type_incref(reinterpret_cast<PyTypeObject*>(PyExc_Exception));
    +    }
    +    else {
    +      type->tp_base = type_incref(&PyBaseObject_Type);
    +    }
         type->tp_basicsize = static_cast<ssize_t>(sizeof(instance));
         type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HEAPTYPE;
    
    @@ -404,7 +409,9 @@ inline PyObject *make_object_base_type(PyTypeObject *metaclass) {
         setattr((PyObject *) type, "__module__", str("pybind11_builtins"));
         PYBIND11_SET_OLDPY_QUALNAME(type, name_obj);
    
    -    assert(!PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
    +    if (!is_except) {
    +      assert(!PyType_HasFeature(type, Py_TPFLAGS_HAVE_GC));
    +    }
         return (PyObject *) heap_type;
     }
    
    @@ -565,7 +572,8 @@ inline PyObject* make_new_python_type(const type_record &rec) {
    
         auto &internals = get_internals();
         auto bases = tuple(rec.bases);
    -    auto base = (bases.size() == 0) ? internals.instance_base
    +    auto base = (bases.size() == 0) ? (rec.is_except ? internals.exception_base
    +                                                     : internals.instance_base)
    

    然后是最后的变化,也就是低效率的部分。在 Python 中,一切都是 PyObject,但实际上只有两个字段(使用 PyObject_HEAD 宏设置),实际的对象结构可能有很多额外的字段。并且有一个非常精确的布局很重要,因为 python 有时使用offsetof 来寻找这些东西。从 Python 2.7 源代码(Include/pyerrord.h)中,您可以看到用于基本异常的结构

    typedef struct {
        PyObject_HEAD
        PyObject *dict;
        PyObject *args;
        PyObject *message;
    } PyBaseExceptionObject;
    

    任何扩展 PyExc_Exception 的 pybind11 类型都必须有一个包含相同初始布局的实例结构。目前在 pybind11 中,实例结构只有PyObject_HEAD。这意味着如果您不更改 instance 结构,这将全部编译,但是当 python 搜索该对象时,它会假设存在软管额外字段,然后它将在可用内存结束时立即搜索你会得到各种有趣的段错误。因此,此更改将这些额外字段添加到 pybind11 中的每个 class_。拥有这些额外的字段似乎并没有破坏正常的类,而且它肯定似乎使异常正常工作。如果我们之前违反了保修条款,我们只会将其撕毁并点燃。

    diff --git a/src/pybind11/include/pybind11/detail/common.h b/src/pybind11/include/pybind11/detail/common.h
    index dd626793..b32e0c70 100644
    --- a/src/pybind11/include/pybind11/detail/common.h
    +++ b/src/pybind11/include/pybind11/detail/common.h
    @@ -392,6 +392,10 @@ struct nonsimple_values_and_holders {
     /// The 'instance' type which needs to be standard layout (need to be able to use 'offsetof')
     struct instance {
         PyObject_HEAD
    +    // Necessary to support exceptions.
    +    PyObject *dict;
    +    PyObject *args;
    +    PyObject *message;
         /// Storage for pointers and holder; see simple_layout, below, for a description
    

    但是,一旦完成这些更改,您可以执行以下操作。 在类中绑定

     auto PyBadData = py::class_< ::example::data::BadData>(module, "BadData", py::is_except(), py::dynamic_attr())
        .def(py::init<>())
        .def(py::init< std::string, std::string >())
        .def("__str__", &::example::data::BadData::toString)
        .def("getStack", &::example::data::BadData::getStack)
        .def_property("message", &::example::data::BadData::getMsg, &::example::data::BadData::setMsg)
        .def("getMsg", &::example::data::BadData::getMsg);
    

    并在 c++ 中获取一个引发异常的函数

    void raiseMe()
    {
      throw ::example::data::BadData("this is an error", "");
    }
    

    并把它也绑定进去

    module.def("raiseMe", &raiseMe, "A function throws");
    

    添加异常翻译器,将整个python类型放入异常中

        py::register_exception_translator([](std::exception_ptr p) {
          try {
              if (p) {
                std::rethrow_exception(p);
              }
          } catch (const ::example::data::BadData &e) {
            auto err = py::cast(e);
            auto errType = err.get_type().ptr();
            PyErr_SetObject(errType, err.ptr());
          }
        });
    

    然后你会得到你想要的所有东西!

    >>> import example
    >>> example.raiseMe()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    example.BadData: BadData(msg=this is an error, stack=)
    

    当然,您也可以从 python 实例化并引发异常

    >>> import example
    >>> raise example.BadData("this is my error", "no stack")
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    example.BadData: BadData(msg=this is my error, stack=no stack)
    

    【讨论】:

      猜你喜欢
      • 2013-01-26
      • 1970-01-01
      • 1970-01-01
      • 2013-05-15
      • 1970-01-01
      • 2013-04-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多