1

这似乎是 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 异常之间创建一个自动转换,并将what()c++ 异常的值转换为messagepython 异常的值。但是,来自 c++ 异常的所有额外数据都将丢失,如果您尝试在 python 中抛出异常并在 c++ 中捕获它,则无法将其与任何额外数据一起抛出。

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

BadData因为在 c++ 和 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_并让 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) 它编译然后在尝试创建实例时立即出现 segfaulted 或 ImportError'd。也可能有一个在模块导入时出现段错误。这一切都模糊在一起。也许有一些神奇的公式可以使这样的事情起作用,但我找不到它。从我对 pybind11 内部的阅读来看,我不相信这样的事情实际上是可能的。从原始 python 类型继承似乎不是它允许你做的事情。

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

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

然后让我的 pybind11class_继承。

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

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

4

1 回答 1

0

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

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

基本方法是

  1. class_修改 pybind11 以允许为实例指定异常基类。
  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 是一种垃圾收集类型,因此我必须assert确定检查该类型上 GC 标志的调用的范围。我目前没有看到此更改有任何不良行为,但这肯定会使保修失效。我强烈建议您始终将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 宏设置),实际的对象结构可能有很多额外的字段。并且有一个非常精确的布局很重要,因为 pythonoffsetof有时会使用这些东西来寻找这些东西。从 Python 2.7 源代码 (Include/pyerrord.h) 您可以看到用于基本异常的结构

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

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

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)
于 2020-05-29T14:15:12.687 回答