简体   繁体   English

如何在 pybind11 中将异常与自定义字段和构造函数绑定,并且仍然将它们 function 作为 python 异常?

[英]How can you bind exceptions with custom fields and constructors in pybind11 and still have them function as python exception?

This appears to be a known limitation in pybind11.这似乎是 pybind11 中的已知限制。 I read through all the docs, whatever bug reports seemed applicable, and everything I could find in the pybind11 gitter.我通读了所有文档,任何似乎适用的错误报告,以及我在 pybind11 gitter 中可以找到的所有内容。 I have a custom exception class in c++ that contains custom constructors and fields.我在 c++ 中有一个自定义异常 class,其中包含自定义构造函数和字段。 A very basic example of such a class, trimmed for space is here:这种 class 的一个非常基本的例子,这里是空间修剪:

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;

I currently have python binding code that binds this into python, but it is custom generated and we'd much rather use pybind11 due to its simplicity and compile speed.我目前有 python 绑定代码将它绑定到 python,但它是自定义生成的,我们更愿意使用 pybind11,因为它的简单性和编译速度。

The default mechanism for binding an exception into pybind11 would look like将异常绑定到 pybind11 的默认机制看起来像

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

That will create an automatic translation between the C++ exception and the python exception, with the what() value of the c++ exception translating into the message of the python exception.这将在 C++ 异常和 python 异常之间创建自动转换,其中 c++ 异常的what()值转换为 python 异常的message However, all the extra data from the c++ exception is lost and if you're trying to throw the exception in python and catch it in c++, you cannot throw it with any of the extra data.但是,来自 c++ 异常的所有额外数据都丢失了,如果您试图在 python 中抛出异常并在 c++ 中捕获它,则不能将其与任何额外数据一起抛出。

You can bind extra data onto the python object using the attr and I even went somewhat down the path of trying to extend the pybind11:exception class to make it easier to add custom fields to exceptions.您可以使用attr将额外数据绑定到 python object 上,我什至尝试扩展 pybind11:exception class 以便更轻松地向异常添加自定义字段。

  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;
    }
  };

This adds a def function to exceptions similar to what is done with class_ .这将def function 添加到异常中,类似于对class_所做的。 The naive approach to using this doesn't work使用这个的天真方法不起作用

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

Because there is no automatic translation between BadData in c++ and in python. You can try to work around this by binding in a lambda:因为BadData在 c++ 和 python 之间没有自动转换。您可以尝试通过绑定 lambda 来解决这个问题:

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

The obj.cast there also fails because there is no automatic conversion.那里的obj.cast也失败了,因为没有自动转换。 Basically, with no place to store the c++ instance, there isn't really a workable solution for this approach that I could find.基本上,由于没有地方存储 c++ 实例,因此我找不到适用于这种方法的真正可行的解决方案。 In addition I couldn't find a way to bind in custom constructors at all, which made usability on python very weak.此外,我根本找不到绑定自定义构造函数的方法,这使得 python 的可用性非常弱。

The next attempt was based on a suggestion in the pybind11 that you could use the python exception type as a metaclass a normal class_ and have python recognize it as a valid exception.下一次尝试基于 pybind11 中的建议,即您可以将 python 异常类型用作普通类的元类,并让class_将其识别为有效异常。 I tried a plethora of variations on this approach.我尝试了这种方法的多种变体。

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))

There were more that I don't have saved.还有更多我没有保存的。 But the overall results was either 1) It was ignored completely or 2) it failed to compile or 3) It compiled and then immediately segfaulted or ImportError'd when trying to make an instance.但总体结果要么是 1) 它被完全忽略,要么 2) 编译失败,或者 3) 它编译后立即出现段错误或在尝试创建实例时出现 ImportError。 There might have been one that segfaulted on module import too.也可能有一个在模块导入时出现段错误。 It all blurs together.这一切都模糊在一起。 Maybe there is some magic formula that would make such a thing work, but I was unable to find it.也许有一些神奇的公式可以使这样的事情起作用,但我找不到它。 From my reading of the pybind11 internals, I do not believe that such a thing is actually possible.从我对 pybind11 内部的阅读来看,我不相信这样的事情实际上是可能的。 Inheriting from a raw python type does not seem to be something it is setup to let you do.从原始 python 类型继承似乎不是它允许您执行的设置。

The last thing I tried seemed really clever.我尝试的最后一件事似乎真的很聪明。 I made a python exception type我做了一个 python 异常类型

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

and then had my pybind11 class_ inherit from that.然后让我的 pybind11 class_继承自它。

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

But that also segfaulted on import too.但这也会在导入时出现段错误。 So I'm basically back to square one with this.所以我基本上回到了原点。

So I figured out a way to actually do this but it involves 1) doing some hacking of the pybind11 code itself and 2) introducing some size inefficiencies to the bound python types.所以我想出了一种实际执行此操作的方法,但它涉及 1) 对 pybind11 代码本身进行一些黑客攻击,以及 2) 对绑定的 python 类型引入一些大小低效的问题。 From my point of view, the size issues are fairly immaterial.在我看来,尺寸问题并不重要。 Yes it would be better to have everything perfectly sized but I'll take some extra bytes of memory for ease of use.是的,最好让所有的东西都大小合适,但为了便于使用,我会多加一些 memory 字节。 Given this inefficiency, though, I'm not submitting this as a PR to the pybind11 project.不过,鉴于这种低效率,我不会将其作为 PR 提交给 pybind11 项目。 While I think the trade-off is worth it, I doubt that making this the default for most people would be desired.虽然我认为这种权衡是值得的,但我怀疑是否需要将其设为大多数人的默认设置。 It would be possible, I guess to hide this functionality behind a #define in c++ but that seems like it would be super messy long-term.有可能,我想将此功能隐藏在 c++ 中的#define后面,但从长远来看,这似乎会非常混乱。 There is probably a better long-term answer that would involve a degree of template meta-programming (parameterizing on the python container type for class_ ) that I'm just not up to.可能有一个更好的长期答案,它涉及一定程度的模板元编程(在 python 容器类型上为class_进行参数化),而我只是做不到。

