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

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

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

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() 值转换为 [=55= 的 message ] 例外。然而,来自 c++ 异常的所有额外数据都丢失了,如果你试图在 python 中抛出异常并在 c++ 中捕获它,你不能将它与任何额外数据一起抛出。

您可以使用 attr 将额外的数据绑定到 python 对象上,我什至尝试扩展 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;
    }
  };

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

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

因为c++中的BadData和python之间没有自动转换。您可以尝试通过在 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 正常 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。也可能有一个在模块导入时出现段错误。这一切都模糊在一起。也许有一些神奇的公式可以使这样的事情起作用,但我找不到它。从我对 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)

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

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

我在这里提供了我的更改,作为与 git 中当前 master 分支的差异,当它被写入时(散列 a54eab92d265337996b8e4b4149d9176c2d428a6)。

基本方法是

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

对于第一部分,我向 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; }
+};

我修改了 internals.h 文件,为异常类型添加了一个单独的基础 class。我还向 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 是一种垃圾收集类型,我必须确定 assert 调用的范围以检查该类型的 GC 标志。我目前还没有看到此更改有任何不良行为,但这肯定会使保修失效。我强烈建议始终将 py:dynamic_attr() 标志传递给您正在使用 py:except 的任何 classes,因为这会打开所有必要的功能以正确处理 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_。拥有这些额外的字段似乎并没有破坏正常的 classes,而且它似乎确实可以使异常正常工作。如果我们之前破坏了保修,我们只是将其撕毁并点燃它。

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

但是,完成所有这些更改后,您可以执行以下操作。 在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);

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