Как вы можете связать исключения с настраиваемыми полями и конструкторами в pybind11 и по-прежнему использовать их как исключение python? - PullRequest
0 голосов
/ 29 мая 2020

Похоже, это известное ограничение в pybind11. Я прочитал все документы, любые отчеты об ошибках, которые казались подходящими, и все, что я мог найти в gitter pybind11. У меня есть собственный класс исключений в С ++, который содержит настраиваемые конструкторы и поля. Вот очень простой пример 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 перевод между исключением C ++ и исключением python, при этом значение what() исключения C ++ преобразуется в message исключения python. Однако все дополнительные данные из исключения 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);

Потому что нет автоматического c перевода между BadData в c ++ и python. Вы можете попытаться обойти это, привязав лямбда:

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

obj.cast также не работает, потому что нет автоматического преобразования c. По сути, из-за отсутствия места для хранения экземпляра 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 при попытке создать экземпляр. Возможно, при импорте модуля произошел сбой. Все это размывается. Может быть, есть какая-то формула magi c, которая заставит такую ​​штуку работать, но я не смог ее найти. Изучая внутреннее устройство 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 Ответ

0 голосов
/ 29 мая 2020

Итак, я придумал, как это сделать, но он включает в себя 1) взлом самого кода pybind11 и 2) внесение некоторой неэффективности в размер связанных типов python. С моей точки зрения, размер проблемы не имеет значения. Да, было бы лучше, чтобы все было идеально по размеру, но я возьму дополнительные байты памяти для удобства использования. Однако, учитывая эту неэффективность, я не отправляю это в качестве PR проекту pybind11. Хотя я думаю, что компромисс того стоит, я сомневаюсь, что сделать это по умолчанию для большинства людей было бы желательно. Было бы возможно, я думаю, скрыть эту функциональность за #define в c ++, но похоже, что это будет очень грязно в долгосрочной перспективе. Вероятно, есть лучший долгосрочный ответ, который включал бы определенную степень метапрограммирования шаблонов (параметризацию типа контейнера python для class_), к которому я просто не дошел.

Здесь я представляю свои изменения в виде различий с текущей master веткой в ​​git, когда это было написано (ha sh a54eab92d265337996b8e4b4149d9176c2d428a6).

Базовый подход c было

  1. Измените pybind11, чтобы разрешить спецификацию базового класса исключения для экземпляра class_.
  2. Измените внутренний контейнер pybind11, чтобы иметь дополнительные поля, необходимые для исключения python type
  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, чтобы добавить отдельный базовый класс для типов исключений. Я также добавил дополнительный аргумент bool в make_object_base_type.

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, который проверял флаг G C на типе. В настоящее время я не замечал плохого поведения в результате этого изменения, но это определенно аннулирует гарантию прямо здесь. Я очень, очень рекомендую всегда передавать флаг py:dynamic_attr() любым классам, которые вы используете py:except, поскольку это включает все необходимые навороты для правильной обработки G C (я думаю). Лучшим решением может быть включение всех этих вещей в 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;

Любой тип 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);

И взять функцию на С ++, которая генерирует исключение

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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...