I'm providing my changes here as diffs against the current master branch in git when this was written (hash a54eab92d265337996b8e4b4149d9176c2d428a6).我在这里提供我的更改作为与 git 中当前master分支的差异,当它被写入时(散列 a54eab92d265337996b8e4b4149d9176c2d428a6)。

The basic approach was基本方法是

  1. Modify pybind11 to allow the specification of an exception base class for a class_ instance.修改 pybind11 以允许为class_实例指定异常基数 class。
  2. Modify pybind11's internal container to have the extra fields needed for a python exception type修改 pybind11 的内部容器以具有 python 异常类型所需的额外字段
  3. Write a small amount of custom binding code to handle setting the error correctly in python.编写少量自定义绑定代码来正确处理python中的设置错误。

For the first part, I added a new attribute to type_record to specify if a class is an exception and added the associated process_attribute call for parsing it.对于第一部分,我向 type_record 添加了一个新属性以指定 class 是否为异常,并添加了关联的 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; }
+};

I modified the internals.h file to add a separate base class for exception types.我修改了 internals.h 文件,为异常类型添加了一个单独的基础 class。 I also added an extra bool argument to make_object_base_type.我还向 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);

And then in class.h I added the necessary code to generate the exception base type.然后在class.h中添加了必要的代码来生成异常基类型。 The first caveat is here.第一个警告就在这里。 Since PyExc_Exception is a garbage collected type, I had to scope the assert call that checked the GC flag on the type.由于 PyExc_Exception 是一种垃圾收集类型,我必须 scope assert调用检查类型上的 GC 标志。 I have not currently seen any bad behavior from this change, but this is definitely voiding the warranty right here.我目前还没有看到此更改有任何不良行为,但这肯定会使保修失效。 I would highly, highly recommend always passing the py:dynamic_attr() flag to any classes you are using py:except on, since that turns on all the necessary bells and whistles to handle GC correctly (I think).我强烈建议始终将py:dynamic_attr()标志传递给您正在使用py:except on 的任何类,因为这会打开所有必要的功能以正确处理 GC(我认为)。 A better solution might be to turn all those things on in make_object_base_type without having to invoke py::dynamic_attr .更好的解决方案可能是在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)

And then the final change, which is the inefficiency part.然后是最后的改变,也就是效率低下的部分。 In Python, everything is a PyObject, but that is really only two fields (setup with the PyObject_HEAD macro) and the actual object struct may have a lot of extra fields.在 Python 中,一切都是 PyObject,但这实际上只有两个字段(使用 PyObject_HEAD 宏设置),实际的 object 结构可能有很多额外的字段。 And having a very precise layout is important because python uses offsetof to seek into these things some times.拥有一个非常精确的布局很重要,因为 python 有时会使用offsetof来寻找这些东西。 From the Python 2.7 source code (Include/pyerrord.h) you can see the struct that is used for base exceptions从 Python 2.7 源代码 (Include/pyerrord.h) 可以看到用于基本异常的结构

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

Any pybind11 type that extends PyExc_Exception has to have a instance struct that contains the same initial layout.任何扩展PyExc_Exception的 pybind11 类型都必须有一个包含相同初始布局的实例结构。 And in pybind11 currently, the instance struct just has PyObject_HEAD .目前在 pybind11 中,实例结构只有PyObject_HEAD That means if you don't change the instance struct, this will all compile, but when python seeks into this object, it will do with the assumption that hose extra fields exist and then it will seek right off the end of viable memory and you'll get all sorts of fun segfaults.这意味着如果你不改变instance结构,这将全部编译,但是当 python 寻找这个 object 时,它将假设软管额外字段存在,然后它会立即寻找可行的 memory 的末尾,你会遇到各种有趣的段错误。 So this change adds those extra fields to every class_ in pybind11.因此,此更改将这些额外字段添加到 pybind11 中的每个class_ It does not seem to break normal classes to have these extra fields and it definitely seems to make exceptions work correctly.它似乎并没有破坏正常的类来拥有这些额外的字段,而且它似乎确实可以使异常正常工作。 If we broke the warranty before, we just tore it up and lit it on fire.如果我们之前违反保修条款,我们只是将其撕毁并点燃。

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

However, once these changes are all made, here is what you can do.但是,完成所有这些更改后,您可以执行以下操作。 Bind in the class绑定在class

 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);

And take a function in c++ that throws the exception并在抛出异常的c++中取一个function

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

and bind that in too并将其绑定

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

Add an exception translator to put the entire python type into the exception添加异常翻译器,将整个 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());
      }
    });

And then you get all the things you could want!然后你会得到所有你想要的东西!

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

You can, of course, also instantiate and raise the exception from python as well当然,您也可以从 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)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM